Commit 86498b5a authored by Jiaan Louw's avatar Jiaan Louw Committed by Martin Wortschack

Improve and limit audit event date filter

This adds quick date range selector options to the audit
events filter bar. It also limits the max date range to
31 days by default for improved performance.
parent 19211f3d
......@@ -743,3 +743,22 @@ export const differenceInMilliseconds = (startDate, endDate = Date.now()) => {
const endDateInMS = endDate instanceof Date ? endDate.getTime() : endDate;
return endDateInMS - startDateInMS;
};
/**
* A utility which returns a new date at the first day of the month for any given date.
*
* @param {Date} date
*
* @return {Date} the date at the first day of the month
*/
export const dateAtFirstDayOfMonth = date => new Date(newDate(date).setDate(1));
/**
* A utility function which checks if two dates match.
*
* @param {Date|Int} date1 Can be either a date object or a unix timestamp.
* @param {Date|Int} date2 Can be either a date object or a unix timestamp.
*
* @return {Boolean} true if the dates match
*/
export const datesMatch = (date1, date2) => differenceInMilliseconds(date1, date2) === 0;
......@@ -58,9 +58,9 @@ export default {
<audit-events-export-button v-if="hasExportUrl" :export-href="exportHref" />
</div>
</header>
<div class="row-content-block second-block pb-0">
<div class="d-flex justify-content-between audit-controls row">
<div class="col-lg-auto flex-fill form-group align-items-lg-center pr-lg-8">
<div class="row-content-block second-block gl-pb-0">
<div class="gl-display-flex gl-justify-content-space-between audit-controls gl-flex-wrap">
<div class="gl-mb-5 gl-w-full">
<audit-events-filter
:filter-token-options="filterTokenOptions"
:value="filterValue"
......@@ -68,9 +68,9 @@ export default {
@submit="searchForAuditEvents"
/>
</div>
<div class="d-flex col-lg-auto flex-wrap pl-lg-0">
<div class="gl-display-flex gl-flex-wrap gl-w-full">
<div
class="audit-controls d-flex align-items-lg-center flex-column flex-lg-row col-lg-auto px-0"
class="audit-controls gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-justify-content-space-between gl-px-0 gl-w-full"
>
<date-range-field
:start-date="startDate"
......
<script>
import { GlButtonGroup, GlButton } from '@gitlab/ui';
import { n__, s__ } from '~/locale';
import { datesMatch, dateAtFirstDayOfMonth, getDateInPast } from '~/lib/utils/datetime_utility';
import { CURRENT_DATE } from '../constants';
const DATE_RANGE_OPTIONS = [
{
text: n__('Last %d day', 'Last %d days', 7),
startDate: getDateInPast(CURRENT_DATE, 7),
endDate: CURRENT_DATE,
},
{
text: n__('Last %d day', 'Last %d days', 14),
startDate: getDateInPast(CURRENT_DATE, 14),
endDate: CURRENT_DATE,
},
{
text: s__('AuditLogs|This month'),
startDate: dateAtFirstDayOfMonth(CURRENT_DATE),
endDate: CURRENT_DATE,
},
];
export default {
components: {
GlButton,
GlButtonGroup,
},
props: {
dateRange: {
type: Object,
required: true,
},
},
methods: {
onDateRangeClicked({ startDate, endDate }) {
this.$emit('input', { startDate, endDate });
},
isCurrentDateRange({ startDate, endDate }) {
const { dateRange } = this;
return datesMatch(startDate, dateRange.startDate) && datesMatch(endDate, dateRange.endDate);
},
},
DATE_RANGE_OPTIONS,
};
</script>
<template>
<gl-button-group>
<gl-button
v-for="(dateRangeOption, idx) in $options.DATE_RANGE_OPTIONS"
:key="idx"
:selected="isCurrentDateRange(dateRangeOption)"
@click="onDateRangeClicked(dateRangeOption)"
>{{ dateRangeOption.text }}</gl-button
>
</gl-button-group>
</template>
<script>
import { GlDaterangePicker } from '@gitlab/ui';
import { dateAtFirstDayOfMonth } from '~/lib/utils/datetime_utility';
import { CURRENT_DATE, MAX_DATE_RANGE } from '../constants';
import DateRangeButtons from './date_range_buttons.vue';
export default {
components: {
DateRangeButtons,
GlDaterangePicker,
},
props: {
......@@ -17,21 +21,43 @@ export default {
default: null,
},
},
computed: {
defaultStartDate() {
return this.startDate || dateAtFirstDayOfMonth(CURRENT_DATE);
},
defaultEndDate() {
return this.endDate || CURRENT_DATE;
},
defaultDateRange() {
return { startDate: this.defaultStartDate, endDate: this.defaultEndDate };
},
},
methods: {
onInput(dates) {
this.$emit('selected', dates);
},
},
CURRENT_DATE,
MAX_DATE_RANGE,
};
</script>
<template>
<div
class="gl-display-flex gl-align-items-flex-end gl-xs-align-items-baseline gl-xs-flex-direction-column"
>
<div class="gl-pr-5 gl-mb-5">
<date-range-buttons :date-range="defaultDateRange" @input="onInput" />
</div>
<gl-daterange-picker
class="d-flex flex-wrap flex-sm-nowrap"
:default-start-date="startDate"
:default-end-date="endDate"
start-picker-class="form-group align-items-lg-center mr-0 mr-sm-1 d-flex flex-column flex-lg-row"
end-picker-class="form-group align-items-lg-center mr-0 mr-sm-2 d-flex flex-column flex-lg-row"
class="gl-display-flex gl-pl-0 gl-w-full"
:default-start-date="defaultStartDate"
:default-end-date="defaultEndDate"
:default-max-date="$options.CURRENT_DATE"
:max-date-range="$options.MAX_DATE_RANGE"
start-picker-class="gl-mb-5 gl-pr-5 gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-flex-fill-1"
end-picker-class="gl-mb-5 gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-flex-fill-1"
@input="onInput"
/>
</div>
</template>
......@@ -47,7 +47,7 @@ export default {
<template>
<div>
<gl-dropdown :text="selectedOption.text" class="w-100 flex-column flex-lg-row form-group">
<gl-dropdown :text="selectedOption.text" class="w-100 flex-column flex-lg-row gl-mb-5">
<gl-dropdown-section-header> {{ $options.SORTING_TITLE }}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="option in $options.SORTING_OPTIONS"
......
......@@ -56,3 +56,7 @@ export const AUDIT_FILTER_CONFIGS = [
];
export const AVAILABLE_TOKEN_TYPES = AUDIT_FILTER_CONFIGS.map(token => token.type);
export const MAX_DATE_RANGE = 31;
export const CURRENT_DATE = new Date();
.audit-controls .gl-dropdown-toggle {
.audit-controls {
.gl-dropdown-toggle {
border-color: $gray-100;
border-radius: 0.25rem;
box-shadow: inset 0 0 0 0.0625rem $gray-200;
padding: 0.5rem 0.75rem;
}
.gl-daterange-picker > div {
@include media-breakpoint-up(lg) {
align-items: baseline;
}
}
}
---
title: Add quick select date options to audit events filter
merge_request: 42711
author:
type: added
......@@ -13,13 +13,13 @@ exports[`AuditEventsApp when initialized matches the snapshot 1`] = `
</header>
<div
class="row-content-block second-block pb-0"
class="row-content-block second-block gl-pb-0"
>
<div
class="d-flex justify-content-between audit-controls row"
class="gl-display-flex gl-justify-content-space-between audit-controls gl-flex-wrap"
>
<div
class="col-lg-auto flex-fill form-group align-items-lg-center pr-lg-8"
class="gl-mb-5 gl-w-full"
>
<div
class="input-group bg-white flex-grow-1"
......@@ -37,10 +37,10 @@ exports[`AuditEventsApp when initialized matches the snapshot 1`] = `
</div>
<div
class="d-flex col-lg-auto flex-wrap pl-lg-0"
class="gl-display-flex gl-flex-wrap gl-w-full"
>
<div
class="audit-controls d-flex align-items-lg-center flex-column flex-lg-row col-lg-auto px-0"
class="audit-controls gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-justify-content-space-between gl-px-0 gl-w-full"
>
<date-range-field-stub
enddate="Sun Feb 02 2020 00:00:00 GMT+0000 (Greenwich Mean Time)"
......
import { shallowMount } from '@vue/test-utils';
import { GlButtonGroup, GlButton } from '@gitlab/ui';
import DateRangeButtons from 'ee/audit_events/components/date_range_buttons.vue';
import { CURRENT_DATE } from 'ee/audit_events/constants';
import { getDateInPast } from '~/lib/utils/datetime_utility';
describe('DateRangeButtons component', () => {
let wrapper;
const createComponent = (props = {}) => {
wrapper = shallowMount(DateRangeButtons, {
propsData: { ...props },
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('shows the selected the option that matches the provided dateRange property', () => {
createComponent({
dateRange: { startDate: getDateInPast(CURRENT_DATE, 7), endDate: CURRENT_DATE },
});
expect(
wrapper
.find(GlButtonGroup)
.find('[selected="true"]')
.text(),
).toBe('Last 7 days');
});
it('shows no date range as selected when the dateRange property does not match any option', () => {
createComponent({
dateRange: {
startDate: getDateInPast(CURRENT_DATE, 5),
endDate: getDateInPast(CURRENT_DATE, 2),
},
});
expect(
wrapper
.find(GlButtonGroup)
.find('[selected="true"]')
.exists(),
).toBe(false);
});
it('emits an "input" event with the dateRange when a new date range is selected', async () => {
createComponent({
dateRange: { startDate: getDateInPast(CURRENT_DATE, 1), endDate: CURRENT_DATE },
});
wrapper
.find(GlButtonGroup)
.find(GlButton)
.vm.$emit('click');
await wrapper.vm.$nextTick();
expect(wrapper.emitted().input[0]).toEqual([
{
startDate: getDateInPast(CURRENT_DATE, 7),
endDate: CURRENT_DATE,
},
]);
});
});
import { shallowMount } from '@vue/test-utils';
import { GlDaterangePicker } from '@gitlab/ui';
import DateRangeButtons from 'ee/audit_events/components/date_range_buttons.vue';
import DateRangeField from 'ee/audit_events/components/date_range_field.vue';
import { parsePikadayDate } from '~/lib/utils/datetime_utility';
import { CURRENT_DATE, MAX_DATE_RANGE } from 'ee/audit_events/constants';
import { dateAtFirstDayOfMonth, parsePikadayDate } from '~/lib/utils/datetime_utility';
describe('DateRangeField component', () => {
let wrapper;
......@@ -10,6 +12,9 @@ describe('DateRangeField component', () => {
const startDate = parsePikadayDate('2020-03-13');
const endDate = parsePikadayDate('2020-03-14');
const findDatePicker = () => wrapper.find(GlDaterangePicker);
const findDateRangeButtons = () => wrapper.find(DateRangeButtons);
const createComponent = (props = {}) => {
wrapper = shallowMount(DateRangeField, {
propsData: { ...props },
......@@ -21,39 +26,74 @@ describe('DateRangeField component', () => {
wrapper = null;
});
it('passes the startDate to the date picker as defaultStartDate', () => {
createComponent({ startDate });
describe('default behaviour', () => {
it('sets the max date range on the date picker', () => {
createComponent();
expect(wrapper.find(GlDaterangePicker).props()).toMatchObject({
defaultStartDate: startDate,
defaultEndDate: null,
expect(findDatePicker().props('maxDateRange')).toBe(MAX_DATE_RANGE);
});
it("sets the max selectable date to today's date on the date picker", () => {
createComponent();
expect(
findDatePicker()
.props('defaultMaxDate')
.toDateString(),
).toBe(CURRENT_DATE.toDateString());
});
it('passes the endDate to the date picker as defaultEndDate', () => {
createComponent({ endDate });
it('sets the default start date to the start of the month', () => {
createComponent();
expect(wrapper.find(GlDaterangePicker).props()).toMatchObject({
defaultStartDate: null,
defaultEndDate: endDate,
expect(
findDatePicker()
.props('defaultStartDate')
.toDateString(),
).toBe(dateAtFirstDayOfMonth(CURRENT_DATE).toDateString());
});
it("sets the default end date to today's date", () => {
createComponent();
expect(
findDatePicker()
.props('defaultEndDate')
.toDateString(),
).toBe(CURRENT_DATE.toDateString());
});
it('passes both startDate and endDate to the date picker as default dates', () => {
createComponent({ startDate, endDate });
expect(wrapper.find(GlDaterangePicker).props()).toMatchObject({
expect(findDatePicker().props()).toMatchObject({
defaultStartDate: startDate,
defaultEndDate: endDate,
});
});
});
describe('when a new date range is picked', () => {
it('emits the "selected" event with the picked startDate and endDate', async () => {
createComponent();
findDatePicker().vm.$emit('input', { startDate, endDate });
await wrapper.vm.$nextTick();
expect(wrapper.emitted().selected[0]).toEqual([
{
startDate,
endDate,
},
]);
});
});
it('should emit the "selected" event with startDate and endDate on input change', () => {
describe('when a date range button is pressed', () => {
it('emits the "selected" event with the picked startDate and endDate', async () => {
createComponent();
wrapper.find(GlDaterangePicker).vm.$emit('input', { startDate, endDate });
findDateRangeButtons().vm.$emit('input', { startDate, endDate });
return wrapper.vm.$nextTick(() => {
expect(wrapper.emitted().selected).toBeTruthy();
await wrapper.vm.$nextTick();
expect(wrapper.emitted().selected[0]).toEqual([
{
startDate,
......
......@@ -3666,6 +3666,9 @@ msgstr ""
msgid "AuditLogs|Target"
msgstr ""
msgid "AuditLogs|This month"
msgstr ""
msgid "AuditLogs|User Events"
msgstr ""
......
......@@ -667,3 +667,26 @@ describe('differenceInMilliseconds', () => {
expect(datetimeUtility.differenceInMilliseconds(startDate, endDate)).toBe(expected);
});
});
describe('dateAtFirstDayOfMonth', () => {
const date = new Date('2019-07-16T12:00:00.000Z');
it('returns the date at the first day of the month', () => {
const startDate = datetimeUtility.dateAtFirstDayOfMonth(date);
const expectedStartDate = new Date('2019-07-01T12:00:00.000Z');
expect(startDate).toStrictEqual(expectedStartDate);
});
});
describe('datesMatch', () => {
const date = new Date('2019-07-17T00:00:00.000Z');
it.each`
date1 | date2 | expected
${date} | ${new Date('2019-07-17T00:00:00.000Z')} | ${true}
${date} | ${new Date('2019-07-17T12:00:00.000Z')} | ${false}
`('returns $expected for $date1 matches $date2', ({ date1, date2, expected }) => {
expect(datetimeUtility.datesMatch(date1, date2)).toBe(expected);
});
});
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