Commit fed9026b authored by Illya Klymov's avatar Illya Klymov

Merge branch...

Merge branch '217781-refactor-the-group-audit-events-to-use-the-audit-events-shared-template' into 'master'

Refactor group audit events to use the audit events shared template

See merge request gitlab-org/gitlab!33305
parents 22e6de6e 0d4d35bb
...@@ -16,6 +16,7 @@ class AuditEvent < ApplicationRecord ...@@ -16,6 +16,7 @@ class AuditEvent < ApplicationRecord
scope :by_entity_type, -> (entity_type) { where(entity_type: entity_type) } scope :by_entity_type, -> (entity_type) { where(entity_type: entity_type) }
scope :by_entity_id, -> (entity_id) { where(entity_id: entity_id) } scope :by_entity_id, -> (entity_id) { where(entity_id: entity_id) }
scope :by_author_id, -> (author_id) { where(author_id: author_id) }
after_initialize :initialize_details after_initialize :initialize_details
......
...@@ -23,7 +23,7 @@ export default { ...@@ -23,7 +23,7 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
enabledTokenTypes: { filterTokenOptions: {
type: Array, type: Array,
required: true, required: true,
}, },
...@@ -53,7 +53,7 @@ export default { ...@@ -53,7 +53,7 @@ export default {
<div class="d-flex justify-content-between audit-controls row"> <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="col-lg-auto flex-fill form-group align-items-lg-center pr-lg-8">
<audit-events-filter <audit-events-filter
:enabled-token-types="enabledTokenTypes" :filter-token-options="filterTokenOptions"
:qa-selector="filterQaSelector" :qa-selector="filterQaSelector"
:value="filterValue" :value="filterValue"
@selected="setFilterValue" @selected="setFilterValue"
......
<script> <script>
import { GlFilteredSearch } from '@gitlab/ui'; import { GlFilteredSearch } from '@gitlab/ui';
import { FILTER_TOKENS, AVAILABLE_TOKEN_TYPES } from '../constants'; import { AUDIT_FILTER_CONFIGS } from '../constants';
import { availableTokensValidator } from '../validators'; import { filterTokenOptionsValidator } from '../validators';
export default { export default {
components: { components: {
...@@ -13,11 +13,11 @@ export default { ...@@ -13,11 +13,11 @@ export default {
required: false, required: false,
default: () => [], default: () => [],
}, },
enabledTokenTypes: { filterTokenOptions: {
type: Array, type: Array,
required: false, required: false,
default: () => AVAILABLE_TOKEN_TYPES, default: () => AUDIT_FILTER_CONFIGS,
validator: availableTokensValidator, validator: filterTokenOptionsValidator,
}, },
qaSelector: { qaSelector: {
type: String, type: String,
...@@ -25,25 +25,30 @@ export default { ...@@ -25,25 +25,30 @@ export default {
default: undefined, default: undefined,
}, },
}, },
data() {
return {
filterTokens: this.filterTokenOptions.map(option => ({
...AUDIT_FILTER_CONFIGS.find(({ type }) => type === option.type),
...option,
})),
};
},
computed: { computed: {
searchTerm() { tokenSearchTerm() {
return this.value.find(term => AVAILABLE_TOKEN_TYPES.includes(term.type)); return this.value.find(term => this.filterTokens.find(token => token.type === term.type));
}, },
enabledTokens() { enabledTokens() {
return FILTER_TOKENS.filter(token => this.enabledTokenTypes.includes(token.type)); const { tokenSearchTerm } = this;
},
filterTokens() {
// This limits the user to search by only one of the available tokens
const { enabledTokens, searchTerm } = this;
if (searchTerm?.type) { // If a user has searched for a term within a token, limit the user to that one token
return enabledTokens.map(token => ({ if (tokenSearchTerm) {
return this.filterTokens.map(token => ({
...token, ...token,
disabled: searchTerm.type !== token.type, disabled: tokenSearchTerm.type !== token.type,
})); }));
} }
return enabledTokens; return this.filterTokens;
}, },
}, },
methods: { methods: {
...@@ -68,7 +73,7 @@ export default { ...@@ -68,7 +73,7 @@ export default {
: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="enabledTokens"
class="gl-h-32 w-100" class="gl-h-32 w-100"
@submit="onSubmit" @submit="onSubmit"
@input="onInput" @input="onInput"
......
<script>
import Api from '~/api';
import AuditFilterToken from './shared/audit_filter_token.vue';
export default {
components: {
AuditFilterToken,
},
inheritAttrs: false,
tokenMethods: {
fetchItem(id) {
return Api.user(id).then(res => res.data);
},
fetchSuggestions(term) {
return Api.groupMembers(this.config.groupId, { search: term }).then(res => res.data);
},
getItemName({ name }) {
return name;
},
},
};
</script>
<template>
<audit-filter-token v-bind="{ ...this.$attrs, ...this.$options.tokenMethods }" v-on="$listeners">
<template #suggestion="{item: user}">
<p class="m-0">{{ user.name }}</p>
<p class="m-0">@{{ user.username }}</p>
</template>
</audit-filter-token>
</template>
...@@ -64,7 +64,10 @@ export default { ...@@ -64,7 +64,10 @@ export default {
return this.suggestions.length > 0; return this.suggestions.length > 0;
}, },
lowerCaseType() { lowerCaseType() {
return this.config.type.toLowerCase(); return this.config.type
.replace('_', ' ')
.trim()
.toLowerCase();
}, },
noSuggestionsString() { noSuggestionsString() {
return sprintf(s__('AuditLogs|No matching %{type} found.'), { type: this.lowerCaseType }); return sprintf(s__('AuditLogs|No matching %{type} found.'), { type: this.lowerCaseType });
......
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import UserToken from './components/tokens/user_token.vue'; import UserToken from './components/tokens/user_token.vue';
import GroupMemberToken from './components/tokens/group_member_token.vue';
import ProjectToken from './components/tokens/project_token.vue'; import ProjectToken from './components/tokens/project_token.vue';
import GroupToken from './components/tokens/group_token.vue'; import GroupToken from './components/tokens/group_token.vue';
...@@ -8,28 +10,49 @@ const DEFAULT_TOKEN_OPTIONS = { ...@@ -8,28 +10,49 @@ const DEFAULT_TOKEN_OPTIONS = {
unique: true, unique: true,
}; };
export const FILTER_TOKENS = [ // Due to the i18n eslint rule we can't have a capitalized string even if it is a case-aware URL param
/* eslint-disable @gitlab/require-i18n-strings */
const ENTITY_TYPES = {
USER: 'User',
AUTHOR: 'Author',
GROUP: 'Group',
PROJECT: 'Project',
};
/* eslint-enable @gitlab/require-i18n-strings */
export const AUDIT_FILTER_CONFIGS = [
{ {
...DEFAULT_TOKEN_OPTIONS, ...DEFAULT_TOKEN_OPTIONS,
icon: 'user', icon: 'user',
title: s__('AuditLogs|User Events'), title: s__('AuditLogs|User Events'),
type: 'User', type: 'user',
entityType: ENTITY_TYPES.USER,
token: UserToken, token: UserToken,
}, },
{
...DEFAULT_TOKEN_OPTIONS,
icon: 'user',
title: s__('AuditLogs|Member Events'),
type: 'group_member',
entityType: ENTITY_TYPES.AUTHOR,
token: GroupMemberToken,
},
{ {
...DEFAULT_TOKEN_OPTIONS, ...DEFAULT_TOKEN_OPTIONS,
icon: 'bookmark', icon: 'bookmark',
title: s__('AuditLogs|Project Events'), title: s__('AuditLogs|Project Events'),
type: 'Project', type: 'project',
entityType: ENTITY_TYPES.PROJECT,
token: ProjectToken, token: ProjectToken,
}, },
{ {
...DEFAULT_TOKEN_OPTIONS, ...DEFAULT_TOKEN_OPTIONS,
icon: 'group', icon: 'group',
title: s__('AuditLogs|Group Events'), title: s__('AuditLogs|Group Events'),
type: 'Group', type: 'group',
entityType: ENTITY_TYPES.GROUP,
token: GroupToken, token: GroupToken,
}, },
]; ];
export const AVAILABLE_TOKEN_TYPES = FILTER_TOKENS.map(token => token.type); export const AVAILABLE_TOKEN_TYPES = AUDIT_FILTER_CONFIGS.map(token => token.type);
import Vue from 'vue'; import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import { convertObjectPropsToCamelCase, 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'; import createStore from './store';
export default selector => { export default selector => {
const el = document.querySelector(selector); const el = document.querySelector(selector);
const { events, isLastPage, enabledTokenTypes, filterQaSelector, tableQaSelector } = el.dataset; const { events, isLastPage, filterTokenOptions, filterQaSelector, tableQaSelector } = el.dataset;
const store = createStore(); const store = createStore();
store.dispatch('initializeAuditEvents'); store.dispatch('initializeAuditEvents');
...@@ -18,7 +20,9 @@ export default selector => { ...@@ -18,7 +20,9 @@ export default selector => {
props: { props: {
events: JSON.parse(events), events: JSON.parse(events),
isLastPage: parseBoolean(isLastPage), isLastPage: parseBoolean(isLastPage),
enabledTokenTypes: JSON.parse(enabledTokenTypes), filterTokenOptions: JSON.parse(filterTokenOptions).map(filterTokenOption =>
convertObjectPropsToCamelCase(filterTokenOption),
),
filterQaSelector, filterQaSelector,
tableQaSelector, tableQaSelector,
}, },
......
import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility'; import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility';
import { AVAILABLE_TOKEN_TYPES } from './constants'; import { AVAILABLE_TOKEN_TYPES, AUDIT_FILTER_CONFIGS } from './constants';
export const isNumeric = str => { export const isNumeric = str => {
return !Number.isNaN(parseInt(str, 10), 10); return !Number.isNaN(parseInt(str, 10));
};
export const getTypeFromEntityType = entityType => {
return AUDIT_FILTER_CONFIGS.find(
({ entityType: configEntityType }) => configEntityType === entityType,
)?.type;
};
export const getEntityTypeFromType = type => {
return AUDIT_FILTER_CONFIGS.find(({ type: configType }) => configType === type)?.entityType;
}; };
export const parseAuditEventSearchQuery = ({ export const parseAuditEventSearchQuery = ({
created_after: createdAfter, created_after: createdAfter,
created_before: createdBefore, created_before: createdBefore,
entity_type: entityType,
...restOfParams ...restOfParams
}) => ({ }) => ({
...restOfParams, ...restOfParams,
created_after: createdAfter ? parsePikadayDate(createdAfter) : null, created_after: createdAfter ? parsePikadayDate(createdAfter) : null,
created_before: createdBefore ? parsePikadayDate(createdBefore) : null, created_before: createdBefore ? parsePikadayDate(createdBefore) : null,
entity_type: getTypeFromEntityType(entityType),
}); });
export const createAuditEventSearchQuery = ({ filterValue, startDate, endDate, sortBy }) => { export const createAuditEventSearchQuery = ({ filterValue, startDate, endDate, sortBy }) => {
...@@ -23,7 +35,7 @@ export const createAuditEventSearchQuery = ({ filterValue, startDate, endDate, s ...@@ -23,7 +35,7 @@ export const createAuditEventSearchQuery = ({ filterValue, startDate, endDate, s
created_before: endDate ? pikadayToString(endDate) : null, created_before: endDate ? pikadayToString(endDate) : null,
sort: sortBy, sort: sortBy,
entity_id: entityValue?.value.data, entity_id: entityValue?.value.data,
entity_type: entityValue?.type, entity_type: getEntityTypeFromType(entityValue?.type),
// When changing the search parameters, we should be resetting to the first page // When changing the search parameters, we should be resetting to the first page
page: null, page: null,
}; };
......
import { AVAILABLE_TOKEN_TYPES } from './constants'; import { AVAILABLE_TOKEN_TYPES } from './constants';
export function availableTokensValidator(value) { export function filterTokenOptionsValidator(filterTokenOptions) {
return value.every(type => AVAILABLE_TOKEN_TYPES.includes(type)); return filterTokenOptions
.map(({ type }) => type)
.every(type => AVAILABLE_TOKEN_TYPES.includes(type));
} }
export default {}; export default {};
import initAuditEvents from 'ee/audit_events/init_audit_events';
initAuditEvents('#js-group-audit-events-app');
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module AuditEvents module AuditEvents
module AuditLogsParams module AuditLogsParams
def audit_logs_params def audit_logs_params
params.permit(:entity_type, :entity_id, :created_before, :created_after, :sort) params.permit(:entity_type, :entity_id, :created_before, :created_after, :sort, :author_id)
end end
end end
end end
...@@ -12,13 +12,26 @@ class Groups::AuditEventsController < Groups::ApplicationController ...@@ -12,13 +12,26 @@ class Groups::AuditEventsController < Groups::ApplicationController
def index def index
level = Gitlab::Audit::Levels::Group.new(group: group) level = Gitlab::Audit::Levels::Group.new(group: group)
# This is an interim change until we have proper API support within Audit Events
params = transform_author_entity_type(audit_logs_params)
events = AuditLogFinder events = AuditLogFinder
.new(level: level, params: audit_logs_params) .new(level: level, params: params)
.execute .execute
.page(params[:page]) .page(params[:page])
.without_count .without_count
@events = Gitlab::Audit::Events::Preloader.preload!(events) @events = Gitlab::Audit::Events::Preloader.preload!(events)
@table_events = AuditEventSerializer.new.represent(@events)
end
private
def transform_author_entity_type(params)
return params unless params[:entity_type] == 'Author'
params[:author_id] = params[:entity_id]
params.except(:entity_type, :entity_id)
end end
end end
...@@ -31,6 +31,7 @@ class AuditLogFinder ...@@ -31,6 +31,7 @@ class AuditLogFinder
audit_events = init_collection audit_events = init_collection
audit_events = by_entity(audit_events) audit_events = by_entity(audit_events)
audit_events = by_created_at(audit_events) audit_events = by_created_at(audit_events)
audit_events = by_author(audit_events)
sort(audit_events) sort(audit_events)
end end
...@@ -61,6 +62,12 @@ class AuditLogFinder ...@@ -61,6 +62,12 @@ class AuditLogFinder
audit_events audit_events
end end
def by_author(audit_events)
return audit_events unless valid_author_id?
audit_events.by_author_id(params[:author_id])
end
def sort(audit_events) def sort(audit_events)
audit_events.order_by(params[:sort]) audit_events.order_by(params[:sort])
end end
...@@ -72,4 +79,8 @@ class AuditLogFinder ...@@ -72,4 +79,8 @@ class AuditLogFinder
def valid_entity_id? def valid_entity_id?
params[:entity_id].to_i.nonzero? params[:entity_id].to_i.nonzero?
end end
def valid_author_id?
params[:author_id].to_i.nonzero?
end
end end
# frozen_string_literal: true # frozen_string_literal: true
module AuditEventsHelper module AuditEventsHelper
FILTER_TOKEN_TYPES = {
user: :user,
group: :group,
project: :project,
group_member: :group_member
}.freeze
def admin_audit_event_tokens
[{ type: FILTER_TOKEN_TYPES[:user] }, { type: FILTER_TOKEN_TYPES[:group] }, { type: FILTER_TOKEN_TYPES[:project] }].freeze
end
def group_audit_event_tokens(group_id)
[{ type: FILTER_TOKEN_TYPES[:group_member], group_id: group_id }]
end
def human_text(details) def human_text(details)
return details[:custom_message] if details[:custom_message] return details[:custom_message] if details[:custom_message]
......
# frozen_string_literal: true
module AuditLogsHelper
def admin_audit_log_token_types
%w[User Group Project].freeze
end
end
...@@ -5,4 +5,4 @@ ...@@ -5,4 +5,4 @@
is_last_page: @events.last_page?.to_json, is_last_page: @events.last_page?.to_json,
filter_qa_selector: 'admin_audit_log_filter', filter_qa_selector: 'admin_audit_log_filter',
table_qa_selector: 'admin_audit_log_table', table_qa_selector: 'admin_audit_log_table',
enabled_token_types: admin_audit_log_token_types } } filter_token_options: admin_audit_event_tokens.to_json } }
...@@ -3,5 +3,9 @@ ...@@ -3,5 +3,9 @@
%h3.page-title= _('Group Audit Events') %h3.page-title= _('Group Audit Events')
%p.light= _('Events in %{group_name}') % { group_name: @group.name } %p.light= _('Events in %{group_name}') % { group_name: @group.name }
= render 'shared/audit_events/event_filter', path: group_audit_events_path(@group) #js-group-audit-events-app{ data: { form_path: group_audit_events_path(@group),
= render 'shared/audit_events/event_table', events: @events events: @table_events.to_json,
is_last_page: @events.last_page?.to_json,
filter_qa_selector: 'group_audit_log_filter',
table_qa_selector: 'group_audit_log_table',
filter_token_options: group_audit_event_tokens(@group.id).to_json } }
---
title: Update group audit events to use the new searchable table
merge_request: 33305
author:
type: changed
...@@ -9,9 +9,11 @@ RSpec.describe Groups::AuditEventsController do ...@@ -9,9 +9,11 @@ RSpec.describe Groups::AuditEventsController do
describe 'GET #index' do describe 'GET #index' do
let(:sort) { nil } let(:sort) { nil }
let(:entity_type) { nil }
let(:entity_id) { nil }
let(:request) do let(:request) do
get :index, params: { group_id: group.to_param, sort: sort } get :index, params: { group_id: group.to_param, sort: sort, entity_type: entity_type, entity_id: entity_id }
end end
context 'authorized' do context 'authorized' do
...@@ -22,7 +24,7 @@ RSpec.describe Groups::AuditEventsController do ...@@ -22,7 +24,7 @@ RSpec.describe Groups::AuditEventsController do
context 'when audit_events feature is available' do context 'when audit_events feature is available' do
let(:level) { Gitlab::Audit::Levels::Group.new(group: group) } let(:level) { Gitlab::Audit::Levels::Group.new(group: group) }
let(:audit_logs_params) { ActionController::Parameters.new(sort: '').permit! } let(:audit_logs_params) { ActionController::Parameters.new(sort: '', entity_type: '', entity_id: '').permit! }
before do before do
stub_licensed_features(audit_events: true) stub_licensed_features(audit_events: true)
...@@ -31,6 +33,16 @@ RSpec.describe Groups::AuditEventsController do ...@@ -31,6 +33,16 @@ RSpec.describe Groups::AuditEventsController do
allow(AuditLogFinder).to receive(:new).and_call_original allow(AuditLogFinder).to receive(:new).and_call_original
end end
shared_examples 'AuditLogFinder params' do
it 'has the correct params' do
request
expect(AuditLogFinder).to have_received(:new).with(
level: level, params: audit_logs_params
)
end
end
it 'renders index with 200 status code' do it 'renders index with 200 status code' do
request request
...@@ -38,12 +50,22 @@ RSpec.describe Groups::AuditEventsController do ...@@ -38,12 +50,22 @@ RSpec.describe Groups::AuditEventsController do
expect(response).to render_template(:index) expect(response).to render_template(:index)
end end
it 'invokes AuditLogFinder with correct arguments' do context 'invokes AuditLogFinder with correct arguments' do
request it_behaves_like 'AuditLogFinder params'
end
context 'author' do
context 'when no author entity type is specified' do
it_behaves_like 'AuditLogFinder params'
end
expect(AuditLogFinder).to have_received(:new).with( context 'when the author entity type is specified' do
level: level, params: audit_logs_params let(:entity_type) { 'Author' }
) let(:entity_id) { 1 }
let(:audit_logs_params) { ActionController::Parameters.new(sort: '', author_id: '1').permit! }
it_behaves_like 'AuditLogFinder params'
end
end end
context 'ordering' do context 'ordering' do
......
...@@ -56,23 +56,43 @@ RSpec.describe 'Groups > Audit Events', :js do ...@@ -56,23 +56,43 @@ RSpec.describe 'Groups > Audit Events', :js do
click_link 'Audit Events' click_link 'Audit Events'
page.within('#audits') do page.within('.audit-log-table') do
expect(page).to have_content 'Change access level from developer to maintainer' expect(page).to have_content 'Changed access level from Developer to Maintainer'
expect(page).to have_content(user.name) expect(page).to have_content(user.name)
expect(page).to have_content('Alex') expect(page).to have_content('Alex')
end end
end end
end end
describe 'filter by date', js: false do describe 'filter by date' do
let!(:audit_event_1) { create(:group_audit_event, entity_type: 'Group', entity_id: group.id, created_at: 5.days.ago) } let!(:audit_event_1) { create(:group_audit_event, entity_type: 'Group', entity_id: group.id, created_at: 5.days.ago) }
let!(:audit_event_2) { create(:group_audit_event, entity_type: 'Group', entity_id: group.id, created_at: 3.days.ago) } let!(:audit_event_2) { create(:group_audit_event, entity_type: 'Group', entity_id: group.id, created_at: 3.days.ago) }
let!(:audit_event_3) { create(:group_audit_event, entity_type: 'Group', entity_id: group.id, created_at: 1.day.ago) } let!(:audit_event_3) { create(:group_audit_event, entity_type: 'Group', entity_id: group.id, created_at: 1.day.ago) }
before do it 'shows only 2 days old events' do
visit group_audit_events_path(group) visit group_audit_events_path(group, created_after: 4.days.ago.to_date, created_before: 2.days.ago.to_date)
find('.audit-log-table td', match: :first)
expect(page).not_to have_content(audit_event_1.present.date)
expect(page).to have_content(audit_event_2.present.date)
expect(page).not_to have_content(audit_event_3.present.date)
end end
it_behaves_like 'audit events filter' it 'shows only yesterday events' do
visit group_audit_events_path(group, created_after: 2.days.ago.to_date)
find('.audit-log-table td', match: :first)
expect(page).not_to have_content(audit_event_1.present.date)
expect(page).not_to have_content(audit_event_2.present.date)
expect(page).to have_content(audit_event_3.present.date)
end
it 'shows a message if provided date is invalid' do
visit group_audit_events_path(group, created_after: '12-345-6789')
expect(page).to have_content('Invalid date format. Please use UTC format as YYYY-MM-DD')
end
end end
end end
...@@ -3,15 +3,16 @@ ...@@ -3,15 +3,16 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe AuditLogFinder do RSpec.describe AuditLogFinder do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) } let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:project) { create(:project, namespace: group) } let_it_be(:project) { create(:project, namespace: group) }
let_it_be(:subproject) { create(:project, namespace: subgroup) } let_it_be(:subproject) { create(:project, namespace: subgroup) }
let_it_be(:user_audit_event) { create(:user_audit_event, created_at: 3.days.ago) } let_it_be(:user_audit_event) { create(:user_audit_event, created_at: 3.days.ago) }
let_it_be(:project_audit_event) { create(:project_audit_event, entity_id: project.id, created_at: 2.days.ago) } let_it_be(:project_audit_event) { create(:project_audit_event, entity_id: project.id, author_id: user.id, created_at: 2.days.ago) }
let_it_be(:subproject_audit_event) { create(:project_audit_event, entity_id: subproject.id, created_at: 2.days.ago) } let_it_be(:subproject_audit_event) { create(:project_audit_event, entity_id: subproject.id, created_at: 2.days.ago) }
let_it_be(:group_audit_event) { create(:group_audit_event, entity_id: group.id, created_at: 1.day.ago) } let_it_be(:group_audit_event) { create(:group_audit_event, entity_id: group.id, author_id: user.id, created_at: 1.day.ago) }
let(:level) { Gitlab::Audit::Levels::Instance.new } let(:level) { Gitlab::Audit::Levels::Instance.new }
let(:params) { {} } let(:params) { {} }
...@@ -180,6 +181,59 @@ RSpec.describe AuditLogFinder do ...@@ -180,6 +181,59 @@ RSpec.describe AuditLogFinder do
end end
end end
context 'filtering by author_id' do
context 'no author_id provided' do
let(:params) { { entity_type: 'Author' } }
it_behaves_like 'no filtering'
end
context 'invalid author_id' do
let(:params) { { author_id: '0' } }
it 'ignores author_id and returns all events irrespective of entity_type' do
expect(subject.count).to eq(4)
end
end
shared_examples 'finds the right event' do
it 'finds the right event' do
expect(subject.count).to eq(1)
entity = subject.first
expect(entity.entity_type).to eq(entity_type)
expect(entity.id).to eq(audit_event.id)
expect(entity.author_id).to eq(audit_event.author_id)
end
end
context 'Group Event' do
let(:level) { Gitlab::Audit::Levels::Group.new(group: group) }
let(:params) { { author_id: group_audit_event.author_id } }
before do
# Only looking for group event, with this on it tests Group and Project events
stub_feature_flags(audit_log_group_level: false)
end
it_behaves_like 'finds the right event' do
let(:entity_type) { 'Group' }
let(:audit_event) { group_audit_event }
end
end
context 'Project Event' do
let(:level) { Gitlab::Audit::Levels::Project.new(project: project) }
let(:params) { { author_id: project_audit_event.author_id } }
it_behaves_like 'finds the right event' do
let(:entity_type) { 'Project' }
let(:audit_event) { project_audit_event }
end
end
end
context 'filtering by created_at' do context 'filtering by created_at' do
context 'through created_after' do context 'through created_after' do
let(:params) { { created_after: group_audit_event.created_at } } let(:params) { { created_after: group_audit_event.created_at } }
......
...@@ -17,7 +17,7 @@ exports[`AuditEventsApp when initialized matches the snapshot 1`] = ` ...@@ -17,7 +17,7 @@ exports[`AuditEventsApp when initialized matches the snapshot 1`] = `
data-testid="audit-events-filter" data-testid="audit-events-filter"
> >
<gl-filtered-search-stub <gl-filtered-search-stub
availabletokens="[object Object],[object Object],[object Object]" availabletokens="[object Object],[object Object],[object Object],[object Object]"
class="gl-h-32 w-100" class="gl-h-32 w-100"
clearbuttontitle="Clear" clearbuttontitle="Clear"
close-button-title="Close" close-button-title="Close"
......
...@@ -18,7 +18,7 @@ describe('AuditEventsApp', () => { ...@@ -18,7 +18,7 @@ describe('AuditEventsApp', () => {
let store; let store;
const events = [{ foo: 'bar' }]; const events = [{ foo: 'bar' }];
const enabledTokenTypes = AVAILABLE_TOKEN_TYPES; const filterTokenOptions = AVAILABLE_TOKEN_TYPES.map(type => ({ type }));
const filterQaSelector = 'filter_qa_selector'; const filterQaSelector = 'filter_qa_selector';
const tableQaSelector = 'table_qa_selector'; const tableQaSelector = 'table_qa_selector';
...@@ -29,7 +29,7 @@ describe('AuditEventsApp', () => { ...@@ -29,7 +29,7 @@ describe('AuditEventsApp', () => {
isLastPage: true, isLastPage: true,
filterQaSelector, filterQaSelector,
tableQaSelector, tableQaSelector,
enabledTokenTypes, filterTokenOptions,
events, events,
...props, ...props,
}, },
...@@ -74,7 +74,7 @@ describe('AuditEventsApp', () => { ...@@ -74,7 +74,7 @@ describe('AuditEventsApp', () => {
it('renders audit events filter', () => { it('renders audit events filter', () => {
expect(wrapper.find(AuditEventsFilter).props()).toEqual({ expect(wrapper.find(AuditEventsFilter).props()).toEqual({
enabledTokenTypes, filterTokenOptions,
qaSelector: filterQaSelector, qaSelector: filterQaSelector,
value: TEST_FILTER_VALUE, value: TEST_FILTER_VALUE,
}); });
......
...@@ -7,11 +7,10 @@ import { AVAILABLE_TOKEN_TYPES } from 'ee/audit_events/constants'; ...@@ -7,11 +7,10 @@ import { AVAILABLE_TOKEN_TYPES } from 'ee/audit_events/constants';
describe('AuditEventsFilter', () => { describe('AuditEventsFilter', () => {
let wrapper; let wrapper;
const value = [{ type: 'Project', value: { data: 1, operator: '=' } }]; 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 => getAvailableTokens().find(token => token.type === type);
getAvailableTokens().filter(token => token.type === type)[0];
const initComponent = (props = {}) => { const initComponent = (props = {}) => {
wrapper = shallowMount(AuditEventsFilter, { wrapper = shallowMount(AuditEventsFilter, {
...@@ -27,10 +26,11 @@ describe('AuditEventsFilter', () => { ...@@ -27,10 +26,11 @@ describe('AuditEventsFilter', () => {
}); });
describe.each` describe.each`
type | title type | title
${'Project'} | ${'Project Events'} ${'project'} | ${'Project Events'}
${'Group'} | ${'Group Events'} ${'group'} | ${'Group Events'}
${'User'} | ${'User Events'} ${'user'} | ${'User Events'}
${'group_member'} | ${'Member Events'}
`('for the list of available tokens', ({ type, title }) => { `('for the list of available tokens', ({ type, title }) => {
it(`creates a unique token for ${type}`, () => { it(`creates a unique token for ${type}`, () => {
initComponent(); initComponent();
...@@ -52,9 +52,9 @@ describe('AuditEventsFilter', () => { ...@@ -52,9 +52,9 @@ describe('AuditEventsFilter', () => {
}); });
it('only one token matching the selected token type is enabled', () => { it('only one token matching the selected token type is enabled', () => {
expect(getAvailableTokenProps('Project').disabled).toEqual(false); expect(getAvailableTokenProps('project').disabled).toEqual(false);
expect(getAvailableTokenProps('Group').disabled).toEqual(true); expect(getAvailableTokenProps('group').disabled).toEqual(true);
expect(getAvailableTokenProps('User').disabled).toEqual(true); expect(getAvailableTokenProps('user').disabled).toEqual(true);
}); });
describe('and the user submits the search field', () => { describe('and the user submits the search field', () => {
...@@ -103,11 +103,11 @@ describe('AuditEventsFilter', () => { ...@@ -103,11 +103,11 @@ describe('AuditEventsFilter', () => {
beforeEach(() => { beforeEach(() => {
initComponent({ initComponent({
enabledTokenTypes: [type], filterTokenOptions: [{ type }],
}); });
}); });
it('only the enabled token type is available for selection', () => { it('only the enabled tokens type is available for selection', () => {
expect(getAvailableTokens().length).toEqual(1); expect(getAvailableTokens().length).toEqual(1);
expect(getAvailableTokens()).toMatchObject([{ type }]); expect(getAvailableTokens()).toMatchObject([{ type }]);
}); });
......
...@@ -33,7 +33,7 @@ describe('AuditFilterToken', () => { ...@@ -33,7 +33,7 @@ describe('AuditFilterToken', () => {
propsData: { propsData: {
value: {}, value: {},
config: { config: {
type: 'Foo', type: 'foo',
}, },
active: false, active: false,
...tokenMethods, ...tokenMethods,
...@@ -108,13 +108,14 @@ describe('AuditFilterToken', () => { ...@@ -108,13 +108,14 @@ describe('AuditFilterToken', () => {
describe('when fetching suggestions', () => { describe('when fetching suggestions', () => {
let resolveSuggestions; let resolveSuggestions;
let rejectSuggestions; let rejectSuggestions;
const fetchSuggestions = () =>
new Promise((resolve, reject) => {
resolveSuggestions = resolve;
rejectSuggestions = reject;
});
beforeEach(() => { beforeEach(() => {
const value = { data: '' }; const value = { data: '' };
const fetchSuggestions = () =>
new Promise((resolve, reject) => {
resolveSuggestions = resolve;
rejectSuggestions = reject;
});
initComponent({ value, fetchSuggestions }); initComponent({ value, fetchSuggestions });
}); });
...@@ -144,6 +145,19 @@ describe('AuditFilterToken', () => { ...@@ -144,6 +145,19 @@ describe('AuditFilterToken', () => {
); );
}); });
}); });
describe('and the fetch fails with a multi-word type', () => {
beforeEach(() => {
initComponent({ config: { type: 'foo_bar' }, fetchSuggestions });
rejectSuggestions({ response: { status: httpStatusCodes.NOT_FOUND } });
});
it('shows a flash error message', () => {
expect(createFlash).toHaveBeenCalledWith(
'Failed to find foo bar. Please search for another foo bar.',
);
});
});
}); });
describe('when fetching the view item', () => { describe('when fetching the view item', () => {
......
...@@ -115,7 +115,7 @@ describe('Audit Event actions', () => { ...@@ -115,7 +115,7 @@ describe('Audit Event actions', () => {
created_after: new Date('2020-06-05T00:00:00.000Z'), created_after: new Date('2020-06-05T00:00:00.000Z'),
created_before: new Date('2020-06-25T00:00:00.000Z'), created_before: new Date('2020-06-25T00:00:00.000Z'),
entity_id: '44', entity_id: '44',
entity_type: 'User', entity_type: 'user',
sort: 'created_desc', sort: 'created_desc',
}, },
}, },
......
import { parseAuditEventSearchQuery, createAuditEventSearchQuery } from 'ee/audit_events/utils'; import {
isNumeric,
getTypeFromEntityType,
getEntityTypeFromType,
parseAuditEventSearchQuery,
createAuditEventSearchQuery,
} from 'ee/audit_events/utils';
describe('Audit Event Utils', () => { describe('Audit Event Utils', () => {
describe('isNumeric', () => {
describe.each`
value
${false}
${true}
${undefined}
${null}
${'abcd'}
${''}
`('for a list of non-numeric values', ({ value }) => {
it(`returns false for ${value}`, () => {
expect(isNumeric(value)).toBe(false);
});
});
describe.each`
value
${0}
${12345}
${'0'}
${'6789'}
`('for a list of numeric values', ({ value }) => {
it(`returns true for ${value}`, () => {
expect(isNumeric(value)).toBe(true);
});
});
});
describe('getTypeFromEntityType', () => {
it('returns the correct type when given a valid entity type', () => {
expect(getTypeFromEntityType('User')).toEqual('user');
});
it('returns `undefined` when given an invalid entity type', () => {
expect(getTypeFromEntityType('ABCDEF')).toBeUndefined();
});
});
describe('getEntityTypeFromType', () => {
it('returns the correct entity type when given a valid type', () => {
expect(getEntityTypeFromType('group_member')).toEqual('Author');
});
it('returns `undefined` when given an invalid type', () => {
expect(getTypeFromEntityType('abcdef')).toBeUndefined();
});
});
describe('parseAuditEventSearchQuery', () => { describe('parseAuditEventSearchQuery', () => {
it('returns a query object with parsed date values', () => { it('returns a query object with parsed date values', () => {
const input = { const input = {
...@@ -20,7 +74,7 @@ describe('Audit Event Utils', () => { ...@@ -20,7 +74,7 @@ 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: [{ type: 'User', value: { data: '1', operator: '=' } }], filterValue: [{ type: 'user', value: { data: '1', operator: '=' } }],
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',
......
import { sample } from 'lodash'; import { sample } from 'lodash';
import { AVAILABLE_TOKEN_TYPES } from 'ee/audit_events/constants'; import { AVAILABLE_TOKEN_TYPES } from 'ee/audit_events/constants';
import { availableTokensValidator } from 'ee/audit_events/validators'; import { filterTokenOptionsValidator } from 'ee/audit_events/validators';
describe('availableTokensValidator', () => { describe('filterTokenOptionsValidator', () => {
it('returns true when the input contains an available token type', () => { it('returns true when the input contains a valid token type', () => {
const input = [sample(AVAILABLE_TOKEN_TYPES)]; const input = [{ type: sample(AVAILABLE_TOKEN_TYPES) }];
expect(availableTokensValidator(input)).toEqual(true); expect(filterTokenOptionsValidator(input)).toEqual(true);
}); });
it('returns false when the input contains an unavailable token type', () => {
const input = ['InvalidType']; it('returns false when the input contains an invalid token type', () => {
expect(availableTokensValidator(input)).toEqual(false); const input = [{ type: 'InvalidType' }];
expect(filterTokenOptionsValidator(input)).toEqual(false);
}); });
}); });
...@@ -3,6 +3,23 @@ ...@@ -3,6 +3,23 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe AuditEventsHelper do RSpec.describe AuditEventsHelper do
using RSpec::Parameterized::TableSyntax
describe '#admin_audit_event_tokens' do
it 'returns the available tokens' do
available_tokens = [{ type: AuditEventsHelper::FILTER_TOKEN_TYPES[:user] }, { type: AuditEventsHelper::FILTER_TOKEN_TYPES[:group] }, { type: AuditEventsHelper::FILTER_TOKEN_TYPES[:project] }]
expect(admin_audit_event_tokens).to eq(available_tokens)
end
end
describe '#group_audit_event_tokens' do
let(:group_id) { 1 }
it 'returns the available tokens' do
available_tokens = [{ type: AuditEventsHelper::FILTER_TOKEN_TYPES[:group_member], group_id: group_id }]
expect(group_audit_event_tokens(group_id)).to eq(available_tokens)
end
end
describe '#human_text' do describe '#human_text' do
let(:target_type) { 'User' } let(:target_type) { 'User' }
let(:details) do let(:details) do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe AuditLogsHelper do
using RSpec::Parameterized::TableSyntax
describe '#admin_audit_log_token_types' do
it 'returns the available tokens' do
available_tokens = %w[User Group Project]
expect(admin_audit_log_token_types).to eq(available_tokens)
end
end
end
...@@ -3106,6 +3106,9 @@ msgstr "" ...@@ -3106,6 +3106,9 @@ msgstr ""
msgid "AuditLogs|IP Address" msgid "AuditLogs|IP Address"
msgstr "" msgstr ""
msgid "AuditLogs|Member Events"
msgstr ""
msgid "AuditLogs|No matching %{type} found." msgid "AuditLogs|No matching %{type} found."
msgstr "" msgstr ""
......
...@@ -56,7 +56,7 @@ module QA ...@@ -56,7 +56,7 @@ module QA
Page::Group::Menu.perform(&:click_group_general_settings_item) Page::Group::Menu.perform(&:click_group_general_settings_item)
end end
it_behaves_like 'audit event', ['Add group'] do it_behaves_like 'audit event', ['Added group'] do
let(:group) do let(:group) do
Resource::Group.fabricate_via_api! do |group| Resource::Group.fabricate_via_api! do |group|
group.name = group_name group.name = group_name
...@@ -75,7 +75,7 @@ module QA ...@@ -75,7 +75,7 @@ module QA
settings.click_save_name_visibility_settings_button settings.click_save_name_visibility_settings_button
end end
end end
it_behaves_like 'audit event', ['Change repository size limit'] it_behaves_like 'audit event', ['Changed repository size limit']
end end
context 'Update group name' do context 'Update group name' do
...@@ -90,7 +90,7 @@ module QA ...@@ -90,7 +90,7 @@ module QA
end end
end end
it_behaves_like 'audit event', ['Change name'] it_behaves_like 'audit event', ['Changed name']
end end
context 'Add user, change access level, remove user' do context 'Add user, change access level, remove user' do
...@@ -105,7 +105,7 @@ module QA ...@@ -105,7 +105,7 @@ module QA
end end
end end
it_behaves_like 'audit event', ['Add user access as guest', 'Change access level', 'Remove user access'] it_behaves_like 'audit event', ['Added user access as Guest', 'Changed access level', 'Removed user access']
end end
context 'Add and remove project access' do context 'Add and remove project access' do
...@@ -126,7 +126,7 @@ module QA ...@@ -126,7 +126,7 @@ module QA
@group.visit! @group.visit!
end end
it_behaves_like 'audit event', ['Add project access', 'Remove project access'] it_behaves_like 'audit event', ['Added project access', 'Removed project access']
end end
end end
......
...@@ -31,7 +31,7 @@ module QA ...@@ -31,7 +31,7 @@ module QA
Page::Group::Settings::General.perform(&:set_lfs_enabled) Page::Group::Settings::General.perform(&:set_lfs_enabled)
end end
it_behaves_like 'audit event', ["Change lfs enabled from false to true", "Change lfs enabled from true to false"] it_behaves_like 'audit event', ["Changed lfs enabled from false to true", "Changed lfs enabled from true to false"]
end end
context 'Enable and disable LFS' do context 'Enable and disable LFS' do
...@@ -45,7 +45,7 @@ module QA ...@@ -45,7 +45,7 @@ module QA
Page::Group::Settings::General.perform(&:set_membership_lock_disabled) Page::Group::Settings::General.perform(&:set_membership_lock_disabled)
end end
it_behaves_like 'audit event', ["Change membership lock from true to false", "Change membership lock from false to true"] it_behaves_like 'audit event', ["Changed membership lock from true to false", "Changed membership lock from false to true"]
end end
context 'Enable and disable allow user request access' do context 'Enable and disable allow user request access' do
...@@ -59,7 +59,7 @@ module QA ...@@ -59,7 +59,7 @@ module QA
Page::Group::Settings::General.perform(&:toggle_request_access) Page::Group::Settings::General.perform(&:toggle_request_access)
end end
it_behaves_like 'audit event', ["Change request access enabled from true to false", "Change request access enabled from false to true"] it_behaves_like 'audit event', ["Changed request access enabled from true to false", "Changed request access enabled from false to true"]
end end
# Bug issue: https://gitlab.com/gitlab-org/gitlab/issues/31764 # Bug issue: https://gitlab.com/gitlab-org/gitlab/issues/31764
...@@ -76,7 +76,7 @@ module QA ...@@ -76,7 +76,7 @@ module QA
Page::Group::Settings::General.perform(&:set_require_2fa_disabled) Page::Group::Settings::General.perform(&:set_require_2fa_disabled)
end end
it_behaves_like 'audit event', ["Change require two factor authentication from true to false", "Change require two factor authentication from false to true"] it_behaves_like 'audit event', ["Changed require two factor authentication from true to false", "Changed require two factor authentication from false to true"]
end end
context 'Change project creation level' do context 'Change project creation level' do
...@@ -89,7 +89,7 @@ module QA ...@@ -89,7 +89,7 @@ module QA
end end
end end
it_behaves_like 'audit event', ["Change project creation level"] it_behaves_like 'audit event', ["Changed project creation level"]
end end
end end
......
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