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 Chart') }}
+
+
+ {{ __('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 @@
+
+
+
+
+
+
{{ name }}
+
{{ value }}
+
+
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 ""