Commit 6d836666 authored by Mark Florian's avatar Mark Florian Committed by Kushal Pandya

Add "Show last" Threat Monitoring filter

Part of [WAF statistics reporting][1].

This adds the "Show last" filter dropdown to the UI, allowing for
statistics over different time ranges to be displayed.

[1]: https://gitlab.com/gitlab-org/gitlab/issues/14707
parent 428d07da
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlFormGroup, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { TIME_WINDOWS } from '../constants';
export default {
name: 'ThreatMonitoringFilters',
......@@ -10,13 +11,25 @@ export default {
GlDropdownItem,
},
computed: {
...mapState('threatMonitoring', ['environments', 'currentEnvironmentId']),
...mapGetters('threatMonitoring', ['currentEnvironmentName']),
...mapState('threatMonitoring', [
'environments',
'currentEnvironmentId',
'isLoadingEnvironments',
'isLoadingWafStatistics',
]),
...mapGetters('threatMonitoring', ['currentEnvironmentName', 'currentTimeWindowName']),
isDisabled() {
return (
this.isLoadingEnvironments || this.isLoadingWafStatistics || this.environments.length === 0
);
},
},
methods: {
...mapActions('threatMonitoring', ['setCurrentEnvironmentId']),
...mapActions('threatMonitoring', ['setCurrentEnvironmentId', 'setCurrentTimeWindow']),
},
environmentFilterId: 'threat-monitoring-environment-filter',
showLastFilterId: 'threat-monitoring-show-last-filter',
timeWindows: TIME_WINDOWS,
};
</script>
......@@ -31,19 +44,45 @@ export default {
>
<gl-dropdown
:id="$options.environmentFilterId"
ref="environmentsDropdown"
class="mb-0 d-flex"
toggle-class="d-flex justify-content-between"
:text="currentEnvironmentName"
:disabled="environments.length === 0"
:disabled="isDisabled"
>
<gl-dropdown-item
v-for="environment in environments"
:key="environment.id"
ref="environmentsDropdownItem"
@click="setCurrentEnvironmentId(environment.id)"
>{{ environment.name }}</gl-dropdown-item
>
</gl-dropdown>
</gl-form-group>
<gl-form-group
:label="s__('ThreatMonitoring|Show last')"
label-size="sm"
:label-for="$options.showLastFilterId"
class="col-sm-6 col-md-4 col-lg-3 col-xl-2"
>
<gl-dropdown
:id="$options.showLastFilterId"
ref="showLastDropdown"
class="mb-0 d-flex"
toggle-class="d-flex justify-content-between"
:text="currentTimeWindowName"
:disabled="isDisabled"
>
<gl-dropdown-item
v-for="(timeWindowConfig, timeWindow) in $options.timeWindows"
:key="timeWindow"
ref="showLastDropdownItem"
@click="setCurrentTimeWindow(timeWindow)"
>{{ timeWindowConfig.name }}</gl-dropdown-item
>
</gl-dropdown>
</gl-form-group>
</div>
</div>
</template>
import { __ } from '~/locale';
export const INVALID_CURRENT_ENVIRONMENT_NAME = '';
const INTERVALS = {
minute: 'minute',
hour: 'hour',
day: 'day',
};
export const TIME_WINDOWS = {
thirtyMinutes: {
name: __('30 minutes'),
durationInMilliseconds: 30 * 60 * 1000,
interval: INTERVALS.minute,
},
oneHour: {
name: __('1 hour'),
durationInMilliseconds: 60 * 60 * 1000,
interval: INTERVALS.minute,
},
twentyFourHours: {
name: __('24 hours'),
durationInMilliseconds: 24 * 60 * 60 * 1000,
interval: INTERVALS.hour,
},
sevenDays: {
name: __('7 days'),
durationInMilliseconds: 7 * 24 * 60 * 60 * 1000,
interval: INTERVALS.hour,
},
thirtyDays: {
name: __('30 days'),
durationInMilliseconds: 30 * 24 * 60 * 60 * 1000,
interval: INTERVALS.day,
},
};
export const DEFAULT_TIME_WINDOW = 'thirtyDays';
......@@ -2,6 +2,7 @@ import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import * as types from './mutation_types';
import { getTimeWindowParams } from './utils';
export const setEndpoints = ({ commit }, endpoints) => commit(types.SET_ENDPOINTS, endpoints);
......@@ -49,6 +50,11 @@ export const setCurrentEnvironmentId = ({ commit, dispatch }, environmentId) =>
return dispatch('fetchWafStatistics');
};
export const setCurrentTimeWindow = ({ commit, dispatch }, timeWindow) => {
commit(types.SET_CURRENT_TIME_WINDOW, timeWindow);
return dispatch('fetchWafStatistics');
};
export const requestWafStatistics = ({ commit }) => commit(types.REQUEST_WAF_STATISTICS);
export const receiveWafStatisticsSuccess = ({ commit }, statistics) =>
commit(types.RECEIVE_WAF_STATISTICS_SUCCESS, statistics);
......@@ -68,6 +74,7 @@ export const fetchWafStatistics = ({ state, dispatch }) => {
.get(state.wafStatisticsEndpoint, {
params: {
environment_id: state.currentEnvironmentId,
...getTimeWindowParams(state.currentTimeWindow, Date.now()),
},
})
.then(({ data }) => dispatch('receiveWafStatisticsSuccess', data))
......
// eslint-disable-next-line import/prefer-default-export
export const INVALID_CURRENT_ENVIRONMENT_NAME = '';
import { INVALID_CURRENT_ENVIRONMENT_NAME } from './constants';
import { INVALID_CURRENT_ENVIRONMENT_NAME } from '../../../constants';
import { getTimeWindowConfig } from './utils';
// eslint-disable-next-line import/prefer-default-export
export const currentEnvironmentName = ({ currentEnvironmentId, environments }) => {
const environment = environments.find(({ id }) => id === currentEnvironmentId);
return environment ? environment.name : INVALID_CURRENT_ENVIRONMENT_NAME;
};
export const currentTimeWindowName = ({ currentTimeWindow }) =>
getTimeWindowConfig(currentTimeWindow).name;
......@@ -5,6 +5,8 @@ export const RECEIVE_ENVIRONMENTS_SUCCESS = 'RECEIVE_ENVIRONMENTS_SUCCESS';
export const RECEIVE_ENVIRONMENTS_ERROR = 'RECEIVE_ENVIRONMENTS_ERROR';
export const SET_CURRENT_ENVIRONMENT_ID = 'SET_CURRENT_ENVIRONMENT_ID';
export const SET_CURRENT_TIME_WINDOW = 'SET_CURRENT_TIME_WINDOW';
export const REQUEST_WAF_STATISTICS = 'REQUEST_WAF_STATISTICS';
export const RECEIVE_WAF_STATISTICS_SUCCESS = 'RECEIVE_WAF_STATISTICS_SUCCESS';
export const RECEIVE_WAF_STATISTICS_ERROR = 'RECEIVE_WAF_STATISTICS_ERROR';
......@@ -22,6 +22,9 @@ export default {
[types.SET_CURRENT_ENVIRONMENT_ID](state, payload) {
state.currentEnvironmentId = payload;
},
[types.SET_CURRENT_TIME_WINDOW](state, payload) {
state.currentTimeWindow = payload;
},
[types.REQUEST_WAF_STATISTICS](state) {
state.isLoadingWafStatistics = true;
state.errorLoadingWafStatistics = false;
......
import { DEFAULT_TIME_WINDOW } from '../../../constants';
export default () => ({
environmentsEndpoint: '',
environments: [],
isLoadingEnvironments: false,
errorLoadingEnvironments: false,
currentEnvironmentId: -1,
currentTimeWindow: DEFAULT_TIME_WINDOW,
wafStatisticsEndpoint: '',
wafStatistics: {
totalTraffic: 0,
......
import { TIME_WINDOWS, DEFAULT_TIME_WINDOW } from 'ee/threat_monitoring/constants';
export const getTimeWindowConfig = timeWindow =>
TIME_WINDOWS[timeWindow] || TIME_WINDOWS[DEFAULT_TIME_WINDOW];
/**
* Get the from/to/interval query parameters for the given time window.
* @param {string} timeWindow - The time window (keyof TIME_WINDOWS)
* @param {number} to - Milliseconds past the epoch corresponding to the
* returned `to` parameter
* @returns {Object} Query parameters `from` and `to` are ISO 8601 dates and
* `interval` is the configured interval for the time window.
*/
export const getTimeWindowParams = (timeWindow, to) => {
const { durationInMilliseconds, interval } = getTimeWindowConfig(timeWindow);
return {
from: new Date(to - durationInMilliseconds).toISOString(),
to: new Date(to).toISOString(),
interval,
};
};
import { shallowMount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import createStore from 'ee/threat_monitoring/store';
import ThreatMonitoringFilters from 'ee/threat_monitoring/components/threat_monitoring_filters.vue';
import { INVALID_CURRENT_ENVIRONMENT_NAME } from 'ee/threat_monitoring/store/modules/threat_monitoring/constants';
import { INVALID_CURRENT_ENVIRONMENT_NAME, TIME_WINDOWS } from 'ee/threat_monitoring/constants';
import { mockEnvironmentsResponse } from '../mock_data';
const mockEnvironments = mockEnvironmentsResponse.environments;
describe('ThreatMonitoringFilters component', () => {
let store;
let wrapper;
......@@ -21,21 +22,19 @@ describe('ThreatMonitoringFilters component', () => {
});
};
const findEnvironmentsDropdown = () => wrapper.find(GlDropdown);
const findEnvironmentsDropdownItems = () => wrapper.findAll(GlDropdownItem).wrappers;
const findEnvironmentsDropdown = () => wrapper.find({ ref: 'environmentsDropdown' });
const findEnvironmentsDropdownItems = () => wrapper.findAll({ ref: 'environmentsDropdownItem' });
const findShowLastDropdown = () => wrapper.find({ ref: 'showLastDropdown' });
const findShowLastDropdownItems = () => wrapper.findAll({ ref: 'showLastDropdownItem' });
afterEach(() => {
wrapper.destroy();
});
describe('given there are no environments', () => {
beforeEach(() => {
factory();
});
describe('the environments dropdown', () => {
it('is disabled', () => {
expect(findEnvironmentsDropdown().attributes().disabled).toBe('true');
describe('the environments dropdown', () => {
describe('given there are no environments', () => {
beforeEach(() => {
factory();
});
it('has text set to the INVALID_CURRENT_ENVIRONMENT_NAME', () => {
......@@ -46,20 +45,17 @@ describe('ThreatMonitoringFilters component', () => {
expect(findEnvironmentsDropdownItems()).toHaveLength(0);
});
});
});
describe('given there are environments', () => {
const { environments } = mockEnvironmentsResponse;
const currentEnvironment = environments[1];
describe('given there are environments', () => {
const currentEnvironment = mockEnvironments[1];
beforeEach(() => {
factory({
environments,
currentEnvironmentId: currentEnvironment.id,
beforeEach(() => {
factory({
environments: mockEnvironments,
currentEnvironmentId: currentEnvironment.id,
});
});
});
describe('the environments dropdown', () => {
it('is not disabled', () => {
expect(findEnvironmentsDropdown().attributes().disabled).toBe(undefined);
});
......@@ -71,10 +67,11 @@ describe('ThreatMonitoringFilters component', () => {
it('has dropdown items for each environment', () => {
const dropdownItems = findEnvironmentsDropdownItems();
environments.forEach((environment, i) => {
expect(dropdownItems[i].text()).toBe(environment.name);
mockEnvironments.forEach((environment, i) => {
const dropdownItem = dropdownItems.at(i);
expect(dropdownItem.text()).toBe(environment.name);
dropdownItems[i].vm.$emit('click');
dropdownItem.vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith(
'threatMonitoring/setCurrentEnvironmentId',
environment.id,
......@@ -83,4 +80,61 @@ describe('ThreatMonitoringFilters component', () => {
});
});
});
describe('the "show last" dropdown', () => {
beforeEach(() => {
factory({
environments: mockEnvironments,
});
});
it('is not disabled', () => {
expect(findShowLastDropdown().attributes().disabled).toBe(undefined);
});
it('has text set to the current time window name', () => {
const currentTimeWindowName = store.getters['threatMonitoring/currentTimeWindowName'];
expect(findShowLastDropdown().attributes().text).toBe(currentTimeWindowName);
});
it('has dropdown items for each time window', () => {
const dropdownItems = findShowLastDropdownItems();
Object.entries(TIME_WINDOWS).forEach(([timeWindow, config], i) => {
const dropdownItem = dropdownItems.at(i);
expect(dropdownItem.text()).toBe(config.name);
dropdownItem.vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith(
'threatMonitoring/setCurrentTimeWindow',
timeWindow,
);
});
});
});
describe.each`
context | isLoadingEnvironments | isLoadingWafStatistics | environments
${'environments are loading'} | ${true} | ${false} | ${mockEnvironments}
${'WAF statistics are loading'} | ${false} | ${true} | ${mockEnvironments}
${'there are no environments'} | ${false} | ${false} | ${[]}
`('given $context', ({ isLoadingEnvironments, isLoadingWafStatistics, environments }) => {
beforeEach(() => {
factory({
environments,
isLoadingEnvironments,
isLoadingWafStatistics,
});
return wrapper.vm.$nextTick();
});
it('disables the environments dropdown', () => {
expect(findEnvironmentsDropdown().attributes('disabled')).toBe('true');
});
it('disables the "show last" dropdown', () => {
expect(findShowLastDropdown().attributes('disabled')).toBe('true');
});
});
});
......@@ -198,6 +198,19 @@ describe('Threat Monitoring actions', () => {
));
});
describe('setCurrentTimeWindow', () => {
const timeWindow = 'foo';
it('commits the SET_CURRENT_TIME_WINDOW mutation and dispatches fetchWafStatistics', () =>
testAction(
actions.setCurrentTimeWindow,
timeWindow,
state,
[{ type: types.SET_CURRENT_TIME_WINDOW, payload: timeWindow }],
[{ type: 'fetchWafStatistics' }],
));
});
describe('requestWafStatistics', () => {
it('commits the REQUEST_WAF_STATISTICS mutation', () =>
testAction(
......@@ -262,8 +275,17 @@ describe('Threat Monitoring actions', () => {
describe('on success', () => {
beforeEach(() => {
jest.spyOn(global.Date, 'now').mockImplementation(() => new Date(2019, 0, 31).getTime());
mock
.onGet(wafStatisticsEndpoint, { params: { environment_id: currentEnvironmentId } })
.onGet(wafStatisticsEndpoint, {
params: {
environment_id: currentEnvironmentId,
from: '2019-01-01T00:00:00.000Z',
to: '2019-01-31T00:00:00.000Z',
interval: 'day',
},
})
.replyOnce(200, mockWafStatisticsResponse);
});
......
import createState from 'ee/threat_monitoring/store/modules/threat_monitoring/state';
import * as getters from 'ee/threat_monitoring/store/modules/threat_monitoring/getters';
import { INVALID_CURRENT_ENVIRONMENT_NAME } from 'ee/threat_monitoring/store/modules/threat_monitoring/constants';
import { INVALID_CURRENT_ENVIRONMENT_NAME, TIME_WINDOWS } from 'ee/threat_monitoring/constants';
describe('threatMonitoring module getters', () => {
let state;
......@@ -26,4 +26,18 @@ describe('threatMonitoring module getters', () => {
});
});
});
describe('currentTimeWindowName', () => {
it('gives the correct name for a valid time window', () => {
Object.keys(TIME_WINDOWS).forEach(timeWindow => {
state.currentTimeWindow = timeWindow;
expect(getters.currentTimeWindowName(state)).toBe(TIME_WINDOWS[timeWindow].name);
});
});
it('gives the default name for an invalid time window', () => {
state.currentTimeWindowName = 'foo';
expect(getters.currentTimeWindowName(state)).toBe('30 days');
});
});
});
......@@ -80,6 +80,18 @@ describe('Threat Monitoring mutations', () => {
});
});
describe(types.SET_CURRENT_TIME_WINDOW, () => {
const timeWindow = 'foo';
beforeEach(() => {
mutations[types.SET_CURRENT_TIME_WINDOW](state, timeWindow);
});
it('sets currentTimeWindow', () => {
expect(state.currentTimeWindow).toBe(timeWindow);
});
});
describe(types.REQUEST_WAF_STATISTICS, () => {
beforeEach(() => {
mutations[types.REQUEST_WAF_STATISTICS](state);
......
import {
getTimeWindowConfig,
getTimeWindowParams,
} from 'ee/threat_monitoring/store/modules/threat_monitoring/utils';
import { DEFAULT_TIME_WINDOW, TIME_WINDOWS } from 'ee/threat_monitoring/constants';
describe('threatMonitoring module utils', () => {
describe('getTimeWindowConfig', () => {
it('gives the correct config for a valid time window', () => {
Object.entries(TIME_WINDOWS).forEach(([timeWindow, expectedConfig]) => {
expect(getTimeWindowConfig(timeWindow)).toBe(expectedConfig);
});
});
it('gives the default name for an invalid time window', () => {
expect(getTimeWindowConfig('foo')).toBe(TIME_WINDOWS[DEFAULT_TIME_WINDOW]);
});
});
describe('getTimeWindowParams', () => {
const mockTimestamp = new Date(2020, 0, 1, 10).getTime();
it.each`
timeWindow | expectedFrom | interval
${'thirtyMinutes'} | ${'2020-01-01T09:30:00.000Z'} | ${'minute'}
${'oneHour'} | ${'2020-01-01T09:00:00.000Z'} | ${'minute'}
${'twentyFourHours'} | ${'2019-12-31T10:00:00.000Z'} | ${'hour'}
${'sevenDays'} | ${'2019-12-25T10:00:00.000Z'} | ${'hour'}
${'thirtyDays'} | ${'2019-12-02T10:00:00.000Z'} | ${'day'}
${'foo'} | ${'2019-12-02T10:00:00.000Z'} | ${'day'}
`(
'returns the expected params given "$timeWindow"',
({ timeWindow, expectedFrom, interval }) => {
expect(getTimeWindowParams(timeWindow, mockTimestamp)).toEqual({
from: expectedFrom,
to: '2020-01-01T10:00:00.000Z',
interval,
});
},
);
});
});
......@@ -557,6 +557,9 @@ msgid_plural "%d groups"
msgstr[0] ""
msgstr[1] ""
msgid "1 hour"
msgstr ""
msgid "1 merged merge request"
msgid_plural "%{merge_requests} merged merge requests"
msgstr[0] ""
......@@ -607,6 +610,9 @@ msgstr ""
msgid "20-29 contributions"
msgstr ""
msgid "24 hours"
msgstr ""
msgid "2FA"
msgstr ""
......@@ -619,6 +625,9 @@ msgstr ""
msgid "3 hours"
msgstr ""
msgid "30 days"
msgstr ""
msgid "30 minutes"
msgstr ""
......@@ -640,6 +649,9 @@ msgstr ""
msgid "404|Please contact your GitLab administrator if you think this is a mistake."
msgstr ""
msgid "7 days"
msgstr ""
msgid "8 hours"
msgstr ""
......@@ -18797,6 +18809,9 @@ msgstr ""
msgid "ThreatMonitoring|Requests"
msgstr ""
msgid "ThreatMonitoring|Show last"
msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch WAF statistics"
msgstr ""
......
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