Commit a2266aae authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'date-range-group-secure-graph' into 'master'

Date range group secure graph

See merge request gitlab-org/gitlab-ee!9446
parents 355bf7d5 3306d030
......@@ -90,9 +90,9 @@ export default {
<div>
<filters :dashboard-documentation="dashboardDocumentation" />
<vulnerability-count-list />
<h4 class="my-4">{{ __('Vulnerability Chart') }}</h4>
<vulnerability-chart />
<h4 class="my-4">{{ __('Vulnerability List') }}</h4>
<security-dashboard-table
:dashboard-documentation="dashboardDocumentation"
:empty-state-svg-path="emptyStateSvgPath"
......
......@@ -54,6 +54,8 @@ export default {
<template>
<div class="ci-table">
<h4 class="my-4">{{ __('Vulnerability List') }}</h4>
<div
class="gl-responsive-table-row table-row-header vulnerabilities-row-header px-2"
role="row"
......
<script>
import dateFormat from 'dateformat';
import { mapState } from 'vuex';
import { mapState, mapGetters, mapActions } from 'vuex';
import { GlChart } from '@gitlab/ui/dist/charts';
import ChartTooltip from './vulnerability_chart_tooltip.vue';
import ChartButtons from './vulnerability_chart_buttons.vue';
import { DAY_IN_MS, DAYS } from '../store/modules/vulnerabilities/constants';
export default {
name: 'VulnerabilityChart',
components: {
GlChart,
ChartTooltip,
ChartButtons,
},
DAYS,
data: () => ({
tooltipTitle: '',
tooltipEntries: [],
......@@ -37,12 +41,17 @@ export default {
],
}),
computed: {
...mapState('vulnerabilities', ['vulnerabilitiesHistory']),
...mapState('vulnerabilities', [
'vulnerabilitiesHistory',
'vulnerabilitiesHistoryDayRange',
'vulnerabilitiesHistoryShowSplitLine',
'vulnerabilitiesHistoryMaxDayInterval',
]),
...mapGetters('vulnerabilities', ['getFilteredVulnerabilitiesHistory']),
series() {
return this.lines.map(line => {
const { name, color } = line;
const history = this.vulnerabilitiesHistory[name.toLowerCase()];
const data = history ? Object.entries(history) : [];
const data = this.getFilteredVulnerabilitiesHistory(name);
return {
borderWidth: 2,
......@@ -88,11 +97,14 @@ export default {
width: 2,
},
},
splitLine: {
show: this.vulnerabilitiesHistoryShowSplitLine,
},
axisTick: {
show: false,
},
maxInterval: 1000 * 60 * 60 * 24 * 7,
min: Date.now() - 1000 * 60 * 60 * 24 * 28,
maxInterval: DAY_IN_MS * this.vulnerabilitiesHistoryMaxDayInterval,
min: Date.now() - DAY_IN_MS * this.vulnerabilitiesHistoryDayRange,
name: 'Date',
nameGap: 50,
nameLocation: 'center',
......@@ -141,8 +153,13 @@ export default {
series: this.series,
};
},
days() {
const { $options } = this;
return [$options.DAYS.THIRTY, $options.DAYS.SIXTY, $options.DAYS.NINETY];
},
},
methods: {
...mapActions('vulnerabilities', ['setVulnerabilitiesHistoryDayRange']),
renderTooltip(params, ticket, callback) {
this.tooltipTitle = dateFormat(params[0].axisValue, 'd mmmm');
this.tooltipEntries = params;
......@@ -154,10 +171,29 @@ export default {
</script>
<template>
<div class="vulnerabilities-chart">
<div class="vulnerabilities-chart-wrapper">
<gl-chart :options="options" :width="1240" />
<chart-tooltip v-show="false" ref="tooltip" :title="tooltipTitle" :entries="tooltipEntries" />
<div>
<div class="d-flex justify-content-between">
<h4 class="my-4">{{ __('Vulnerability Chart') }}</h4>
<div class="align-self-center">
<chart-buttons
:days="days"
:active-day="vulnerabilitiesHistoryDayRange"
@click="setVulnerabilitiesHistoryDayRange"
/>
</div>
</div>
<div class="vulnerabilities-chart">
<div class="vulnerabilities-chart-wrapper">
<gl-chart :options="options" />
<chart-tooltip
v-show="false"
ref="tooltip"
:title="tooltipTitle"
:entries="tooltipEntries"
/>
</div>
</div>
</div>
</template>
<script>
import { GlButton } from '@gitlab/ui';
import { n__ } from '~/locale';
export default {
name: 'VulnerabilityChartButtons',
components: {
GlButton,
},
props: {
days: {
type: Array,
required: true,
},
activeDay: {
type: Number,
required: true,
},
},
computed: {
buttonContent() {
return days => n__('1 Day', '%d Days', days);
},
},
methods: {
clickHandler(days) {
this.$emit('click', days);
},
},
};
</script>
<template>
<div class="btn-group">
<gl-button
v-for="day in days"
:key="day"
:class="{ active: activeDay === day }"
variant="secondary"
:data-days="day"
@click="clickHandler(day)"
>
{{ buttonContent(day) }}
</gl-button>
</div>
</template>
......@@ -233,6 +233,10 @@ export const fetchVulnerabilitiesHistory = ({ state, dispatch }, params = {}) =>
});
};
export const setVulnerabilitiesHistoryDayRange = ({ commit }, days) => {
commit(types.SET_VULNERABILITIES_HISTORY_DAY_RANGE, days);
};
export const requestVulnerabilitiesHistory = ({ commit }) => {
commit(types.REQUEST_VULNERABILITIES_HISTORY);
};
......
......@@ -2,3 +2,11 @@ export const CRITICAL = 'critical';
export const HIGH = 'high';
export const MEDIUM = 'medium';
export const LOW = 'low';
export const DAY_IN_MS = 1000 * 60 * 60 * 24;
export const DAYS = {
THIRTY: 30,
SIXTY: 60,
NINETY: 90,
};
......@@ -5,4 +5,27 @@ export const dashboardListError = state =>
export const dashboardCountError = state =>
!state.errorLoadingVulnerabilities && state.errorLoadingVulnerabilitiesCount;
export const getVulnerabilityHistoryByName = state => name =>
state.vulnerabilitiesHistory[name.toLowerCase()];
export const getFilteredVulnerabilitiesHistory = (state, getters) => name => {
const history = getters.getVulnerabilityHistoryByName(name);
const days = state.vulnerabilitiesHistoryDayRange;
if (!history) {
return [];
}
const data = Object.entries(history);
const currentDate = new Date();
const startDate = new Date();
startDate.setDate(currentDate.getDate() - days);
return data.filter(date => {
const parsedDate = Date.parse(date[0]);
return parsedDate > startDate;
});
};
export default () => {};
......@@ -9,6 +9,7 @@ export const RECEIVE_VULNERABILITIES_COUNT_SUCCESS = 'RECEIVE_VULNERABILITIES_CO
export const RECEIVE_VULNERABILITIES_COUNT_ERROR = 'RECEIVE_VULNERABILITIES_COUNT_ERROR';
export const SET_VULNERABILITIES_HISTORY_ENDPOINT = 'SET_VULNERABILITIES_HISTORY_ENDPOINT';
export const SET_VULNERABILITIES_HISTORY_DAY_RANGE = 'SET_VULNERABILITIES_HISTORY_DAY_RANGE';
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';
......
......@@ -2,6 +2,7 @@ import Vue from 'vue';
import { s__ } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import * as types from './mutation_types';
import { DAYS } from './constants';
export default {
[types.SET_VULNERABILITIES_ENDPOINT](state, payload) {
......@@ -38,6 +39,18 @@ export default {
[types.SET_VULNERABILITIES_HISTORY_ENDPOINT](state, payload) {
state.vulnerabilitiesHistoryEndpoint = payload;
},
[types.SET_VULNERABILITIES_HISTORY_DAY_RANGE](state, days) {
state.vulnerabilitiesHistoryDayRange = days;
state.vulnerabilitiesHistoryShowSplitLine = days <= DAYS.THIRTY;
if (days <= DAYS.THIRTY) {
state.vulnerabilitiesHistoryMaxDayInterval = 1;
} else if (days > DAYS.THIRTY && days <= DAYS.SIXTY) {
state.vulnerabilitiesHistoryMaxDayInterval = 7;
} else if (days > DAYS.SIXTY) {
state.vulnerabilitiesHistoryMaxDayInterval = 14;
}
},
[types.REQUEST_VULNERABILITIES_HISTORY](state) {
state.isLoadingVulnerabilitiesHistory = true;
state.errorLoadingVulnerabilitiesHistory = false;
......
......@@ -10,6 +10,9 @@ export default () => ({
isLoadingVulnerabilitiesHistory: true,
errorLoadingVulnerabilitiesHistory: false,
vulnerabilitiesHistory: {},
vulnerabilitiesHistoryDayRange: 90,
vulnerabilitiesHistoryShowSplitLine: false,
vulnerabilitiesHistoryMaxDayInterval: 7,
pageInfo: {},
vulnerabilitiesCountEndpoint: null,
vulnerabilitiesHistoryEndpoint: null,
......
......@@ -2,7 +2,7 @@ $trans-white: rgba(255, 255, 255, 0);
.vulnerabilities-chart-wrapper {
-webkit-overflow-scrolling: touch;
overflow: scroll;
overflow: auto;
}
@media screen and (max-width: 1240px) {
......@@ -14,7 +14,7 @@ $trans-white: rgba(255, 255, 255, 0);
background-image: linear-gradient(to right, $trans-white, $gl-gray-350);
bottom: 0;
content: '';
height: 310px;
height: 305px;
position: absolute;
right: -1px;
top: 10px;
......
---
title: Add date range for security dashboard graph
merge_request: 9446
author:
type: added
import Vue from 'vue';
import component from 'ee/security_dashboard/components/vulnerability_chart_buttons.vue';
import { DAYS } from 'ee/security_dashboard/store/modules/vulnerabilities/constants';
import { shallowMount, mount } from '@vue/test-utils';
describe('Vulnerability Chart Buttons', () => {
const Component = Vue.extend(component);
const days = [DAYS.THIRTY, DAYS.SIXTY, DAYS.NINETY];
describe('when rendering the buttons', () => {
it('should render with 90 days selected', () => {
const activeDay = DAYS.NINETY;
const wrapper = shallowMount(Component, { propsData: { days, activeDay } });
const activeButton = wrapper.find('[data-days="90"].active');
expect(activeButton.attributes('data-days')).toMatch('90');
});
it('should render with 60 days selected', () => {
const activeDay = DAYS.SIXTY;
const wrapper = shallowMount(Component, { propsData: { days, activeDay } });
const activeButton = wrapper.find('[data-days="60"].active');
expect(activeButton.attributes('data-days')).toMatch('60');
});
it('should render with 30 days selected', () => {
const activeDay = DAYS.THIRTY;
const wrapper = shallowMount(Component, { propsData: { days, activeDay } });
const activeButton = wrapper.find('[data-days="30"].active');
expect(activeButton.attributes('data-days')).toMatch('30');
});
});
describe('when clicking the button', () => {
const activeDay = DAYS.THIRTY;
let wrapper;
beforeEach(() => {
wrapper = mount(Component, { propsData: { days, activeDay }, sync: false });
});
it('should call the clickHandler', () => {
spyOn(wrapper.vm, 'clickHandler').and.stub();
wrapper.find('[data-days="30"].active').trigger('click', DAYS.THIRTY);
expect(wrapper.vm.clickHandler).toHaveBeenCalledWith(DAYS.THIRTY);
});
it('should emit a click event', () => {
wrapper.find('[data-days="30"].active').trigger('click', DAYS.THIRTY);
expect(wrapper.emitted().click[0]).toEqual([DAYS.THIRTY]);
});
});
});
......@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import testAction from 'spec/helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
import { DAYS } from 'ee/security_dashboard/store/modules/vulnerabilities/constants';
import initialState from 'ee/security_dashboard/store/modules/vulnerabilities/state';
import * as types from 'ee/security_dashboard/store/modules/vulnerabilities/mutation_types';
......@@ -708,6 +709,26 @@ describe('vulnerabilities history actions', () => {
});
});
describe('setVulnerabilitiesHistoryDayRange', () => {
it('should commit the number of past days to show', done => {
const state = initialState;
const days = DAYS.THIRTY;
testAction(
actions.setVulnerabilitiesHistoryDayRange,
days,
state,
[
{
type: types.SET_VULNERABILITIES_HISTORY_DAY_RANGE,
payload: days,
},
],
[],
done,
);
});
});
describe('fetchVulnerabilitiesTimeline', () => {
let mock;
const state = initialState;
......
......@@ -373,7 +373,38 @@
"2018-12-28": 72,
"2018-12-29": 58,
"2018-12-30": 68,
"2018-12-31": 54
"2018-12-31": 54,
"2019-1-1": 139,
"2019-1-2": 137,
"2019-1-3": 142,
"2019-1-4": 137,
"2019-1-5": 134,
"2019-1-6": 133,
"2019-1-7": 137,
"2019-1-8": 140,
"2019-1-9": 130,
"2019-1-10": 132,
"2019-1-11": 134,
"2019-1-12": 143,
"2019-1-13": 130,
"2019-1-14": 133,
"2019-1-15": 137,
"2019-1-16": 141,
"2019-1-17": 139,
"2019-1-18": 145,
"2019-1-19": 141,
"2019-1-20": 137,
"2019-1-21": 139,
"2019-1-22": 131,
"2019-1-23": 134,
"2019-1-24": 144,
"2019-1-25": 140,
"2019-1-26": 145,
"2019-1-27": 138,
"2019-1-28": 136,
"2019-1-29": 144,
"2019-1-30": 131,
"2019-1-31": 142
},
"unknown": {
"2018-10-1": 39,
......@@ -561,6 +592,37 @@
"2018-12-28": 136,
"2018-12-29": 144,
"2018-12-30": 131,
"2018-12-31": 142
"2018-12-31": 142,
"2019-1-1": 139,
"2019-1-2": 137,
"2019-1-3": 142,
"2019-1-4": 137,
"2019-1-5": 134,
"2019-1-6": 133,
"2019-1-7": 137,
"2019-1-8": 140,
"2019-1-9": 130,
"2019-1-10": 132,
"2019-1-11": 134,
"2019-1-12": 143,
"2019-1-13": 130,
"2019-1-14": 133,
"2019-1-15": 137,
"2019-1-16": 141,
"2019-1-17": 139,
"2019-1-18": 145,
"2019-1-19": 141,
"2019-1-20": 137,
"2019-1-21": 139,
"2019-1-22": 131,
"2019-1-23": 134,
"2019-1-24": 144,
"2019-1-25": 140,
"2019-1-26": 145,
"2019-1-27": 138,
"2019-1-28": 136,
"2019-1-29": 144,
"2019-1-30": 131,
"2019-1-31": 142
}
}
\ No newline at end of file
}
import createState from 'ee/security_dashboard/store/modules/vulnerabilities/state';
import { DAYS } from 'ee/security_dashboard/store/modules/vulnerabilities/constants';
import * as getters from 'ee/security_dashboard/store/modules/vulnerabilities/getters';
import mockHistoryData from '../vulnerabilities/data/mock_data_vulnerabilities_history.json';
describe('vulnerabilities module getters', () => {
describe('dashboardError', () => {
......@@ -53,4 +56,52 @@ describe('vulnerabilities module getters', () => {
expect(result).toBe(false);
});
});
describe('getFilteredVulnerabilitiesHistory', () => {
let state;
const mockedGetters = () => {
const getVulnerabilityHistoryByName = name =>
getters.getVulnerabilityHistoryByName(state)(name);
return { getVulnerabilityHistoryByName };
};
beforeEach(() => {
state = createState();
state.vulnerabilitiesHistory = mockHistoryData;
jasmine.clock().install();
jasmine.clock().mockDate(new Date(2019, 1, 2));
});
afterEach(function() {
jasmine.clock().uninstall();
});
it('should filter the data to the last 30 days and days we have data for', () => {
state.vulnerabilitiesHistoryDayRange = DAYS.THIRTY;
const filteredResults = getters.getFilteredVulnerabilitiesHistory(state, mockedGetters())(
'critical',
);
expect(filteredResults.length).toEqual(28);
});
it('should filter the data to the last 60 days and days we have data for', () => {
state.vulnerabilitiesHistoryDayRange = DAYS.SIXTY;
const filteredResults = getters.getFilteredVulnerabilitiesHistory(state, mockedGetters())(
'critical',
);
expect(filteredResults.length).toEqual(58);
});
it('should filter the data to the last 90 days and days we have data for', () => {
state.vulnerabilitiesHistoryDayRange = DAYS.NINETY;
const filteredResults = getters.getFilteredVulnerabilitiesHistory(state, mockedGetters())(
'critical',
);
expect(filteredResults.length).toEqual(88);
});
});
});
import createState from 'ee/security_dashboard/store/modules/vulnerabilities/state';
import * as types from 'ee/security_dashboard/store/modules/vulnerabilities/mutation_types';
import mutations from 'ee/security_dashboard/store/modules/vulnerabilities/mutations';
import { DAYS } from 'ee/security_dashboard/store/modules/vulnerabilities/constants';
import mockData from './data/mock_data_vulnerabilities.json';
describe('vulnerabilities module mutations', () => {
......@@ -191,6 +192,52 @@ describe('vulnerabilities module mutations', () => {
});
});
describe('SET_VULNERABILITIES_HISTORY_DAY_RANGE', () => {
let state;
beforeEach(() => {
state = createState();
});
it('should set the vulnerabilitiesHistoryDayRange to number of days', () => {
mutations[types.SET_VULNERABILITIES_HISTORY_DAY_RANGE](state, DAYS.THIRTY);
expect(state.vulnerabilitiesHistoryDayRange).toEqual(DAYS.THIRTY);
});
it('should set the vulnerabilitiesHistoryShowSplitLine to true if 30 days or under', () => {
mutations[types.SET_VULNERABILITIES_HISTORY_DAY_RANGE](state, DAYS.THIRTY);
expect(state.vulnerabilitiesHistoryShowSplitLine).toEqual(true);
});
it('should set the vulnerabilitiesHistoryShowSplitLine to false if 30 days or over', () => {
mutations[types.SET_VULNERABILITIES_HISTORY_DAY_RANGE](state, DAYS.SIXTY);
expect(state.vulnerabilitiesHistoryShowSplitLine).toEqual(false);
});
it('should set the vulnerabilitiesHistoryMaxDayInterval to 1 if days are 30 and under', () => {
mutations[types.SET_VULNERABILITIES_HISTORY_DAY_RANGE](state, DAYS.THIRTY);
expect(state.vulnerabilitiesHistoryMaxDayInterval).toEqual(1);
});
it('should set the vulnerabilitiesHistoryMaxDayInterval to 7 if between 30 and 60', () => {
const days = 45;
mutations[types.SET_VULNERABILITIES_HISTORY_DAY_RANGE](state, days);
expect(state.vulnerabilitiesHistoryMaxDayInterval).toEqual(7);
});
it('should set the vulnerabilitiesHistoryMaxDayInterval to 14 if over 60', () => {
mutations[types.SET_VULNERABILITIES_HISTORY_DAY_RANGE](state, DAYS.NINETY);
expect(state.vulnerabilitiesHistoryMaxDayInterval).toEqual(14);
});
});
describe('SET_MODAL_DATA', () => {
describe('with all the data', () => {
const vulnerability = mockData[0];
......
......@@ -268,6 +268,11 @@ msgid_plural "%{count} %{type} modifications"
msgstr[0] ""
msgstr[1] ""
msgid "1 Day"
msgid_plural "%d Days"
msgstr[0] ""
msgstr[1] ""
msgid "1 closed issue"
msgid_plural "%d closed issues"
msgstr[0] ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment