Commit 355f2c6b authored by Jiaan Louw's avatar Jiaan Louw Committed by Paul Slaughter

Remove form logic from audit event components

https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33741
parent d3c5b084
<script> <script>
import { mapActions, mapState } from 'vuex';
import AuditEventsFilter from './audit_events_filter.vue'; import AuditEventsFilter from './audit_events_filter.vue';
import DateRangeField from './date_range_field.vue'; import DateRangeField from './date_range_field.vue';
import SortingField from './sorting_field.vue'; import SortingField from './sorting_field.vue';
...@@ -12,10 +13,6 @@ export default { ...@@ -12,10 +13,6 @@ export default {
AuditEventsTable, AuditEventsTable,
}, },
props: { props: {
formPath: {
type: String,
required: true,
},
events: { events: {
type: Array, type: Array,
required: false, required: false,
...@@ -41,16 +38,11 @@ export default { ...@@ -41,16 +38,11 @@ export default {
default: undefined, default: undefined,
}, },
}, },
data() { computed: {
return { ...mapState(['filterValue', 'startDate', 'endDate', 'sortBy']),
formElement: null,
};
}, },
mounted() { methods: {
// Passing the form to child components is only temporary ...mapActions(['setDateRange', 'setFilterValue', 'setSortBy', 'searchForAuditEvents']),
// and should be changed when this issue is completed:
// https://gitlab.com/gitlab-org/gitlab/-/issues/217759
this.formElement = this.$refs.form;
}, },
}; };
</script> </script>
...@@ -58,25 +50,34 @@ export default { ...@@ -58,25 +50,34 @@ export default {
<template> <template>
<div> <div>
<div class="row-content-block second-block pb-0"> <div class="row-content-block second-block pb-0">
<form <div class="d-flex justify-content-between audit-controls row">
ref="form"
method="GET"
:path="formPath"
class="filter-form 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="col-lg-auto flex-fill form-group align-items-lg-center pr-lg-8">
<audit-events-filter v-bind="{ enabledTokenTypes, qaSelector: filterQaSelector }" /> <audit-events-filter
:enabled-token-types="enabledTokenTypes"
:qa-selector="filterQaSelector"
:value="filterValue"
@selected="setFilterValue"
@submit="searchForAuditEvents"
/>
</div> </div>
<div class="d-flex col-lg-auto flex-wrap pl-lg-0"> <div class="d-flex col-lg-auto flex-wrap pl-lg-0">
<div <div
class="audit-controls d-flex align-items-lg-center flex-column flex-lg-row col-lg-auto px-0" class="audit-controls d-flex align-items-lg-center flex-column flex-lg-row col-lg-auto px-0"
> >
<date-range-field v-if="formElement" :form-element="formElement" /> <date-range-field
<sorting-field /> :start-date="startDate"
:end-date="endDate"
@selected="setDateRange"
/>
<sorting-field :sort-by="sortBy" @selected="setSortBy" />
</div> </div>
</div> </div>
</form> </div>
</div> </div>
<audit-events-table v-bind="{ events, isLastPage, qaSelector: tableQaSelector }" /> <audit-events-table
:events="events"
:is-last-page="isLastPage"
:qa-selector="tableQaSelector"
/>
</div> </div>
</template> </template>
<script> <script>
import { GlFilteredSearch } from '@gitlab/ui'; import { GlFilteredSearch } from '@gitlab/ui';
import { queryToObject } from '~/lib/utils/url_utility';
import { FILTER_TOKENS, AVAILABLE_TOKEN_TYPES } from '../constants'; import { FILTER_TOKENS, AVAILABLE_TOKEN_TYPES } from '../constants';
import { availableTokensValidator } from '../validators'; import { availableTokensValidator } from '../validators';
...@@ -9,6 +8,11 @@ export default { ...@@ -9,6 +8,11 @@ export default {
GlFilteredSearch, GlFilteredSearch,
}, },
props: { props: {
value: {
type: Array,
required: false,
default: () => [],
},
enabledTokenTypes: { enabledTokenTypes: {
type: Array, type: Array,
required: false, required: false,
...@@ -21,14 +25,9 @@ export default { ...@@ -21,14 +25,9 @@ export default {
default: undefined, default: undefined,
}, },
}, },
data() {
return {
searchTerms: [],
};
},
computed: { computed: {
searchTerm() { searchTerm() {
return this.searchTerms.find(term => AVAILABLE_TOKEN_TYPES.includes(term.type)); return this.value.find(term => AVAILABLE_TOKEN_TYPES.includes(term.type));
}, },
enabledTokens() { enabledTokens() {
return FILTER_TOKENS.filter(token => this.enabledTokenTypes.includes(token.type)); return FILTER_TOKENS.filter(token => this.enabledTokenTypes.includes(token.type));
...@@ -36,39 +35,23 @@ export default { ...@@ -36,39 +35,23 @@ export default {
filterTokens() { filterTokens() {
// This limits the user to search by only one of the available tokens // This limits the user to search by only one of the available tokens
const { enabledTokens, searchTerm } = this; const { enabledTokens, searchTerm } = this;
if (searchTerm?.type) { if (searchTerm?.type) {
return enabledTokens.map(token => ({ return enabledTokens.map(token => ({
...token, ...token,
disabled: searchTerm.type !== token.type, disabled: searchTerm.type !== token.type,
})); }));
} }
return enabledTokens; return enabledTokens;
}, },
id() {
return this.searchTerm?.value?.data;
},
type() {
return this.searchTerm?.type;
},
},
created() {
this.setSearchTermsFromQuery();
}, },
methods: { methods: {
// The form logic here will be removed once all the audit onSubmit() {
// components are migrated into a single Vue application. this.$emit('submit');
// https://gitlab.com/gitlab-org/gitlab/-/issues/215363
getFormElement() {
return this.$refs.input.form;
}, },
setSearchTermsFromQuery() { onInput(val) {
const { entity_type: type, entity_id: value } = queryToObject(window.location.search); this.$emit('selected', val);
if (type && value) {
this.searchTerms = [{ type, value: { data: value, operator: '=' } }];
}
},
filteredSearchSubmit() {
this.getFormElement().submit();
}, },
}, },
}; };
...@@ -81,16 +64,14 @@ export default { ...@@ -81,16 +64,14 @@ export default {
:data-qa-selector="qaSelector" :data-qa-selector="qaSelector"
> >
<gl-filtered-search <gl-filtered-search
v-model="searchTerms" :value="value"
:placeholder="__('Search')" :placeholder="__('Search')"
:clear-button-title="__('Clear')" :clear-button-title="__('Clear')"
:close-button-title="__('Close')" :close-button-title="__('Close')"
:available-tokens="filterTokens" :available-tokens="filterTokens"
class="gl-h-32 w-100" class="gl-h-32 w-100"
@submit="filteredSearchSubmit" @submit="onSubmit"
@input="onInput"
/> />
<input ref="input" v-model="type" type="hidden" name="entity_type" />
<input v-model="id" type="hidden" name="entity_id" />
</div> </div>
</template> </template>
<script> <script>
import { GlDaterangePicker } from '@gitlab/ui'; import { GlDaterangePicker } from '@gitlab/ui';
import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility';
import { queryToObject } from '~/lib/utils/url_utility';
export default { export default {
components: { components: {
GlDaterangePicker, GlDaterangePicker,
}, },
props: { props: {
formElement: { startDate: {
type: HTMLFormElement, type: Date,
required: true, required: false,
default: null,
}, },
}, endDate: {
data() { type: Date,
const data = { required: false,
startDate: null, default: null,
endDate: null,
};
const { created_after: initialStartDate, created_before: initialEndDate } = queryToObject(
window.location.search,
);
if (initialStartDate) {
data.startDate = parsePikadayDate(initialStartDate);
}
if (initialEndDate) {
data.endDate = parsePikadayDate(initialEndDate);
}
return data;
},
computed: {
createdAfter() {
return this.startDate ? pikadayToString(this.startDate) : '';
},
createdBefore() {
return this.endDate ? pikadayToString(this.endDate) : '';
}, },
}, },
methods: { methods: {
handleInput(dates) { onInput(dates) {
this.startDate = dates.startDate; this.$emit('selected', dates);
this.endDate = dates.endDate;
this.$nextTick(() => this.formElement.submit());
}, },
}, },
}; };
</script> </script>
<template> <template>
<div> <gl-daterange-picker
<gl-daterange-picker class="d-flex flex-wrap flex-sm-nowrap"
class="d-flex flex-wrap flex-sm-nowrap" :default-start-date="startDate"
:default-start-date="startDate" :default-end-date="endDate"
: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"
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"
end-picker-class="form-group align-items-lg-center mr-0 mr-sm-2 d-flex flex-column flex-lg-row" @input="onInput"
@input="handleInput" />
/>
<input type="hidden" name="created_after" :value="createdAfter" />
<input type="hidden" name="created_before" :value="createdBefore" />
</div>
</template> </template>
<script> <script>
import { GlNewDropdown, GlNewDropdownHeader, GlNewDropdownItem } from '@gitlab/ui'; import { GlNewDropdown, GlNewDropdownHeader, GlNewDropdownItem } from '@gitlab/ui';
import { setUrlParams, queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
const SORTING_TITLE = s__('SortOptions|Sort by:'); const SORTING_TITLE = s__('SortOptions|Sort by:');
...@@ -22,24 +20,24 @@ export default { ...@@ -22,24 +20,24 @@ export default {
GlNewDropdownHeader, GlNewDropdownHeader,
GlNewDropdownItem, GlNewDropdownItem,
}, },
data() { props: {
const { sort: selectedOption } = queryToObject(window.location.search); sortBy: {
type: String,
return { required: false,
selectedOption: selectedOption || SORTING_OPTIONS[0].key, default: null,
}; },
}, },
computed: { computed: {
selectedOptionText() { selectedOption() {
return SORTING_OPTIONS.find(option => option.key === this.selectedOption).text; return SORTING_OPTIONS.find(option => option.key === this.sortBy) || SORTING_OPTIONS[0];
}, },
}, },
methods: { methods: {
getItemLink(key) { onItemClick(option) {
return setUrlParams({ sort: key }); this.$emit('selected', option);
}, },
isChecked(key) { isChecked(key) {
return key === this.selectedOption; return key === this.selectedOption.key;
}, },
}, },
SORTING_TITLE, SORTING_TITLE,
...@@ -49,23 +47,17 @@ export default { ...@@ -49,23 +47,17 @@ export default {
<template> <template>
<div> <div>
<gl-new-dropdown <gl-new-dropdown :text="selectedOption.text" class="w-100 flex-column flex-lg-row form-group">
v-model="selectedOption"
:text="selectedOptionText"
class="w-100 flex-column flex-lg-row form-group"
>
<gl-new-dropdown-header> {{ $options.SORTING_TITLE }}</gl-new-dropdown-header> <gl-new-dropdown-header> {{ $options.SORTING_TITLE }}</gl-new-dropdown-header>
<gl-new-dropdown-item <gl-new-dropdown-item
v-for="option in $options.SORTING_OPTIONS" v-for="option in $options.SORTING_OPTIONS"
:key="option.key" :key="option.key"
:is-check-item="true" :is-check-item="true"
:is-checked="isChecked(option.key)" :is-checked="isChecked(option.key)"
:href="getItemLink(option.key)" @click="onItemClick(option.key)"
> >
{{ option.text }} {{ option.text }}
</gl-new-dropdown-item> </gl-new-dropdown-item>
</gl-new-dropdown> </gl-new-dropdown>
<input type="hidden" name="sort" :value="selectedOption" />
</div> </div>
</template> </template>
import Vue from 'vue'; import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import AuditEventsApp from './components/audit_events_app.vue'; import AuditEventsApp from './components/audit_events_app.vue';
import createStore from './store';
export default selector => { export default selector => {
const el = document.querySelector(selector); const el = document.querySelector(selector);
const { const { events, isLastPage, enabledTokenTypes, filterQaSelector, tableQaSelector } = el.dataset;
events,
isLastPage, const store = createStore();
formPath, store.dispatch('initializeAuditEvents');
enabledTokenTypes,
filterQaSelector,
tableQaSelector,
} = el.dataset;
return new Vue({ return new Vue({
el, el,
store,
render: createElement => render: createElement =>
createElement(AuditEventsApp, { createElement(AuditEventsApp, {
props: { props: {
events: JSON.parse(events), events: JSON.parse(events),
isLastPage: parseBoolean(isLastPage), isLastPage: parseBoolean(isLastPage),
enabledTokenTypes: JSON.parse(enabledTokenTypes), enabledTokenTypes: JSON.parse(enabledTokenTypes),
formPath,
filterQaSelector, filterQaSelector,
tableQaSelector, tableQaSelector,
}, },
......
...@@ -18,9 +18,8 @@ export const setDateRange = ({ commit, dispatch }, { startDate, endDate }) => { ...@@ -18,9 +18,8 @@ export const setDateRange = ({ commit, dispatch }, { startDate, endDate }) => {
dispatch('searchForAuditEvents'); dispatch('searchForAuditEvents');
}; };
export const setFilterValue = ({ commit, dispatch }, { id, type }) => { export const setFilterValue = ({ commit }, filterValue) => {
commit(types.SET_FILTER_VALUE, { id, type }); commit(types.SET_FILTER_VALUE, filterValue);
dispatch('searchForAuditEvents');
}; };
export const setSortBy = ({ commit, dispatch }, sortBy) => { export const setSortBy = ({ commit, dispatch }, sortBy) => {
......
...@@ -11,14 +11,14 @@ export default { ...@@ -11,14 +11,14 @@ export default {
sort: sortBy = null, sort: sortBy = null,
} = {}, } = {},
) { ) {
state.filterValue = { id, type }; state.filterValue = type && id ? [{ type, value: { data: id, operator: '=' } }] : [];
state.startDate = startDate; state.startDate = startDate;
state.endDate = endDate; state.endDate = endDate;
state.sortBy = sortBy; state.sortBy = sortBy;
}, },
[types.SET_FILTER_VALUE](state, { id, type }) { [types.SET_FILTER_VALUE](state, filterValue) {
state.filterValue = { id, type }; state.filterValue = filterValue;
}, },
[types.SET_DATE_RANGE](state, { startDate, endDate }) { [types.SET_DATE_RANGE](state, { startDate, endDate }) {
......
export default () => ({ export default () => ({
filterValue: { filterValue: [],
id: null,
type: null,
},
startDate: null, startDate: null,
endDate: null, endDate: null,
......
import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility'; import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility';
import { AVAILABLE_TOKEN_TYPES } from './constants';
export const isNumeric = str => { export const isNumeric = str => {
return !Number.isNaN(parseInt(str, 10), 10); return !Number.isNaN(parseInt(str, 10), 10);
...@@ -14,10 +15,16 @@ export const parseAuditEventSearchQuery = ({ ...@@ -14,10 +15,16 @@ export const parseAuditEventSearchQuery = ({
created_before: createdBefore ? parsePikadayDate(createdBefore) : null, created_before: createdBefore ? parsePikadayDate(createdBefore) : null,
}); });
export const createAuditEventSearchQuery = ({ filterValue, startDate, endDate, sortBy }) => ({ export const createAuditEventSearchQuery = ({ filterValue, startDate, endDate, sortBy }) => {
entity_id: filterValue.id, const entityValue = filterValue.find(value => AVAILABLE_TOKEN_TYPES.includes(value.type));
entity_type: filterValue.type,
created_after: startDate ? pikadayToString(startDate) : null, return {
created_before: endDate ? pikadayToString(endDate) : null, created_after: startDate ? pikadayToString(startDate) : null,
sort: sortBy, created_before: endDate ? pikadayToString(endDate) : null,
}); sort: sortBy,
entity_id: entityValue?.value.data,
entity_type: entityValue?.type,
// When changing the search parameters, we should be resetting to the first page
page: null,
};
};
...@@ -162,9 +162,10 @@ RSpec.describe 'Admin::AuditLogs', :js do ...@@ -162,9 +162,10 @@ RSpec.describe 'Admin::AuditLogs', :js do
end end
def filter_for(type, name) def filter_for(type, name)
within '[data-qa-selector="admin_audit_log_filter"]' do filter_container = '[data-testid="audit-events-filter"]'
find('input').click
find(filter_container).click
within filter_container do
click_link type click_link type
click_link name click_link name
......
...@@ -5,10 +5,8 @@ exports[`AuditEventsApp when initialized matches the snapshot 1`] = ` ...@@ -5,10 +5,8 @@ exports[`AuditEventsApp when initialized matches the snapshot 1`] = `
<div <div
class="row-content-block second-block pb-0" class="row-content-block second-block pb-0"
> >
<form <div
class="filter-form d-flex justify-content-between audit-controls row" class="d-flex justify-content-between audit-controls row"
method="GET"
path="form/path"
> >
<div <div
class="col-lg-auto flex-fill form-group align-items-lg-center pr-lg-8" class="col-lg-auto flex-fill form-group align-items-lg-center pr-lg-8"
...@@ -24,17 +22,7 @@ exports[`AuditEventsApp when initialized matches the snapshot 1`] = ` ...@@ -24,17 +22,7 @@ exports[`AuditEventsApp when initialized matches the snapshot 1`] = `
clearbuttontitle="Clear" clearbuttontitle="Clear"
close-button-title="Close" close-button-title="Close"
placeholder="Search" placeholder="Search"
value="" value="[object Object]"
/>
<input
name="entity_type"
type="hidden"
/>
<input
name="entity_id"
type="hidden"
/> />
</div> </div>
</div> </div>
...@@ -46,13 +34,16 @@ exports[`AuditEventsApp when initialized matches the snapshot 1`] = ` ...@@ -46,13 +34,16 @@ exports[`AuditEventsApp when initialized matches the snapshot 1`] = `
class="audit-controls d-flex align-items-lg-center flex-column flex-lg-row col-lg-auto px-0" class="audit-controls d-flex align-items-lg-center flex-column flex-lg-row col-lg-auto px-0"
> >
<date-range-field-stub <date-range-field-stub
formelement="[object HTMLFormElement]" enddate="Sun Feb 02 2020 00:00:00 GMT+0000 (Greenwich Mean Time)"
startdate="Wed Jan 01 2020 00:00:00 GMT+0000 (Greenwich Mean Time)"
/> />
<sorting-field-stub /> <sorting-field-stub
sortby="created_asc"
/>
</div> </div>
</div> </div>
</form> </div>
</div> </div>
<audit-events-table-stub <audit-events-table-stub
......
...@@ -2,12 +2,20 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -2,12 +2,20 @@ import { shallowMount } from '@vue/test-utils';
import AuditEventsApp from 'ee/audit_events/components/audit_events_app.vue'; import AuditEventsApp from 'ee/audit_events/components/audit_events_app.vue';
import DateRangeField from 'ee/audit_events/components/date_range_field.vue'; import DateRangeField from 'ee/audit_events/components/date_range_field.vue';
import SortingField from 'ee/audit_events/components/sorting_field.vue';
import AuditEventsTable from 'ee/audit_events/components/audit_events_table.vue'; import AuditEventsTable from 'ee/audit_events/components/audit_events_table.vue';
import AuditEventsFilter from 'ee/audit_events/components/audit_events_filter.vue'; import AuditEventsFilter from 'ee/audit_events/components/audit_events_filter.vue';
import { AVAILABLE_TOKEN_TYPES } from 'ee/audit_events/constants'; import { AVAILABLE_TOKEN_TYPES } from 'ee/audit_events/constants';
import createStore from 'ee/audit_events/store';
const TEST_SORT_BY = 'created_asc';
const TEST_START_DATE = new Date('2020-01-01');
const TEST_END_DATE = new Date('2020-02-02');
const TEST_FILTER_VALUE = [{ id: 50, type: 'User' }];
describe('AuditEventsApp', () => { describe('AuditEventsApp', () => {
let wrapper; let wrapper;
let store;
const events = [{ foo: 'bar' }]; const events = [{ foo: 'bar' }];
const enabledTokenTypes = AVAILABLE_TOKEN_TYPES; const enabledTokenTypes = AVAILABLE_TOKEN_TYPES;
...@@ -16,8 +24,8 @@ describe('AuditEventsApp', () => { ...@@ -16,8 +24,8 @@ describe('AuditEventsApp', () => {
const initComponent = (props = {}) => { const initComponent = (props = {}) => {
wrapper = shallowMount(AuditEventsApp, { wrapper = shallowMount(AuditEventsApp, {
store,
propsData: { propsData: {
formPath: 'form/path',
isLastPage: true, isLastPage: true,
filterQaSelector, filterQaSelector,
tableQaSelector, tableQaSelector,
...@@ -31,9 +39,20 @@ describe('AuditEventsApp', () => { ...@@ -31,9 +39,20 @@ describe('AuditEventsApp', () => {
}); });
}; };
beforeEach(() => {
store = createStore();
Object.assign(store.state, {
startDate: TEST_START_DATE,
endDate: TEST_END_DATE,
sortBy: TEST_SORT_BY,
filterValue: TEST_FILTER_VALUE,
});
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
store = null;
}); });
describe('when initialized', () => { describe('when initialized', () => {
...@@ -45,25 +64,51 @@ describe('AuditEventsApp', () => { ...@@ -45,25 +64,51 @@ describe('AuditEventsApp', () => {
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
it('sets the form element on the date range field', () => { it('renders audit events table', () => {
const { element } = wrapper.find('form'); expect(wrapper.find(AuditEventsTable).props()).toEqual({
expect(wrapper.find(DateRangeField).props('formElement')).toEqual(element); events,
qaSelector: tableQaSelector,
isLastPage: true,
});
});
it('renders audit events filter', () => {
expect(wrapper.find(AuditEventsFilter).props()).toEqual({
enabledTokenTypes,
qaSelector: filterQaSelector,
value: TEST_FILTER_VALUE,
});
}); });
it('passes its events property to the logs table', () => { it('renders date range field', () => {
expect(wrapper.find(AuditEventsTable).props('events')).toEqual(events); expect(wrapper.find(DateRangeField).props()).toEqual({
startDate: TEST_START_DATE,
endDate: TEST_END_DATE,
});
}); });
it('passes the tables QA selector to the logs table', () => { it('renders sorting field', () => {
expect(wrapper.find(AuditEventsTable).props('qaSelector')).toEqual(tableQaSelector); expect(wrapper.find(SortingField).props()).toEqual({ sortBy: TEST_SORT_BY });
}); });
});
it('passes its available token types to the logs filter', () => { describe('when a field is selected', () => {
expect(wrapper.find(AuditEventsFilter).props('enabledTokenTypes')).toEqual(enabledTokenTypes); beforeEach(() => {
jest.spyOn(store, 'dispatch').mockImplementation();
initComponent();
}); });
it('passes the filters QA selector to the logs filter', () => { it.each`
expect(wrapper.find(AuditEventsFilter).props('qaSelector')).toEqual(filterQaSelector); name | field | action | payload
${'date range'} | ${DateRangeField} | ${'setDateRange'} | ${'test'}
${'sort by'} | ${SortingField} | ${'setSortBy'} | ${'test'}
${'events filter'} | ${AuditEventsFilter} | ${'setFilterValue'} | ${'test'}
`('for $name, it calls $handler', ({ field, action, payload }) => {
expect(store.dispatch).not.toHaveBeenCalled();
wrapper.find(field).vm.$emit('selected', payload);
expect(store.dispatch).toHaveBeenCalledWith(action, payload);
}); });
}); });
}); });
...@@ -6,9 +6,8 @@ import { AVAILABLE_TOKEN_TYPES } from 'ee/audit_events/constants'; ...@@ -6,9 +6,8 @@ import { AVAILABLE_TOKEN_TYPES } from 'ee/audit_events/constants';
describe('AuditEventsFilter', () => { describe('AuditEventsFilter', () => {
let wrapper; let wrapper;
const formElement = document.createElement('form');
formElement.submit = jest.fn();
const value = [{ type: 'Project', value: { data: 1, operator: '=' } }];
const findFilteredSearch = () => wrapper.find(GlFilteredSearch); const findFilteredSearch = () => wrapper.find(GlFilteredSearch);
const getAvailableTokens = () => findFilteredSearch().props('availableTokens'); const getAvailableTokens = () => findFilteredSearch().props('availableTokens');
const getAvailableTokenProps = type => const getAvailableTokenProps = type =>
...@@ -19,9 +18,6 @@ describe('AuditEventsFilter', () => { ...@@ -19,9 +18,6 @@ describe('AuditEventsFilter', () => {
propsData: { propsData: {
...props, ...props,
}, },
methods: {
getFormElement: () => formElement,
},
}); });
}; };
...@@ -46,74 +42,59 @@ describe('AuditEventsFilter', () => { ...@@ -46,74 +42,59 @@ describe('AuditEventsFilter', () => {
}); });
}); });
describe('when the URL query has a search term', () => { describe('when the default token value is set', () => {
const type = 'User';
const id = '1';
beforeEach(() => { beforeEach(() => {
delete window.location; initComponent({ value });
window.location = { search: `entity_type=${type}&entity_id=${id}` };
initComponent();
}); });
it('sets the filtered searched token', () => { it('sets the filtered searched token', () => {
expect(findFilteredSearch().props('value')).toMatchObject([ expect(findFilteredSearch().props('value')).toEqual(value);
{
type,
value: {
data: id,
},
},
]);
}); });
});
describe('when the URL query is empty', () => { it('only one token matching the selected token type is enabled', () => {
beforeEach(() => { expect(getAvailableTokenProps('Project').disabled).toEqual(false);
delete window.location; expect(getAvailableTokenProps('Group').disabled).toEqual(true);
window.location = { search: '' }; expect(getAvailableTokenProps('User').disabled).toEqual(true);
initComponent();
}); });
it('has an empty search value', () => { describe('and the user submits the search field', () => {
expect(findFilteredSearch().vm.value).toEqual([]); beforeEach(() => {
findFilteredSearch().vm.$emit('submit');
});
it('should emit the "submit" event', () => {
expect(wrapper.emitted().submit).toHaveLength(1);
});
}); });
}); });
describe('when submitting the filtered search', () => { describe('when the default token value is not set', () => {
beforeEach(() => { beforeEach(() => {
initComponent(); initComponent();
findFilteredSearch().vm.$emit('submit');
}); });
it("calls submit on this component's FORM element", () => { it('has an empty search value', () => {
expect(formElement.submit).toHaveBeenCalledWith(); expect(findFilteredSearch().vm.value).toEqual([]);
}); });
});
describe('when a search token has been selected', () => { describe('and the user inputs nothing into the search field', () => {
const searchTerm = { beforeEach(() => {
value: { data: '1' }, findFilteredSearch().vm.$emit('input', []);
type: 'Project',
};
beforeEach(() => {
initComponent();
wrapper.setData({
searchTerms: [searchTerm],
}); });
});
it('only one token matching the selected type is available', () => { it('should emit the "selected" event with empty values', () => {
expect(getAvailableTokenProps('Project').disabled).toEqual(false); expect(wrapper.emitted().selected[0]).toEqual([[]]);
expect(getAvailableTokenProps('Group').disabled).toEqual(true); });
expect(getAvailableTokenProps('User').disabled).toEqual(true);
}); describe('and the user submits the search field', () => {
beforeEach(() => {
findFilteredSearch().vm.$emit('submit');
});
it('sets the input values according to the search term', () => { it('should emit the "submit" event', () => {
expect(wrapper.find('input[name="entity_type"]').attributes().value).toEqual(searchTerm.type); expect(wrapper.emitted().submit).toHaveLength(1);
expect(wrapper.find('input[name="entity_id"]').attributes().value).toEqual( });
searchTerm.value.data, });
);
}); });
}); });
......
...@@ -5,81 +5,61 @@ import DateRangeField from 'ee/audit_events/components/date_range_field.vue'; ...@@ -5,81 +5,61 @@ import DateRangeField from 'ee/audit_events/components/date_range_field.vue';
import { parsePikadayDate } from '~/lib/utils/datetime_utility'; import { parsePikadayDate } from '~/lib/utils/datetime_utility';
describe('DateRangeField component', () => { describe('DateRangeField component', () => {
const DATE = '1970-01-01';
let wrapper; let wrapper;
const createComponent = (props = {}) => { const startDate = parsePikadayDate('2020-03-13');
const formElement = document.createElement('form'); const endDate = parsePikadayDate('2020-03-14');
document.body.appendChild(formElement);
return shallowMount(DateRangeField, { const createComponent = (props = {}) => {
propsData: { formElement, ...props }, wrapper = shallowMount(DateRangeField, {
propsData: { ...props },
}); });
}; };
beforeEach(() => {
delete window.location;
window.location = { search: '' };
});
afterEach(() => { afterEach(() => {
document.querySelector('form').remove();
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
it('should populate the initial start date if passed in the query string', () => { it('passes the startDate to the date picker as defaultStartDate', () => {
window.location.search = `?created_after=${DATE}`; createComponent({ startDate });
wrapper = createComponent();
expect(wrapper.find(GlDaterangePicker).props()).toMatchObject({ expect(wrapper.find(GlDaterangePicker).props()).toMatchObject({
defaultStartDate: parsePikadayDate(DATE), defaultStartDate: startDate,
defaultEndDate: null, defaultEndDate: null,
}); });
}); });
it('should populate the initial end date if passed in the query string', () => { it('passes the endDate to the date picker as defaultEndDate', () => {
window.location.search = `?created_before=${DATE}`; createComponent({ endDate });
wrapper = createComponent();
expect(wrapper.find(GlDaterangePicker).props()).toMatchObject({ expect(wrapper.find(GlDaterangePicker).props()).toMatchObject({
defaultStartDate: null, defaultStartDate: null,
defaultEndDate: parsePikadayDate(DATE), defaultEndDate: endDate,
}); });
}); });
it('should populate both the initial start and end dates if passed in the query string', () => { it('passes both startDate and endDate to the date picker as default dates', () => {
window.location.search = `?created_after=${DATE}&created_before=${DATE}`; createComponent({ startDate, endDate });
wrapper = createComponent();
expect(wrapper.find(GlDaterangePicker).props()).toMatchObject({ expect(wrapper.find(GlDaterangePicker).props()).toMatchObject({
defaultStartDate: parsePikadayDate(DATE), defaultStartDate: startDate,
defaultEndDate: parsePikadayDate(DATE), defaultEndDate: endDate,
}); });
}); });
it('should populate the date hidden fields on input', () => { it('should emit the "selected" event with startDate and endDate on input change', () => {
wrapper = createComponent(); createComponent();
wrapper.find(GlDaterangePicker).vm.$emit('input', { startDate, endDate });
wrapper
.find(GlDaterangePicker) return wrapper.vm.$nextTick(() => {
.vm.$emit('input', { startDate: parsePikadayDate(DATE), endDate: parsePikadayDate(DATE) }); expect(wrapper.emitted().selected).toBeTruthy();
expect(wrapper.emitted().selected[0]).toEqual([
return wrapper.vm.$nextTick().then(() => { {
expect(wrapper.find('input[name="created_after"]').attributes().value).toEqual(DATE); startDate,
expect(wrapper.find('input[name="created_before"]').attributes().value).toEqual(DATE); endDate,
}); },
}); ]);
it('should submit the form on input change', () => {
wrapper = createComponent();
const spy = jest.spyOn(wrapper.props().formElement, 'submit');
wrapper
.find(GlDaterangePicker)
.vm.$emit('input', { startDate: parsePikadayDate(DATE), endDate: parsePikadayDate(DATE) });
return wrapper.vm.$nextTick().then(() => {
expect(spy).toHaveBeenCalledTimes(1);
}); });
}); });
}); });
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlNewDropdownItem } from '@gitlab/ui'; import { GlNewDropdownItem } from '@gitlab/ui';
import * as urlUtils from '~/lib/utils/url_utility';
import SortingField from 'ee/audit_events/components/sorting_field.vue'; import SortingField from 'ee/audit_events/components/sorting_field.vue';
describe('SortingField component', () => { describe('SortingField component', () => {
let wrapper; let wrapper;
const DUMMY_URL = 'https://localhost'; const initComponent = (props = {}) => {
const createComponent = () => wrapper = shallowMount(SortingField, {
shallowMount(SortingField, { stubs: { GlNewDropdown: true, GlNewDropdownItem: true } }); propsData: { ...props },
stubs: {
GlNewDropdown: true,
GlNewDropdownItem: true,
},
});
};
const getCheckedOptions = () => const getCheckedOptions = () =>
wrapper.findAll(GlNewDropdownItem).filter(item => item.props().isChecked); wrapper.findAll(GlNewDropdownItem).filter(item => item.props().isChecked);
const getCheckedOptionHref = () => {
return getCheckedOptions()
.at(0)
.attributes().href;
};
beforeEach(() => { beforeEach(() => {
urlUtils.setUrlParams = jest.fn(({ sort }) => `${DUMMY_URL}/?sort=${sort}`); initComponent();
wrapper = createComponent();
}); });
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe('Sorting behaviour', () => { describe('when initialized', () => {
it('should have sorting options', () => { it('should have sorting options', () => {
expect(wrapper.findAll(GlNewDropdownItem)).toHaveLength(2); expect(wrapper.findAll(GlNewDropdownItem)).toHaveLength(2);
}); });
it('should set the sorting option to `created_desc` by default', () => { it('should set the sorting option to `created_desc` by default', () => {
expect(getCheckedOptions()).toHaveLength(1); expect(getCheckedOptions()).toHaveLength(1);
expect(getCheckedOptionHref()).toBe(`${DUMMY_URL}/?sort=created_desc`);
}); });
it('should get the sorting option from the URL', () => { describe('with a sortBy value', () => {
urlUtils.queryToObject = jest.fn(() => ({ sort: 'created_asc' })); beforeEach(() => {
wrapper = createComponent(); initComponent({
sortBy: 'created_asc',
});
});
expect(getCheckedOptions()).toHaveLength(1); it('should set the sorting option accordingly', () => {
expect(getCheckedOptionHref()).toBe(`${DUMMY_URL}/?sort=created_asc`); expect(getCheckedOptions()).toHaveLength(1);
expect(
getCheckedOptions()
.at(0)
.text(),
).toEqual('Oldest created');
});
}); });
});
it('should retain other params when creating the option URL', () => { describe('when the user clicks on a option', () => {
urlUtils.setUrlParams = jest.fn(({ sort }) => `${DUMMY_URL}/?abc=defg&sort=${sort}`); beforeEach(() => {
urlUtils.queryToObject = jest.fn(() => ({ sort: 'created_desc', abc: 'defg' })); initComponent();
wrapper
wrapper = createComponent(); .findAll(GlNewDropdownItem)
.at(1)
.vm.$emit('click');
});
expect(getCheckedOptionHref()).toBe(`${DUMMY_URL}/?abc=defg&sort=created_desc`); it('should emit the "selected" event with clicked option', () => {
expect(wrapper.emitted().selected).toBeTruthy();
expect(wrapper.emitted().selected[0]).toEqual(['created_asc']);
}); });
}); });
}); });
...@@ -18,10 +18,9 @@ describe('Audit Event actions', () => { ...@@ -18,10 +18,9 @@ describe('Audit Event actions', () => {
}); });
it.each` it.each`
action | type | payload action | type | payload
${'setDateRange'} | ${types.SET_DATE_RANGE} | ${{ startDate, endDate }} ${'setDateRange'} | ${types.SET_DATE_RANGE} | ${{ startDate, endDate }}
${'setFilterValue'} | ${types.SET_FILTER_VALUE} | ${{ id: '1', type: 'user' }} ${'setSortBy'} | ${types.SET_SORT_BY} | ${'created_asc'}
${'setSortBy'} | ${types.SET_SORT_BY} | ${'created_asc'}
`( `(
'$action should commit $type with $payload and dispatches "searchForAuditEvents"', '$action should commit $type with $payload and dispatches "searchForAuditEvents"',
({ action, type, payload }) => { ({ action, type, payload }) => {
...@@ -40,6 +39,11 @@ describe('Audit Event actions', () => { ...@@ -40,6 +39,11 @@ describe('Audit Event actions', () => {
}, },
); );
it('setFilterValue action should commit to the store', () => {
const payload = [{ type: 'User', value: { data: 1, operator: '=' } }];
testAction(actions.setFilterValue, payload, state, [{ type: types.SET_FILTER_VALUE, payload }]);
});
describe('searchForAuditEvents', () => { describe('searchForAuditEvents', () => {
let spy; let spy;
......
...@@ -15,10 +15,10 @@ describe('Audit Event mutations', () => { ...@@ -15,10 +15,10 @@ describe('Audit Event mutations', () => {
}); });
it.each` it.each`
mutation | payload | expectedState mutation | payload | expectedState
${types.SET_FILTER_VALUE} | ${{ id: '1', type: 'user' }} | ${{ filterValue: { id: '1', type: 'user' } }} ${types.SET_FILTER_VALUE} | ${[{ type: 'User', value: { data: 1, operator: '=' } }]} | ${{ filterValue: [{ type: 'User', value: { data: 1, operator: '=' } }] }}
${types.SET_DATE_RANGE} | ${{ startDate, endDate }} | ${{ startDate, endDate }} ${types.SET_DATE_RANGE} | ${{ startDate, endDate }} | ${{ startDate, endDate }}
${types.SET_SORT_BY} | ${'created_asc'} | ${{ sortBy: 'created_asc' }} ${types.SET_SORT_BY} | ${'created_asc'} | ${{ sortBy: 'created_asc' }}
`( `(
'$mutation with payload $payload will update state with $expectedState', '$mutation with payload $payload will update state with $expectedState',
({ mutation, payload, expectedState }) => { ({ mutation, payload, expectedState }) => {
...@@ -32,7 +32,7 @@ describe('Audit Event mutations', () => { ...@@ -32,7 +32,7 @@ describe('Audit Event mutations', () => {
describe(`${types.INITIALIZE_AUDIT_EVENTS}`, () => { describe(`${types.INITIALIZE_AUDIT_EVENTS}`, () => {
const payload = { const payload = {
entity_id: '1', entity_id: '1',
entity_type: 'user', entity_type: 'User',
created_after: startDate, created_after: startDate,
created_before: endDate, created_before: endDate,
sort: 'created_asc', sort: 'created_asc',
...@@ -40,7 +40,7 @@ describe('Audit Event mutations', () => { ...@@ -40,7 +40,7 @@ describe('Audit Event mutations', () => {
it.each` it.each`
stateKey | expectedState stateKey | expectedState
${'filterValue'} | ${{ id: payload.entity_id, type: payload.entity_type }} ${'filterValue'} | ${[{ type: payload.entity_type, value: { data: payload.entity_id, operator: '=' } }]}
${'startDate'} | ${payload.created_after} ${'startDate'} | ${payload.created_after}
${'endDate'} | ${payload.created_before} ${'endDate'} | ${payload.created_before}
${'sortBy'} | ${payload.sort} ${'sortBy'} | ${payload.sort}
......
...@@ -8,6 +8,7 @@ describe('Audit Event Utils', () => { ...@@ -8,6 +8,7 @@ describe('Audit Event Utils', () => {
created_before: '2020-04-13', created_before: '2020-04-13',
sortBy: 'created_asc', sortBy: 'created_asc',
}; };
expect(parseAuditEventSearchQuery(input)).toEqual({ expect(parseAuditEventSearchQuery(input)).toEqual({
created_after: new Date('2020-03-13'), created_after: new Date('2020-03-13'),
created_before: new Date('2020-04-13'), created_before: new Date('2020-04-13'),
...@@ -19,20 +20,19 @@ describe('Audit Event Utils', () => { ...@@ -19,20 +20,19 @@ describe('Audit Event Utils', () => {
describe('createAuditEventSearchQuery', () => { describe('createAuditEventSearchQuery', () => {
it('returns a query object with remapped keys and stringified dates', () => { it('returns a query object with remapped keys and stringified dates', () => {
const input = { const input = {
filterValue: { filterValue: [{ type: 'User', value: { data: '1', operator: '=' } }],
id: '1',
type: 'user',
},
startDate: new Date('2020-03-13'), startDate: new Date('2020-03-13'),
endDate: new Date('2020-04-13'), endDate: new Date('2020-04-13'),
sortBy: 'bar', sortBy: 'bar',
}; };
expect(createAuditEventSearchQuery(input)).toEqual({ expect(createAuditEventSearchQuery(input)).toEqual({
entity_id: '1', entity_id: '1',
entity_type: 'user', entity_type: 'User',
created_after: '2020-03-13', created_after: '2020-03-13',
created_before: '2020-04-13', created_before: '2020-04-13',
sort: 'bar', sort: 'bar',
page: null,
}); });
}); });
}); });
......
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