Commit c17a69af authored by Illya Klymov's avatar Illya Klymov

Merge branch...

Merge branch '217782-refactor-the-project-audit-events-to-use-the-audit-events-shared-template' into 'master'

Refactor the project audit events to use the shared template

See merge request gitlab-org/gitlab!34176
parents 2118ea43 5278aae4
<script>
import { GlPagination, GlTable } from '@gitlab/ui';
import { s__ } from '~/locale';
import { getParameterValues, setUrlParams } from '~/lib/utils/url_utility';
import UrlTableCell from './url_table_cell.vue';
import UrlTableCell from './table_cells/url_table_cell.vue';
import HtmlTableCell from './table_cells/html_table_cell.vue';
const TABLE_HEADER_CLASSES = 'bg-transparent border-bottom p-3';
export default {
components: {
HtmlTableCell,
GlTable,
GlPagination,
UrlTableCell,
......@@ -94,6 +98,9 @@ export default {
<template #cell(object)="{ value: { url, name } }">
<url-table-cell :url="url" :name="name" />
</template>
<template #cell(action)="{ value }">
<html-table-cell :html="value" />
</template>
</gl-table>
<gl-pagination
v-if="displayPagination"
......
<script>
import sanitize from 'sanitize-html';
const ALLOWED_TAGS = ['strong'];
export default {
props: {
html: {
type: String,
required: true,
},
},
computed: {
sanitizedHtml() {
return sanitize(this.html, { allowedTags: ALLOWED_TAGS });
},
},
};
</script>
<template>
<span v-html="sanitizedHtml"></span>
</template>
......@@ -13,7 +13,17 @@ export default {
return Api.user(id).then(res => res.data);
},
fetchSuggestions(term) {
return Api.groupMembers(this.config.groupId, { search: term }).then(res => res.data);
const { groupId, projectPath } = this.config;
if (groupId) {
return Api.groupMembers(groupId, { search: term }).then(res => res.data);
}
if (projectPath) {
return Api.projectUsers(projectPath, term);
}
return {};
},
getItemName({ name }) {
return name;
......
import { __, s__ } from '~/locale';
import UserToken from './components/tokens/user_token.vue';
import GroupMemberToken from './components/tokens/group_member_token.vue';
import MemberToken from './components/tokens/member_token.vue';
import ProjectToken from './components/tokens/project_token.vue';
import GroupToken from './components/tokens/group_token.vue';
......@@ -33,9 +33,9 @@ export const AUDIT_FILTER_CONFIGS = [
...DEFAULT_TOKEN_OPTIONS,
icon: 'user',
title: s__('AuditLogs|Member Events'),
type: 'group_member',
type: 'member',
entityType: ENTITY_TYPES.AUTHOR,
token: GroupMemberToken,
token: MemberToken,
},
{
...DEFAULT_TOKEN_OPTIONS,
......
import initAuditEvents from 'ee/audit_events/init_audit_events';
initAuditEvents('#js-project-audit-events-app');
......@@ -13,13 +13,17 @@ class Projects::AuditEventsController < Projects::ApplicationController
def index
level = Gitlab::Audit::Levels::Project.new(project: project)
# This is an interim change until we have proper API support within Audit Events
audit_params = transform_author_entity_type(audit_logs_params)
events = AuditLogFinder
.new(level: level, params: audit_logs_params)
.new(level: level, params: audit_params)
.execute
.page(params[:page])
.without_count
@events = Gitlab::Audit::Events::Preloader.preload!(events)
@table_events = AuditEventSerializer.new.represent(@events)
end
private
......@@ -27,4 +31,12 @@ class Projects::AuditEventsController < Projects::ApplicationController
def check_audit_events_available!
render_404 unless @project.feature_available?(:audit_events) || LicenseHelper.show_promotions?(current_user)
end
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
......@@ -5,7 +5,7 @@ module AuditEventsHelper
user: :user,
group: :group,
project: :project,
group_member: :group_member
member: :member
}.freeze
def admin_audit_event_tokens
......@@ -13,7 +13,11 @@ module AuditEventsHelper
end
def group_audit_event_tokens(group_id)
[{ type: FILTER_TOKEN_TYPES[:group_member], group_id: group_id }]
[{ type: FILTER_TOKEN_TYPES[:member], group_id: group_id }]
end
def project_audit_event_tokens(project_path)
[{ type: FILTER_TOKEN_TYPES[:member], project_path: project_path }]
end
def human_text(details)
......
......@@ -7,7 +7,12 @@
%p.light
= _('Events in %{project_path}') % { project_path: @project.full_path }
= render 'shared/audit_events/event_filter', path: project_audit_events_path(@project)
= render 'shared/audit_events/event_table', events: @events
#js-project-audit-events-app{ data: { form_path: project_audit_events_path(@project),
events: @table_events.to_json,
is_last_page: @events.last_page?.to_json,
filter_qa_selector: 'project_audit_log_filter',
table_qa_selector: 'project_audit_log_table',
filter_token_options: project_audit_event_tokens(@project.full_path).to_json } }
- elsif show_promotions?
= render 'shared/promotions/promote_audit_events'
%form.row-content-block.second-block.d-flex.justify-content-lg-end.pb-0
.audit-controls.d-flex.align-items-lg-center.flex-column.flex-lg-row.col-lg-auto.px-0
.form-group.align-items-lg-center.mr-lg-2.d-flex.flex-column.flex-lg-row
= label_tag :created_after, _('From'), class: 'label-bold pr-1 mb-lg-0 mr-lg-1'
= date_field_tag :created_after, params[:created_after],
class: 'form-control', placeholder: 'YYYY-MM-DD'
.form-group.align-items-lg-center.mr-lg-2.d-flex.flex-column.flex-lg-row
= label_tag :created_before, _('To'), class: 'label-bold pr-1 mb-lg-0 mr-lg-1'
= date_field_tag :created_before, params[:created_before],
class: 'form-control', placeholder: 'YYYY-MM-DD'
= submit_tag _('Search'), class: 'btn mr-lg-2 form-group'
= render 'shared/audit_events/event_sort'
- toggle_value = params[:sort].presence || sort_value_recently_created
- toggle_text = audit_logs_sort_order_hash[toggle_value]
.btn-group.w-100.flex-column.flex-lg-row.form-group{ role: "group" }
.btn-group.w-100.dropdown
%button.btn.btn-default.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } }
= toggle_text
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
%li.dropdown-header
= _("Sort by")
- audit_logs_sort_order_hash.each do |value, title|
%li
= link_to title, filter_audit_path(sort: value), class: ("is-active" if toggle_text == title)
- if defined?(events)
.audit-table#audits
.gl-responsive-table-row.table-row-header{ role: 'row' }
.table-section.section-15{ role: 'rowheader' }
= _('Author')
.table-section.section-50{ role: 'rowheader' }
= s_('AuditEvents|Action')
.table-section.section-15{ role: 'rowheader' }
= s_('AuditEvents|Target')
.table-section.section-20{ role: 'rowheader' }
= s_('AuditEvents|At')
- events.map(&:present).each do |event|
.gl-responsive-table-row
.table-section.section-15
.table-mobile-header{ role: 'rowheader' }
= _('Author')
.table-mobile-content
- if event.author_name
%a{ href: event.author_url }= event.author_name
- else
= s_('AuditEvents|(removed)')
.table-section.section-50.section-wrap.audit-action.js-audit-action
%span= sanitize(human_text(event.details), tags: %w(strong))
.table-section.section-15.section-wrap
= event.target
.table-section.section-20
= event.date
= paginate_without_count events
---
title: Update project audit events to use the new searchable table
merge_request: 34176
author:
type: changed
......@@ -9,9 +9,11 @@ RSpec.describe Projects::AuditEventsController do
describe 'GET #index' do
let(:sort) { nil }
let(:entity_type) { nil }
let(:entity_id) { nil }
let(:request) do
get :index, params: { project_id: project.to_param, namespace_id: project.namespace.to_param, sort: sort }
get :index, params: { project_id: project.to_param, namespace_id: project.namespace.to_param, sort: sort, entity_type: entity_type, entity_id: entity_id }
end
context 'authorized' do
......@@ -22,7 +24,7 @@ RSpec.describe Projects::AuditEventsController do
context 'when audit_events feature is available' do
let(:level) { Gitlab::Audit::Levels::Project.new(project: project) }
let(:audit_logs_params) { ActionController::Parameters.new(sort: '').permit! }
let(:audit_logs_params) { ActionController::Parameters.new(sort: '', entity_type: '', entity_id: '').permit! }
before do
stub_licensed_features(audit_events: true)
......@@ -31,6 +33,16 @@ RSpec.describe Projects::AuditEventsController do
allow(AuditLogFinder).to receive(:new).and_call_original
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
request
......@@ -38,12 +50,22 @@ RSpec.describe Projects::AuditEventsController do
expect(response).to render_template(:index)
end
it 'invokes AuditLogFinder with correct arguments' do
request
context 'invokes AuditLogFinder with correct arguments' do
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(
level: level, params: audit_logs_params
)
context 'when the author entity type is specified' do
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
context 'ordering' do
......@@ -87,6 +109,14 @@ RSpec.describe Projects::AuditEventsController do
end
end
context 'pagination' do
it 'paginates audit events, without casting a count query' do
request
expect(assigns(:events)).to be_kind_of(Kaminari::PaginatableWithoutCount)
end
end
context 'when audit_events feature is not available' do
before do
stub_licensed_features(audit_events: false)
......
......@@ -106,32 +106,10 @@ RSpec.describe 'Admin::AuditLogs', :js do
let_it_be(:audit_event_1) { create(:user_audit_event, created_at: 5.days.ago) }
let_it_be(:audit_event_2) { create(:user_audit_event, created_at: 3.days.ago) }
let_it_be(:audit_event_3) { create(:user_audit_event, created_at: 1.day.ago) }
let_it_be(:events_path) { :admin_audit_logs_path }
let_it_be(:entity) { nil }
it 'shows only 2 days old events' do
visit admin_audit_logs_path(created_after: 4.days.ago.to_date, created_before: 2.days.ago.to_date)
find('[data-testid="audit-events-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
it 'shows only yesterday events' do
visit admin_audit_logs_path(created_after: 2.days.ago.to_date)
find('[data-testid="audit-events-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 admin_audit_logs_path(created_after: '12-345-6789')
expect(page).to have_content('Invalid date format. Please use UTC format as YYYY-MM-DD')
end
it_behaves_like 'audit events date filter'
end
describe 'impersonated events' do
......
......@@ -68,31 +68,9 @@ RSpec.describe 'Groups > Audit Events', :js do
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_3) { create(:group_audit_event, entity_type: 'Group', entity_id: group.id, created_at: 1.day.ago) }
let!(:events_path) { :group_audit_events_path }
let!(:entity) { group }
it 'shows only 2 days old events' do
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
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
it_behaves_like 'audit events date filter'
end
end
......@@ -86,7 +86,7 @@ RSpec.describe 'Projects > Audit Events', :js do
visit project_audit_events_path(project)
expect(page).to have_content('Add deploy key')
expect(page).to have_content('Added deploy key')
visit project_deploy_keys_path(project)
......@@ -97,7 +97,7 @@ RSpec.describe 'Projects > Audit Events', :js do
visit project_audit_events_path(project)
wait_for('Audit event background creation job is done', polling_interval: 0.5, reload: true) do
page.has_content?('Remove deploy key', wait: 0)
page.has_content?('Removed deploy key', wait: 0)
end
end
end
......@@ -121,8 +121,8 @@ RSpec.describe 'Projects > Audit Events', :js do
click_link 'Audit Events'
page.within('#audits') do
expect(page).to have_content 'Change access level from developer to maintainer'
page.within('.audit-log-table') do
expect(page).to have_content 'Changed access level from Developer to Maintainer'
expect(page).to have_content(project.owner.name)
expect(page).to have_content('Pete')
end
......@@ -154,28 +154,22 @@ RSpec.describe 'Projects > Audit Events', :js do
wait_for_all_requests
page.within('#audits') do
page.within('.audit-log-table') do
expect(page).to have_content(project.owner.name)
expect(page).to have_content('Change prevent merge request approval from authors')
expect(page).to have_content('Change prevent merge request approval from reviewers')
expect(page).to have_content('Changed prevent merge request approval from authors')
expect(page).to have_content('Changed prevent merge request approval from reviewers')
expect(page).to have_content(project.name)
end
end
end
it_behaves_like 'audit event contains custom message' do
let(:audit_events_url) { project_audit_events_path(project) }
end
describe 'filter by date', js: false do
describe 'filter by date' do
let!(:audit_event_1) { create(:project_audit_event, entity_type: 'Project', entity_id: project.id, created_at: 5.days.ago) }
let!(:audit_event_2) { create(:project_audit_event, entity_type: 'Project', entity_id: project.id, created_at: 3.days.ago) }
let!(:audit_event_3) { create(:project_audit_event, entity_type: 'Project', entity_id: project.id, created_at: 1.day.ago) }
let!(:events_path) { :project_audit_events_path }
let!(:entity) { project }
before do
visit project_audit_events_path(project)
end
it_behaves_like 'audit events filter'
it_behaves_like 'audit events date filter'
end
end
......@@ -26,11 +26,11 @@ describe('AuditEventsFilter', () => {
});
describe.each`
type | title
${'project'} | ${'Project Events'}
${'group'} | ${'Group Events'}
${'user'} | ${'User Events'}
${'group_member'} | ${'Member Events'}
type | title
${'project'} | ${'Project Events'}
${'group'} | ${'Group Events'}
${'user'} | ${'User Events'}
${'member'} | ${'Member Events'}
`('for the list of available tokens', ({ type, title }) => {
it(`creates a unique token for ${type}`, () => {
initComponent();
......
import { shallowMount } from '@vue/test-utils';
import HtmlTableCell from 'ee/audit_events/components/table_cells/html_table_cell.vue';
describe('HtmlTableCell component', () => {
it('should show <strong> tags if present in the HTML provided', () => {
const html = '<strong>Test HTML</strong>';
const wrapper = shallowMount(HtmlTableCell, {
propsData: { html },
});
expect(wrapper.html()).toBe(`<span>${html}</span>`);
});
it('should not include tags that are not in the allowed list but should keep the content', () => {
const html = '<a href="https://magic.url/">Link</a> <i>Test</i> <h1>HTML</h1>';
const wrapper = shallowMount(HtmlTableCell, {
propsData: { html },
});
expect(wrapper.html()).toBe('<span>Link Test HTML</span>');
});
});
import { shallowMount } from '@vue/test-utils';
import UrlTableCell from 'ee/audit_events/components/url_table_cell.vue';
import UrlTableCell from 'ee/audit_events/components/table_cells/url_table_cell.vue';
describe('UrlTableCell component', () => {
it('should show the link if the URL is provided', () => {
......
......@@ -47,7 +47,7 @@ describe('Audit Event Utils', () => {
describe('getEntityTypeFromType', () => {
it('returns the correct entity type when given a valid type', () => {
expect(getEntityTypeFromType('group_member')).toEqual('Author');
expect(getEntityTypeFromType('member')).toEqual('Author');
});
it('returns `undefined` when given an invalid type', () => {
......
......@@ -16,10 +16,20 @@ RSpec.describe AuditEventsHelper do
let(:group_id) { 1 }
it 'returns the available tokens' do
available_tokens = [{ type: AuditEventsHelper::FILTER_TOKEN_TYPES[:group_member], group_id: group_id }]
available_tokens = [{ type: AuditEventsHelper::FILTER_TOKEN_TYPES[:member], group_id: group_id }]
expect(group_audit_event_tokens(group_id)).to eq(available_tokens)
end
end
describe '#project_audit_event_tokens' do
let(:project_path) { '/abc' }
it 'returns the available tokens' do
available_tokens = [{ type: AuditEventsHelper::FILTER_TOKEN_TYPES[:member], project_path: project_path }]
expect(project_audit_event_tokens(project_path)).to eq(available_tokens)
end
end
describe '#human_text' do
let(:target_type) { 'User' }
let(:details) do
......
# frozen_string_literal: true
RSpec.shared_examples 'audit event contains custom message' do
let(:custom_message) { "Message_with_spaces" }
let(:details) do
{
custom_message: custom_message,
author_name: 'John Doe',
target_id: 1,
target_type: 'User',
target_details: 'Michael'
}
end
let!(:security_event) do
::AuditEventService.new(user, project, details).security_event
end
before do
visit audit_events_url
end
it 'user sees this message' do
expect(page).to have_content('Message_with_spaces')
end
context 'when it contains tags' do
let(:custom_message) { 'Message <strong>with</strong> <i>deleted</i> tags' }
it 'allows only <strong> tag' do
message_row = find('.js-audit-action', text: 'Message with deleted tags')
expect(message_row).to have_selector('strong')
expect(message_row).to have_no_selector('i')
end
end
end
# frozen_string_literal: true
RSpec.shared_examples_for 'audit events filter' do
RSpec.shared_examples_for 'audit events date filter' do
it 'shows only 2 days old events' do
page.within '.content' do
fill_in 'From', with: 4.days.ago
fill_in 'To', with: 2.days.ago
click_button 'Search'
end
visit method(events_path).call(entity, created_after: 4.days.ago.to_date, created_before: 2.days.ago.to_date)
find('.audit-log-table td', match: :first)
expect(page).to have_content(audit_event_2.present.date)
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
it 'shows only yesterday events' do
page.within '.content' do
fill_in 'From', with: 2.days.ago
click_button 'Search'
end
visit method(events_path).call(entity, created_after: 2.days.ago.to_date)
find('.audit-log-table td', match: :first)
expect(page).to have_content(audit_event_3.present.date)
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
page.within '.content' do
fill_in 'From', with: '12-345-6789'
click_button 'Search'
end
visit method(events_path).call(entity, created_after: '12-345-6789')
expect(page).to have_content('Invalid date format. Please use UTC format as YYYY-MM-DD')
end
......
......@@ -3240,18 +3240,6 @@ msgstr ""
msgid "Audit Log"
msgstr ""
msgid "AuditEvents|(removed)"
msgstr ""
msgid "AuditEvents|Action"
msgstr ""
msgid "AuditEvents|At"
msgstr ""
msgid "AuditEvents|Target"
msgstr ""
msgid "AuditLogs|(removed)"
msgstr ""
......
......@@ -32,7 +32,7 @@ module QA
project.initialize_with_readme = true
end.visit!
end
it_behaves_like 'audit event', ["Add project"]
it_behaves_like 'audit event', ["Added project"]
end
context "Add user access as guest" do
......@@ -45,7 +45,7 @@ module QA
end
end
it_behaves_like 'audit event', ["Add user access as guest"]
it_behaves_like 'audit event', ["Added user access as Guest"]
end
context "Add deploy key" do
......@@ -61,7 +61,7 @@ module QA
end
end
it_behaves_like 'audit event', ["Add deploy key"]
it_behaves_like 'audit event', ["Added deploy key"]
end
context "Change visibility" do
......@@ -77,7 +77,7 @@ module QA
end
end
it_behaves_like 'audit event', ["Change visibility from public to private"]
it_behaves_like 'audit event', ["Changed visibility from Public to Private"]
end
context "Export file download", quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217949', type: :investigating } do
......
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