diff --git a/ee/app/assets/javascripts/security_dashboard/components/app.vue b/ee/app/assets/javascripts/security_dashboard/components/app.vue index 00b4e36fed34fa59b43738136c57121e124098fa..f250cdb650d27fb720ff4a0aec3d375403d22fe7 100644 --- a/ee/app/assets/javascripts/security_dashboard/components/app.vue +++ b/ee/app/assets/javascripts/security_dashboard/components/app.vue @@ -6,6 +6,7 @@ import Tabs from '~/vue_shared/components/tabs/tabs'; import Tab from '~/vue_shared/components/tabs/tab.vue'; import IssueModal from 'ee/vue_shared/security_reports/components/modal.vue'; import SecurityDashboardTable from './security_dashboard_table.vue'; +import VulnerabilityChart from './vulnerability_chart.vue'; import VulnerabilityCountList from './vulnerability_count_list.vue'; import Icon from '~/vue_shared/components/icon.vue'; import popover from '~/vue_shared/directives/popover'; @@ -21,6 +22,7 @@ export default { SecurityDashboardTable, Tab, Tabs, + VulnerabilityChart, VulnerabilityCountList, }, props: { @@ -40,6 +42,10 @@ export default { type: String, required: true, }, + vulnerabilitiesHistoryEndpoint: { + type: String, + required: true, + }, vulnerabilityFeedbackHelpPath: { type: String, required: true, @@ -73,15 +79,20 @@ export default { html: true, }; }, + chartFlagEnabled() { + return gon.features && gon.features.groupSecurityDashboardHistory; + }, }, created() { this.setVulnerabilitiesEndpoint(this.vulnerabilitiesEndpoint); this.setVulnerabilitiesCountEndpoint(this.vulnerabilitiesCountEndpoint); + this.setVulnerabilitiesHistoryEndpoint(this.vulnerabilitiesHistoryEndpoint); this.fetchVulnerabilitiesCount(); }, methods: { ...mapActions('vulnerabilities', [ 'setVulnerabilitiesCountEndpoint', + 'setVulnerabilitiesHistoryEndpoint', 'setVulnerabilitiesEndpoint', 'fetchVulnerabilitiesCount', 'createIssue', @@ -108,7 +119,11 @@ export default { -
{{ __('Vulnerability List') }}
+ +

{{ __('Vulnerability List') }}

+import dateFormat from 'dateformat'; +import { mapState, mapActions } from 'vuex'; +import { GlChart } from '@gitlab/ui'; +import ChartTooltip from './vulnerability_chart_tooltip.vue'; + +export default { + name: 'VulnerabilityChart', + components: { + GlChart, + ChartTooltip, + }, + data: () => ({ + tooltipTitle: '', + tooltipEntries: [], + lines: [ + { + name: 'Critical', + color: '#C0341D', + }, + { + name: 'High', + color: '#DE7E00', + }, + { + name: 'Medium', + color: '#6E49CB', + }, + { + name: 'Low', + color: '#4F4F4F', + }, + { + name: 'Total', + color: '#1F78D1', + }, + ], + }), + computed: { + ...mapState('vulnerabilities', ['vulnerabilitiesHistory']), + series() { + return this.lines.map(line => { + const { name, color } = line; + const history = this.vulnerabilitiesHistory[name.toLowerCase()]; + const data = history ? Object.entries(history) : []; + + return { + borderWidth: 2, + color, + data, + name, + symbol: 'circle', + symbolSize: 6, + type: 'line', + }; + }); + }, + options() { + return { + grid: { + bottom: 85, + left: 75, + right: 15, + top: 10, + }, + tooltip: { + backgroundColor: '#fff', + borderColor: 'rgba(0, 0, 0, 0.1)', + borderWidth: 1, + confine: true, + formatter: this.renderTooltip, + padding: 0, + textStyle: { + color: '#4F4F4F', + }, + trigger: 'axis', + }, + xAxis: { + axisLabel: { + color: '#707070', + formatter: date => dateFormat(date, 'd mmm'), + margin: 8, + rotate: 45, + }, + axisLine: { + lineStyle: { + color: '#dedede', + width: 2, + }, + }, + axisTick: { + show: false, + }, + maxInterval: 1000 * 60 * 60 * 24 * 7, + min: Date.now() - 1000 * 60 * 60 * 24 * 28, + name: 'Date', + nameGap: 50, + nameLocation: 'center', + nameTextStyle: { + color: '#2e2e2e', + fontWeight: 'bold', + }, + splitNumber: 12, + type: 'time', + }, + yAxis: { + axisLabel: { + color: '#707070', + }, + axisLine: { + lineStyle: { + color: '#dedede', + width: 2, + }, + }, + axisTick: { + show: false, + }, + interval: 25, + name: 'Vulnerabilities', + nameGap: 42, + nameLocation: 'center', + nameRotation: 90, + nameTextStyle: { + color: '#2e2e2e', + fontWeight: 'bold', + }, + type: 'value', + }, + legend: { + bottom: 0, + icon: 'path://M0,0H120V40H0Z', + itemGap: 15, + left: 70, + textStyle: { + color: '#4F4F4F', + fontWeight: 'bold', + }, + type: 'scroll', + }, + series: this.series, + }; + }, + }, + created() { + this.fetchVulnerabilitiesHistory(); + }, + methods: { + ...mapActions('vulnerabilities', ['fetchVulnerabilitiesHistory']), + renderTooltip(params, ticket, callback) { + this.tooltipTitle = dateFormat(params[0].axisValue, 'd mmmm'); + this.tooltipEntries = params; + this.$nextTick(() => callback(ticket, this.$refs.tooltip.$el.innerHTML)); + return ' '; + }, + }, +}; + + + diff --git a/ee/app/assets/javascripts/security_dashboard/components/vulnerability_chart_label.vue b/ee/app/assets/javascripts/security_dashboard/components/vulnerability_chart_label.vue new file mode 100644 index 0000000000000000000000000000000000000000..3863045591fc557d13ca24d0e8531bc00bbc3887 --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/components/vulnerability_chart_label.vue @@ -0,0 +1,28 @@ + + + diff --git a/ee/app/assets/javascripts/security_dashboard/components/vulnerability_chart_tooltip.vue b/ee/app/assets/javascripts/security_dashboard/components/vulnerability_chart_tooltip.vue new file mode 100644 index 0000000000000000000000000000000000000000..11894a11c6a689bf74189680a05cd35618bda250 --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/components/vulnerability_chart_tooltip.vue @@ -0,0 +1,38 @@ + + + diff --git a/ee/app/assets/javascripts/security_dashboard/index.js b/ee/app/assets/javascripts/security_dashboard/index.js index af45b79524aed72dd3bcd3e590abda95a025d368..d421fc1569a174d965ad314e2ccb8846f53e2c74 100644 --- a/ee/app/assets/javascripts/security_dashboard/index.js +++ b/ee/app/assets/javascripts/security_dashboard/index.js @@ -19,6 +19,7 @@ export default () => { vulnerabilityFeedbackHelpPath: el.dataset.vulnerabilityFeedbackHelpPath, vulnerabilitiesEndpoint: el.dataset.vulnerabilitiesEndpoint, vulnerabilitiesCountEndpoint: el.dataset.vulnerabilitiesSummaryEndpoint, + vulnerabilitiesHistoryEndpoint: el.dataset.vulnerabilitiesHistoryEndpoint, }, }); }, diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/actions.js b/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/actions.js index adb8a1597c000a456114ca0a9d4c2925649179f0..06f8fe9675e695a4d15337727a921ce9a4fd7ed5 100644 --- a/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/actions.js +++ b/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/actions.js @@ -204,4 +204,38 @@ export const receiveRevertDismissalError = ({ commit }, { flashError }) => { } }; +export const setVulnerabilitiesHistoryEndpoint = ({ commit }, endpoint) => { + commit(types.SET_VULNERABILITIES_HISTORY_ENDPOINT, endpoint); +}; + +export const fetchVulnerabilitiesHistory = ({ state, dispatch }) => { + dispatch('requestVulnerabilitiesHistory'); + + axios({ + method: 'GET', + url: state.vulnerabilitiesHistoryEndpoint, + }) + .then(response => { + const { data } = response; + dispatch('receiveVulnerabilitiesHistorySuccess', { data }); + }) + .catch(() => { + dispatch('receiveVulnerabilitiesHistoryError'); + }); +}; + +export const requestVulnerabilitiesHistory = ({ commit }) => { + commit(types.REQUEST_VULNERABILITIES_HISTORY); +}; + +export const receiveVulnerabilitiesHistorySuccess = ({ commit }, { data }) => { + commit(types.RECEIVE_VULNERABILITIES_HISTORY_SUCCESS, data); +}; + +export const receiveVulnerabilitiesHistoryError = ({ commit }) => { + commit(types.RECEIVE_VULNERABILITIES_HISTORY_ERROR); +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +// This is no longer needed after gitlab-ce#52179 is merged export default () => {}; diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/mutation_types.js b/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/mutation_types.js index e3d2c93aebc29e1c221f0d2b1b5a8d74ac75182e..823ad75a670bbf1d4d6c6f63276b8ec8614bf0c7 100644 --- a/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/mutation_types.js +++ b/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/mutation_types.js @@ -8,6 +8,11 @@ export const REQUEST_VULNERABILITIES_COUNT = 'REQUEST_VULNERABILITIES_COUNT'; export const RECEIVE_VULNERABILITIES_COUNT_SUCCESS = 'RECEIVE_VULNERABILITIES_COUNT_SUCCESS'; export const RECEIVE_VULNERABILITIES_COUNT_ERROR = 'RECEIVE_VULNERABILITIES_COUNT_ERROR'; +export const SET_VULNERABILITIES_HISTORY_ENDPOINT = 'SET_VULNERABILITIES_HISTORY_ENDPOINT'; +export const REQUEST_VULNERABILITIES_HISTORY = 'REQUEST_VULNERABILITIES_HISTORY'; +export const RECEIVE_VULNERABILITIES_HISTORY_SUCCESS = 'RECEIVE_VULNERABILITIES_HISTORY_SUCCESS'; +export const RECEIVE_VULNERABILITIES_HISTORY_ERROR = 'RECEIVE_VULNERABILITIES_HISTORY_ERROR'; + export const SET_MODAL_DATA = 'SET_MODAL_DATA'; export const REQUEST_CREATE_ISSUE = 'REQUEST_CREATE_ISSUE'; diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/mutations.js b/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/mutations.js index 757bc1bb9ad1c417c8658f4e3a3918eee1af8feb..ff3f40d88dcb7ecf4d3f3798ac15097321acf0a8 100644 --- a/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/mutations.js +++ b/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/mutations.js @@ -35,6 +35,21 @@ export default { state.isLoadingVulnerabilitiesCount = false; state.errorLoadingVulnerabilitiesCount = true; }, + [types.SET_VULNERABILITIES_HISTORY_ENDPOINT](state, payload) { + state.vulnerabilitiesHistoryEndpoint = payload; + }, + [types.REQUEST_VULNERABILITIES_HISTORY](state) { + state.isLoadingVulnerabilitiesHistory = true; + state.errorLoadingVulnerabilitiesHistory = false; + }, + [types.RECEIVE_VULNERABILITIES_HISTORY_SUCCESS](state, payload) { + state.isLoadingVulnerabilitiesHistory = false; + state.vulnerabilitiesHistory = payload; + }, + [types.RECEIVE_VULNERABILITIES_HISTORY_ERROR](state) { + state.isLoadingVulnerabilitiesHistory = false; + state.errorLoadingVulnerabilitiesHistory = true; + }, [types.SET_MODAL_DATA](state, payload) { const { vulnerability } = payload; diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/state.js b/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/state.js index 7a642b99ca829314317316d0da843b0bed90c4d2..5ef220e3ba8bd545cb104a1066c5d1ffe386eeac 100644 --- a/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/state.js +++ b/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/state.js @@ -3,12 +3,16 @@ import { s__ } from '~/locale'; export default () => ({ isLoadingVulnerabilities: true, errorLoadingVulnerabilities: false, + vulnerabilities: [], isLoadingVulnerabilitiesCount: true, errorLoadingVulnerabilitiesCount: false, - pageInfo: {}, - vulnerabilities: [], vulnerabilitiesCount: {}, + isLoadingVulnerabilitiesHistory: true, + errorLoadingVulnerabilitiesHistory: false, + vulnerabilitiesHistory: {}, + pageInfo: {}, vulnerabilitiesCountEndpoint: null, + vulnerabilitiesHistoryEndpoint: null, vulnerabilitiesEndpoint: null, activeVulnerability: null, modal: { diff --git a/ee/app/assets/stylesheets/components/vulnerability_chart.scss b/ee/app/assets/stylesheets/components/vulnerability_chart.scss new file mode 100644 index 0000000000000000000000000000000000000000..a3f9c419480eaf70fc3df4c3b7b845d89fab0ac1 --- /dev/null +++ b/ee/app/assets/stylesheets/components/vulnerability_chart.scss @@ -0,0 +1,23 @@ +$trans-white: rgba(255, 255, 255, 0); + +.vulnerabilities-chart-wrapper { + -webkit-overflow-scrolling: touch; + overflow: scroll; +} + +@media screen and (max-width: 1240px) { + .vulnerabilities-chart { + position: relative; + } + + .vulnerabilities-chart::after { + background-image: linear-gradient(to right, $trans-white, $gl-gray-350); + bottom: 0; + content: ''; + height: 310px; + position: absolute; + right: -1px; + top: 10px; + width: 32px; + } +} diff --git a/ee/app/controllers/groups/security/dashboard_controller.rb b/ee/app/controllers/groups/security/dashboard_controller.rb index c9b0d7f0820ff367b8814148564c0979fc308eff..c4a7d0b85c38bf4538f0ac0d7ec2290b708789ff 100644 --- a/ee/app/controllers/groups/security/dashboard_controller.rb +++ b/ee/app/controllers/groups/security/dashboard_controller.rb @@ -1,4 +1,8 @@ # frozen_string_literal: true class Groups::Security::DashboardController < Groups::Security::ApplicationController layout 'group' + + before_action do + push_frontend_feature_flag(:group_security_dashboard_history, group) + end end diff --git a/ee/app/views/groups/security/dashboard/show.html.haml b/ee/app/views/groups/security/dashboard/show.html.haml index b435b1b2e8bba6e23e33e17eabfae1d477dfe475..1d9ff08a1c86bc6d2ca35c85937df9c7cbdd5b7c 100644 --- a/ee/app/views/groups/security/dashboard/show.html.haml +++ b/ee/app/views/groups/security/dashboard/show.html.haml @@ -1,8 +1,10 @@ - breadcrumb_title _("Security Dashboard") - page_title _("Security Dashboard") +- vulnerabilities_history_endpoint = Feature.enabled?(:group_security_dashboard_history, @group) ? history_group_security_vulnerabilities_path(@group) : '' #js-group-security-dashboard{ data: { vulnerabilities_endpoint: group_security_vulnerabilities_path(@group), vulnerabilities_summary_endpoint: summary_group_security_vulnerabilities_path(@group), + vulnerabilities_history_endpoint: vulnerabilities_history_endpoint, vulnerability_feedback_help_path: help_page_path("user/project/merge_requests/index", anchor: "interacting-with-security-reports-ultimate"), empty_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'), dashboard_documentation: help_page_path('user/group/security_dashboard/index') } } diff --git a/ee/changelogs/unreleased/6954-dashboard-chart.yml b/ee/changelogs/unreleased/6954-dashboard-chart.yml new file mode 100644 index 0000000000000000000000000000000000000000..e18ec71e645c00b17d4417eb502bb1d3fb0b8d20 --- /dev/null +++ b/ee/changelogs/unreleased/6954-dashboard-chart.yml @@ -0,0 +1,5 @@ +--- +title: Adds group security dashboard metrics chart +merge_request: 8631 +author: +type: added diff --git a/ee/spec/javascripts/security_dashboard/components/vulnerability_chart_label_spec.js b/ee/spec/javascripts/security_dashboard/components/vulnerability_chart_label_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..03c07c845e9a07cce1b79b5c8abbb68f42829a3e --- /dev/null +++ b/ee/spec/javascripts/security_dashboard/components/vulnerability_chart_label_spec.js @@ -0,0 +1,70 @@ +import Vue from 'vue'; +import component from 'ee/security_dashboard/components/vulnerability_chart_label.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; + +function hexToRgb(hex) { + const cleanHex = hex.replace('#', ''); + const [r, g, b] = [ + cleanHex.substring(0, 2), + cleanHex.substring(2, 4), + cleanHex.substring(4, 6), + ].map(rgb => parseInt(rgb, 16)); + + return `rgb(${r}, ${g}, ${b})`; +} + +describe('Vulnerability Chart Label component', () => { + const Component = Vue.extend(component); + let vm; + const props = { + name: 'Chuck Norris', + color: '#BADA55', + value: 42, + }; + + describe('default', () => { + beforeEach(() => { + vm = mountComponent(Component, props); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render the name', () => { + const name = vm.$el.querySelector('.js-name'); + + expect(name.textContent).toContain(props.name); + }); + + it('should render the value', () => { + const value = vm.$el.querySelector('.js-value'); + + expect(value.textContent).toContain(props.value); + }); + + it('should render the color', () => { + const color = vm.$el.querySelector('.js-color'); + + expect(color.style.backgroundColor).toBe(hexToRgb(props.color)); + }); + }); + + describe('when the value is 0', () => { + const newProps = { ...props, value: 0 }; + + beforeEach(() => { + vm = mountComponent(Component, newProps); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should still render the value, but show a "0"', () => { + const value = vm.$el.querySelector('.js-value'); + + expect(value.textContent).toContain(newProps.value); + }); + }); +}); diff --git a/ee/spec/javascripts/security_dashboard/components/vulnerability_chart_spec.js b/ee/spec/javascripts/security_dashboard/components/vulnerability_chart_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..67f36c54dc426cf8b8b97b29fed494e88b42509f --- /dev/null +++ b/ee/spec/javascripts/security_dashboard/components/vulnerability_chart_spec.js @@ -0,0 +1,42 @@ +import Vue from 'vue'; +import MockAdapater from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; + +import component from 'ee/security_dashboard/components/vulnerability_chart.vue'; +import createStore from 'ee/security_dashboard/store'; +import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import waitForPromises from 'spec/helpers/wait_for_promises'; + +import { resetStore } from '../helpers'; +import mockDataVulnerabilitiesHistory from '../store/vulnerabilities/data/mock_data_vulnerabilities_history.json'; + +describe('Vulnerabilities Chart', () => { + const Component = Vue.extend(component); + const vulnerabilitiesHistoryEndpoint = '/vulnerabilitiesEndpoint.json'; + let store; + let mock; + let vm; + + beforeEach(() => { + store = createStore(); + store.state.vulnerabilities.vulnerabilitiesHistoryEndpoint = vulnerabilitiesHistoryEndpoint; + mock = new MockAdapater(axios); + mock.onGet(vulnerabilitiesHistoryEndpoint).replyOnce(200, mockDataVulnerabilitiesHistory); + vm = mountComponentWithStore(Component, { store }); + }); + + afterEach(() => { + resetStore(store); + vm.$destroy(); + mock.restore(); + }); + + it('should render the e-chart instance', done => { + waitForPromises() + .then(() => { + expect(vm.$el.querySelector('[_echarts_instance_]')).not.toBeNull(); + done(); + }) + .catch(done.fail); + }); +}); diff --git a/ee/spec/javascripts/security_dashboard/components/vulnerability_chart_tooltip_spec.js b/ee/spec/javascripts/security_dashboard/components/vulnerability_chart_tooltip_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..870e4e70d59893f35d9583a1fbd2efd65b8c1b1d --- /dev/null +++ b/ee/spec/javascripts/security_dashboard/components/vulnerability_chart_tooltip_spec.js @@ -0,0 +1,54 @@ +import Vue from 'vue'; +import component from 'ee/security_dashboard/components/vulnerability_chart_tooltip.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('Vulnerability Chart Tooltip component', () => { + const Component = Vue.extend(component); + const props = { + title: 'Tooltip Title', + entries: [ + { + dataIndex: 1, + seriesId: 'critical_0', + seriesName: 'critical', + color: '#00f', + data: ['critical', 32], + }, + { + dataIndex: 1, + seriesId: 'high_0', + seriesName: 'high', + color: '#0f0', + data: ['high', 22], + }, + { + dataIndex: 1, + seriesId: 'low_0', + seriesName: 'low', + color: '#f00', + data: ['low', 2], + }, + ], + }; + let vm; + + beforeEach(() => { + vm = mountComponent(Component, props); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render the title', () => { + const header = vm.$el.querySelector('.card-header'); + + expect(header.textContent).toContain(props.title); + }); + + it('should render three legends', () => { + const legends = vm.$el.querySelectorAll('.js-chart-label'); + + expect(legends).toHaveLength(3); + }); +}); diff --git a/ee/spec/javascripts/security_dashboard/store/vulnerabilities/actions_spec.js b/ee/spec/javascripts/security_dashboard/store/vulnerabilities/actions_spec.js index 33597f81ecf86a0e3a2ea5f9832047a0b5d511a2..eff5ee1891bdbb0e16be3af4508478e16bbc1101 100644 --- a/ee/spec/javascripts/security_dashboard/store/vulnerabilities/actions_spec.js +++ b/ee/spec/javascripts/security_dashboard/store/vulnerabilities/actions_spec.js @@ -9,6 +9,7 @@ import * as actions from 'ee/security_dashboard/store/modules/vulnerabilities/ac import mockDataVulnerabilities from './data/mock_data_vulnerabilities.json'; import mockDataVulnerabilitiesCount from './data/mock_data_vulnerabilities_count.json'; +import mockDataVulnerabilitiesHistory from './data/mock_data_vulnerabilities_history.json'; describe('vulnerabiliites count actions', () => { const data = mockDataVulnerabilitiesCount; @@ -634,3 +635,130 @@ describe('revert vulnerability dismissal', () => { }); }); }); + +describe('vulnerabiliites timeline actions', () => { + const data = mockDataVulnerabilitiesHistory; + + describe('setVulnerabilitiesHistoryEndpoint', () => { + it('should commit the correct mutuation', done => { + const state = initialState; + const endpoint = 'fakepath.json'; + + testAction( + actions.setVulnerabilitiesHistoryEndpoint, + endpoint, + state, + [ + { + type: types.SET_VULNERABILITIES_HISTORY_ENDPOINT, + payload: endpoint, + }, + ], + [], + done, + ); + }); + }); + + describe('fetchVulnerabilitesTimeline', () => { + let mock; + const state = initialState; + + beforeEach(() => { + state.vulnerabilitiesCountEndpoint = `${TEST_HOST}/vulnerabilitIES_HISTORY.json`; + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('on success', () => { + beforeEach(() => { + mock.onGet(state.vulnerabilitiesHistoryEndpoint).replyOnce(200, data); + }); + + it('should dispatch the request and success actions', done => { + testAction( + actions.fetchVulnerabilitiesHistory, + {}, + state, + [], + [ + { type: 'requestVulnerabilitiesHistory' }, + { + type: 'receiveVulnerabilitiesHistorySuccess', + payload: { data }, + }, + ], + done, + ); + }); + }); + + describe('on error', () => { + beforeEach(() => { + mock.onGet(state.vulnerabilitiesHistoryEndpoint).replyOnce(404, {}); + }); + + it('should dispatch the request and error actions', done => { + testAction( + actions.fetchVulnerabilitiesHistory, + {}, + state, + [], + [ + { type: 'requestVulnerabilitiesHistory' }, + { type: 'receiveVulnerabilitiesHistoryError' }, + ], + done, + ); + }); + }); + }); + + describe('requestVulnerabilitesTimeline', () => { + it('should commit the request mutation', done => { + const state = initialState; + + testAction( + actions.requestVulnerabilitiesHistory, + {}, + state, + [{ type: types.REQUEST_VULNERABILITIES_HISTORY }], + [], + done, + ); + }); + }); + + describe('receiveVulnerabilitesTimelineSuccess', () => { + it('should commit the success mutation', done => { + const state = initialState; + + testAction( + actions.receiveVulnerabilitiesHistorySuccess, + { data }, + state, + [{ type: types.RECEIVE_VULNERABILITIES_HISTORY_SUCCESS, payload: data }], + [], + done, + ); + }); + }); + + describe('receivetVulnerabilitesTimelineError', () => { + it('should commit the error mutation', done => { + const state = initialState; + + testAction( + actions.receiveVulnerabilitiesHistoryError, + {}, + state, + [{ type: types.RECEIVE_VULNERABILITIES_HISTORY_ERROR }], + [], + done, + ); + }); + }); +}); diff --git a/ee/spec/javascripts/security_dashboard/store/vulnerabilities/data/mock_data_vulnerabilities_history.json b/ee/spec/javascripts/security_dashboard/store/vulnerabilities/data/mock_data_vulnerabilities_history.json new file mode 100644 index 0000000000000000000000000000000000000000..64fa912aefd8e084491c37da2fd0bf51108470a0 --- /dev/null +++ b/ee/spec/javascripts/security_dashboard/store/vulnerabilities/data/mock_data_vulnerabilities_history.json @@ -0,0 +1,566 @@ +{ + "low": { + "2018-10-1": 87, + "2018-10-2": 88, + "2018-10-3": 90, + "2018-10-4": 89, + "2018-10-5": 89, + "2018-10-6": 80, + "2018-10-7": 85, + "2018-10-8": 67, + "2018-10-9": 84, + "2018-10-10": 72, + "2018-10-11": 67, + "2018-10-12": 86, + "2018-10-13": 70, + "2018-10-14": 68, + "2018-10-15": 61, + "2018-10-16": 74, + "2018-10-17": 67, + "2018-10-18": 78, + "2018-10-19": 65, + "2018-10-20": 72, + "2018-10-21": 78, + "2018-10-22": 81, + "2018-10-23": 62, + "2018-10-24": 86, + "2018-10-25": 79, + "2018-10-26": 86, + "2018-10-27": 78, + "2018-10-28": 75, + "2018-10-29": 67, + "2018-10-30": 87, + "2018-10-31": 86, + "2018-11-1": 75, + "2018-11-2": 81, + "2018-11-3": 88, + "2018-11-4": 82, + "2018-11-5": 76, + "2018-11-6": 76, + "2018-11-7": 68, + "2018-11-8": 86, + "2018-11-9": 70, + "2018-11-10": 74, + "2018-11-11": 60, + "2018-11-12": 61, + "2018-11-13": 73, + "2018-11-14": 90, + "2018-11-15": 69, + "2018-11-16": 78, + "2018-11-17": 81, + "2018-11-18": 60, + "2018-11-19": 86, + "2018-11-20": 72, + "2018-11-21": 73, + "2018-11-22": 60, + "2018-11-23": 88, + "2018-11-24": 70, + "2018-11-25": 60, + "2018-11-26": 72, + "2018-11-27": 71, + "2018-11-28": 77, + "2018-11-29": 77, + "2018-11-30": 70, + "2018-12-1": 69, + "2018-12-2": 80, + "2018-12-3": 73, + "2018-12-4": 71, + "2018-12-5": 84, + "2018-12-6": 82, + "2018-12-7": 68, + "2018-12-8": 66, + "2018-12-9": 76, + "2018-12-10": 81, + "2018-12-11": 61, + "2018-12-12": 78, + "2018-12-13": 85, + "2018-12-14": 74, + "2018-12-15": 65, + "2018-12-16": 90, + "2018-12-17": 87, + "2018-12-18": 83, + "2018-12-19": 72, + "2018-12-20": 79, + "2018-12-21": 83, + "2018-12-22": 70, + "2018-12-23": 75, + "2018-12-24": 77, + "2018-12-25": 68, + "2018-12-26": 86, + "2018-12-27": 76, + "2018-12-28": 86, + "2018-12-29": 89, + "2018-12-30": 73, + "2018-12-31": 70 + }, + "medium": { + "2018-10-1": 73, + "2018-10-2": 76, + "2018-10-3": 101, + "2018-10-4": 84, + "2018-10-5": 90, + "2018-10-6": 97, + "2018-10-7": 77, + "2018-10-8": 81, + "2018-10-9": 98, + "2018-10-10": 83, + "2018-10-11": 82, + "2018-10-12": 70, + "2018-10-13": 99, + "2018-10-14": 83, + "2018-10-15": 81, + "2018-10-16": 80, + "2018-10-17": 82, + "2018-10-18": 89, + "2018-10-19": 89, + "2018-10-20": 71, + "2018-10-21": 73, + "2018-10-22": 74, + "2018-10-23": 83, + "2018-10-24": 91, + "2018-10-25": 85, + "2018-10-26": 90, + "2018-10-27": 77, + "2018-10-28": 102, + "2018-10-29": 75, + "2018-10-30": 78, + "2018-10-31": 70, + "2018-11-1": 90, + "2018-11-2": 96, + "2018-11-3": 98, + "2018-11-4": 88, + "2018-11-5": 79, + "2018-11-6": 91, + "2018-11-7": 101, + "2018-11-8": 75, + "2018-11-9": 75, + "2018-11-10": 84, + "2018-11-11": 70, + "2018-11-12": 89, + "2018-11-13": 104, + "2018-11-14": 90, + "2018-11-15": 81, + "2018-11-16": 102, + "2018-11-17": 86, + "2018-11-18": 80, + "2018-11-19": 71, + "2018-11-20": 72, + "2018-11-21": 103, + "2018-11-22": 89, + "2018-11-23": 83, + "2018-11-24": 79, + "2018-11-25": 87, + "2018-11-26": 79, + "2018-11-27": 104, + "2018-11-28": 70, + "2018-11-29": 103, + "2018-11-30": 86, + "2018-12-1": 86, + "2018-12-2": 77, + "2018-12-3": 96, + "2018-12-4": 95, + "2018-12-5": 74, + "2018-12-6": 99, + "2018-12-7": 101, + "2018-12-8": 78, + "2018-12-9": 83, + "2018-12-10": 76, + "2018-12-11": 77, + "2018-12-12": 105, + "2018-12-13": 81, + "2018-12-14": 82, + "2018-12-15": 90, + "2018-12-16": 88, + "2018-12-17": 78, + "2018-12-18": 82, + "2018-12-19": 83, + "2018-12-20": 105, + "2018-12-21": 70, + "2018-12-22": 85, + "2018-12-23": 91, + "2018-12-24": 89, + "2018-12-25": 83, + "2018-12-26": 73, + "2018-12-27": 91, + "2018-12-28": 77, + "2018-12-29": 101, + "2018-12-30": 83, + "2018-12-31": 94 + }, + "high": { + "2018-10-1": 43, + "2018-10-2": 42, + "2018-10-3": 42, + "2018-10-4": 49, + "2018-10-5": 44, + "2018-10-6": 59, + "2018-10-7": 49, + "2018-10-8": 53, + "2018-10-9": 44, + "2018-10-10": 51, + "2018-10-11": 43, + "2018-10-12": 53, + "2018-10-13": 52, + "2018-10-14": 43, + "2018-10-15": 60, + "2018-10-16": 53, + "2018-10-17": 57, + "2018-10-18": 42, + "2018-10-19": 46, + "2018-10-20": 43, + "2018-10-21": 43, + "2018-10-22": 41, + "2018-10-23": 47, + "2018-10-24": 44, + "2018-10-25": 43, + "2018-10-26": 60, + "2018-10-27": 43, + "2018-10-28": 59, + "2018-10-29": 55, + "2018-10-30": 45, + "2018-10-31": 51, + "2018-11-1": 55, + "2018-11-2": 50, + "2018-11-3": 43, + "2018-11-4": 41, + "2018-11-5": 51, + "2018-11-6": 49, + "2018-11-7": 49, + "2018-11-8": 60, + "2018-11-9": 60, + "2018-11-10": 43, + "2018-11-11": 57, + "2018-11-12": 42, + "2018-11-13": 59, + "2018-11-14": 41, + "2018-11-15": 53, + "2018-11-16": 53, + "2018-11-17": 43, + "2018-11-18": 53, + "2018-11-19": 48, + "2018-11-20": 56, + "2018-11-21": 51, + "2018-11-22": 42, + "2018-11-23": 60, + "2018-11-24": 50, + "2018-11-25": 49, + "2018-11-26": 47, + "2018-11-27": 46, + "2018-11-28": 40, + "2018-11-29": 41, + "2018-11-30": 57, + "2018-12-1": 57, + "2018-12-2": 45, + "2018-12-3": 52, + "2018-12-4": 46, + "2018-12-5": 56, + "2018-12-6": 48, + "2018-12-7": 58, + "2018-12-8": 59, + "2018-12-9": 47, + "2018-12-10": 58, + "2018-12-11": 50, + "2018-12-12": 45, + "2018-12-13": 59, + "2018-12-14": 40, + "2018-12-15": 40, + "2018-12-16": 48, + "2018-12-17": 44, + "2018-12-18": 54, + "2018-12-19": 44, + "2018-12-20": 57, + "2018-12-21": 54, + "2018-12-22": 44, + "2018-12-23": 59, + "2018-12-24": 41, + "2018-12-25": 52, + "2018-12-26": 52, + "2018-12-27": 50, + "2018-12-28": 49, + "2018-12-29": 45, + "2018-12-30": 44, + "2018-12-31": 60 + }, + "critical": { + "2018-10-1": 54, + "2018-10-2": 67, + "2018-10-3": 62, + "2018-10-4": 63, + "2018-10-5": 51, + "2018-10-6": 56, + "2018-10-7": 66, + "2018-10-8": 69, + "2018-10-9": 58, + "2018-10-10": 61, + "2018-10-11": 69, + "2018-10-12": 73, + "2018-10-13": 68, + "2018-10-14": 64, + "2018-10-15": 69, + "2018-10-16": 63, + "2018-10-17": 72, + "2018-10-18": 71, + "2018-10-19": 56, + "2018-10-20": 71, + "2018-10-21": 59, + "2018-10-22": 55, + "2018-10-23": 51, + "2018-10-24": 74, + "2018-10-25": 68, + "2018-10-26": 74, + "2018-10-27": 53, + "2018-10-28": 73, + "2018-10-29": 54, + "2018-10-30": 53, + "2018-10-31": 53, + "2018-11-1": 68, + "2018-11-2": 71, + "2018-11-3": 57, + "2018-11-4": 59, + "2018-11-5": 58, + "2018-11-6": 67, + "2018-11-7": 56, + "2018-11-8": 74, + "2018-11-9": 54, + "2018-11-10": 67, + "2018-11-11": 61, + "2018-11-12": 73, + "2018-11-13": 58, + "2018-11-14": 56, + "2018-11-15": 55, + "2018-11-16": 72, + "2018-11-17": 53, + "2018-11-18": 68, + "2018-11-19": 52, + "2018-11-20": 64, + "2018-11-21": 72, + "2018-11-22": 50, + "2018-11-23": 59, + "2018-11-24": 56, + "2018-11-25": 74, + "2018-11-26": 71, + "2018-11-27": 66, + "2018-11-28": 55, + "2018-11-29": 51, + "2018-11-30": 63, + "2018-12-1": 54, + "2018-12-2": 63, + "2018-12-3": 64, + "2018-12-4": 51, + "2018-12-5": 66, + "2018-12-6": 61, + "2018-12-7": 62, + "2018-12-8": 59, + "2018-12-9": 69, + "2018-12-10": 73, + "2018-12-11": 67, + "2018-12-12": 58, + "2018-12-13": 69, + "2018-12-14": 71, + "2018-12-15": 69, + "2018-12-16": 72, + "2018-12-17": 73, + "2018-12-18": 59, + "2018-12-19": 60, + "2018-12-20": 52, + "2018-12-21": 71, + "2018-12-22": 56, + "2018-12-23": 61, + "2018-12-24": 61, + "2018-12-25": 72, + "2018-12-26": 66, + "2018-12-27": 67, + "2018-12-28": 72, + "2018-12-29": 58, + "2018-12-30": 68, + "2018-12-31": 54 + }, + "unknown": { + "2018-10-1": 39, + "2018-10-2": 44, + "2018-10-3": 35, + "2018-10-4": 34, + "2018-10-5": 38, + "2018-10-6": 34, + "2018-10-7": 34, + "2018-10-8": 43, + "2018-10-9": 41, + "2018-10-10": 45, + "2018-10-11": 41, + "2018-10-12": 37, + "2018-10-13": 34, + "2018-10-14": 41, + "2018-10-15": 45, + "2018-10-16": 33, + "2018-10-17": 40, + "2018-10-18": 31, + "2018-10-19": 42, + "2018-10-20": 33, + "2018-10-21": 44, + "2018-10-22": 33, + "2018-10-23": 35, + "2018-10-24": 37, + "2018-10-25": 43, + "2018-10-26": 33, + "2018-10-27": 43, + "2018-10-28": 39, + "2018-10-29": 37, + "2018-10-30": 36, + "2018-10-31": 37, + "2018-11-1": 42, + "2018-11-2": 41, + "2018-11-3": 36, + "2018-11-4": 31, + "2018-11-5": 41, + "2018-11-6": 37, + "2018-11-7": 42, + "2018-11-8": 42, + "2018-11-9": 45, + "2018-11-10": 34, + "2018-11-11": 30, + "2018-11-12": 40, + "2018-11-13": 39, + "2018-11-14": 44, + "2018-11-15": 36, + "2018-11-16": 35, + "2018-11-17": 30, + "2018-11-18": 31, + "2018-11-19": 34, + "2018-11-20": 31, + "2018-11-21": 36, + "2018-11-22": 37, + "2018-11-23": 41, + "2018-11-24": 38, + "2018-11-25": 42, + "2018-11-26": 41, + "2018-11-27": 36, + "2018-11-28": 32, + "2018-11-29": 43, + "2018-11-30": 36, + "2018-12-1": 44, + "2018-12-2": 34, + "2018-12-3": 42, + "2018-12-4": 32, + "2018-12-5": 44, + "2018-12-6": 31, + "2018-12-7": 39, + "2018-12-8": 37, + "2018-12-9": 33, + "2018-12-10": 37, + "2018-12-11": 38, + "2018-12-12": 35, + "2018-12-13": 34, + "2018-12-14": 40, + "2018-12-15": 35, + "2018-12-16": 42, + "2018-12-17": 44, + "2018-12-18": 40, + "2018-12-19": 40, + "2018-12-20": 30, + "2018-12-21": 44, + "2018-12-22": 32, + "2018-12-23": 39, + "2018-12-24": 37, + "2018-12-25": 35, + "2018-12-26": 39, + "2018-12-27": 38, + "2018-12-28": 44, + "2018-12-29": 42, + "2018-12-30": 37, + "2018-12-31": 35 + }, + "all": { + "2018-10-1": 143, + "2018-10-2": 130, + "2018-10-3": 139, + "2018-10-4": 134, + "2018-10-5": 138, + "2018-10-6": 131, + "2018-10-7": 137, + "2018-10-8": 144, + "2018-10-9": 140, + "2018-10-10": 134, + "2018-10-11": 142, + "2018-10-12": 132, + "2018-10-13": 136, + "2018-10-14": 141, + "2018-10-15": 134, + "2018-10-16": 139, + "2018-10-17": 141, + "2018-10-18": 134, + "2018-10-19": 131, + "2018-10-20": 141, + "2018-10-21": 139, + "2018-10-22": 145, + "2018-10-23": 142, + "2018-10-24": 143, + "2018-10-25": 143, + "2018-10-26": 135, + "2018-10-27": 136, + "2018-10-28": 143, + "2018-10-29": 142, + "2018-10-30": 131, + "2018-10-31": 141, + "2018-11-1": 134, + "2018-11-2": 134, + "2018-11-3": 130, + "2018-11-4": 137, + "2018-11-5": 145, + "2018-11-6": 137, + "2018-11-7": 135, + "2018-11-8": 145, + "2018-11-9": 132, + "2018-11-10": 134, + "2018-11-11": 139, + "2018-11-12": 139, + "2018-11-13": 130, + "2018-11-14": 137, + "2018-11-15": 136, + "2018-11-16": 145, + "2018-11-17": 130, + "2018-11-18": 143, + "2018-11-19": 134, + "2018-11-20": 145, + "2018-11-21": 137, + "2018-11-22": 140, + "2018-11-23": 138, + "2018-11-24": 132, + "2018-11-25": 143, + "2018-11-26": 131, + "2018-11-27": 130, + "2018-11-28": 144, + "2018-11-29": 139, + "2018-11-30": 143, + "2018-12-1": 139, + "2018-12-2": 137, + "2018-12-3": 142, + "2018-12-4": 137, + "2018-12-5": 134, + "2018-12-6": 133, + "2018-12-7": 137, + "2018-12-8": 140, + "2018-12-9": 130, + "2018-12-10": 132, + "2018-12-11": 134, + "2018-12-12": 143, + "2018-12-13": 130, + "2018-12-14": 133, + "2018-12-15": 137, + "2018-12-16": 141, + "2018-12-17": 139, + "2018-12-18": 145, + "2018-12-19": 141, + "2018-12-20": 137, + "2018-12-21": 139, + "2018-12-22": 131, + "2018-12-23": 134, + "2018-12-24": 144, + "2018-12-25": 140, + "2018-12-26": 145, + "2018-12-27": 138, + "2018-12-28": 136, + "2018-12-29": 144, + "2018-12-30": 131, + "2018-12-31": 142 + } +} \ No newline at end of file diff --git a/ee/spec/javascripts/security_dashboard/store/vulnerabilities/mutations_spec.js b/ee/spec/javascripts/security_dashboard/store/vulnerabilities/mutations_spec.js index d4cc58efd3774c998e4cdea086e24281c6584bd5..1e557d2230371c16da5d55c1dea15540268f351a 100644 --- a/ee/spec/javascripts/security_dashboard/store/vulnerabilities/mutations_spec.js +++ b/ee/spec/javascripts/security_dashboard/store/vulnerabilities/mutations_spec.js @@ -131,6 +131,66 @@ describe('vulnerabilities module mutations', () => { }); }); + describe('SET_VULNERABILITIES_HISTORY_ENDPOINT', () => { + it('should set `vulnerabilitiesHistoryEndpoint` to `fakepath.json`', () => { + const state = createState(); + const endpoint = 'fakepath.json'; + + mutations[types.SET_VULNERABILITIES_HISTORY_ENDPOINT](state, endpoint); + + expect(state.vulnerabilitiesHistoryEndpoint).toEqual(endpoint); + }); + }); + + describe('REQUEST_VULNERABILITIES_HISTORY', () => { + let state; + + beforeEach(() => { + state = { + ...createState(), + errorLoadingVulnerabilitiesHistory: true, + }; + mutations[types.REQUEST_VULNERABILITIES_HISTORY](state); + }); + + it('should set `isLoadingVulnerabilitiesHistory` to `true`', () => { + expect(state.isLoadingVulnerabilitiesHistory).toBeTruthy(); + }); + + it('should set `errorLoadingVulnerabilitiesHistory` to `false`', () => { + expect(state.errorLoadingVulnerabilitiesHistory).toBeFalsy(); + }); + }); + + describe('RECEIVE_VULNERABILITIES_HISTORY_SUCCESS', () => { + let payload; + let state; + + beforeEach(() => { + payload = mockData; + state = createState(); + mutations[types.RECEIVE_VULNERABILITIES_HISTORY_SUCCESS](state, payload); + }); + + it('should set `isLoadingVulnerabilitiesHistory` to `false`', () => { + expect(state.isLoadingVulnerabilitiesHistory).toBeFalsy(); + }); + + it('should set `vulnerabilitiesHistory`', () => { + expect(state.vulnerabilitiesHistory).toBe(payload); + }); + }); + + describe('RECEIVE_VULNERABILITIES_HISTORY_ERROR', () => { + it('should set `isLoadingVulnerabilitiesHistory` to `false`', () => { + const state = createState(); + + mutations[types.RECEIVE_VULNERABILITIES_HISTORY_ERROR](state); + + expect(state.isLoadingVulnerabilitiesHistory).toBeFalsy(); + }); + }); + describe('SET_MODAL_DATA', () => { describe('with all the data', () => { const vulnerability = mockData[0]; diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 768c0b172a8e6ac98e071d7d4cf7b8f9d690f9cf..de20a941eb0876e170fc3bdb47cd0ecacfcdbba0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -9340,6 +9340,9 @@ msgstr "" msgid "VisibilityLevel|Unknown" msgstr "" +msgid "Vulnerability Chart" +msgstr "" + msgid "Vulnerability List" msgstr ""