diff --git a/src/actions/audit-log-actions.js b/src/actions/audit-log-actions.js index 034956b8d..9a3923b2c 100644 --- a/src/actions/audit-log-actions.js +++ b/src/actions/audit-log-actions.js @@ -19,26 +19,22 @@ import { authErrorHandler, escapeFilterValue } from 'openstack-uicore-foundation/lib/utils/actions'; -import {getAccessTokenSafely} from '../utils/methods'; +import {getAccessTokenSafely,isNumericString, parseDateRangeFilter} from '../utils/methods'; export const CLEAR_LOG_PARAMS = 'CLEAR_LOG_PARAMS'; export const REQUEST_LOG = 'REQUEST_LOG'; export const RECEIVE_LOG = 'RECEIVE_LOG'; -export const getAuditLog = (entityFilter = [], term = null, page = 1, perPage = 100, order = null, orderDir = 1) => async (dispatch, getState) => { +export const getAuditLog = (entityFilter = [], term = null, page = 1, perPage = 100, order = null, orderDir = 1, filters = {}) => async (dispatch, getState) => { const {currentSummitState} = getState(); const accessToken = await getAccessTokenSafely(); const {currentSummit} = currentSummitState; - const filter = [`summit_id==${currentSummit.id}`]; + const summitTZ = currentSummit.time_zone.name; + const summitFilter = [`summit_id==${currentSummit.id}`]; dispatch(startLoading()); - if (term) { - const escapedTerm = escapeFilterValue(term); - filter.push(`user_email=@${escapedTerm},user_full_name=@${escapedTerm},action=@${escapedTerm}`); - } - const params = { page: page, per_page: perPage, @@ -46,7 +42,9 @@ export const getAuditLog = (entityFilter = [], term = null, page = 1, perPage = access_token: accessToken, }; - params['filter[]'] = [...filter, ...entityFilter]; + const parsedFilters = [...summitFilter, ...entityFilter, ...parseFilters(filters, term)]; + + params['filter[]'] = parsedFilters; // order if (order != null && orderDir != null) { @@ -59,13 +57,42 @@ export const getAuditLog = (entityFilter = [], term = null, page = 1, perPage = createAction(RECEIVE_LOG), `${window.API_BASE_URL}/api/v1/audit-logs`, authErrorHandler, - {page, perPage, order, orderDir, term} + {page, perPage, order, orderDir, term, summitTZ, filters} )(params)(dispatch).then(() => { dispatch(stopLoading()); } ); }; +const parseFilters = (filters, term = null) => { + const filter = []; + + if (filters.created_date_filter) { + parseDateRangeFilter(filter, filters.created_date_filter, 'created'); + } + + if (filters.hasOwnProperty('user_id_filter') && Array.isArray(filters.user_id_filter) + && filters.user_id_filter.length > 0) { + filter.push(`user_id==${filters.user_id_filter.map(t => t.id).join('||')}`); + } + + if (term) { + const escapedTerm = escapeFilterValue(term); + let searchString = ''; + + if (isNumericString(term)) { + searchString += `entity_id==${term}`; + } else { + searchString += `action=@${escapedTerm}` + } + + filter.push(searchString); + } + + + return filter; +} + export const clearAuditLogParams = () => async (dispatch, getState) => { dispatch(createAction(CLEAR_LOG_PARAMS)()); }; \ No newline at end of file diff --git a/src/components/audit-logs/index.js b/src/components/audit-logs/index.js index 4e6a81196..a1f8987b8 100644 --- a/src/components/audit-logs/index.js +++ b/src/components/audit-logs/index.js @@ -1,11 +1,26 @@ -import React, {useEffect} from "react"; -import {FreeTextSearch, Table} from "openstack-uicore-foundation/lib/components"; +import React, { useEffect, useState } from "react"; +import { FreeTextSearch, Table, Dropdown, MemberInput, DateTimePicker } from "openstack-uicore-foundation/lib/components"; import T from "i18n-react"; -import {Pagination} from "react-bootstrap"; -import {connect} from "react-redux"; -import {clearAuditLogParams, getAuditLog} from "../../actions/audit-log-actions"; +import { epochToMomentTimeZone } from 'openstack-uicore-foundation/lib/utils/methods' +import { Pagination } from "react-bootstrap"; +import { connect } from "react-redux"; +import { clearAuditLogParams, getAuditLog } from "../../actions/audit-log-actions"; + +const AuditLogs = ({ entityFilter = [], term, logEntries, perPage, lastPage, currentPage, order, orderDir, columns, getAuditLog, clearAuditLogParams, filters }) => { + + const defaultFilters = { + user_id_filter: [], + created_date_filter: Array(2).fill(null) + } + + const [enabledFilters, setEnabledFilters] = useState(Object.keys(filters).filter(e => Array.isArray(filters[e]) ? filters[e]?.some(e => e !== null) : filters[e]?.length > 0)); + const [auditLogFilters, setAuditLogFilters] = useState({ ...defaultFilters, ...filters }); + + const filters_ddl = [ + { label: 'Created', value: 'created_date_filter' }, + { label: 'Member', value: 'user_id_filter' }, + ] -const AuditLogs = ({entityFilter = [], term, logEntries, perPage, lastPage, currentPage, order, orderDir, columns, getAuditLog, clearAuditLogParams}) => { const audit_log_table_options = { sortCol: order, sortDir: orderDir, @@ -22,19 +37,57 @@ const AuditLogs = ({entityFilter = [], term, logEntries, perPage, lastPage, curr const show_columns = columns ? audit_log_columns.filter(c => columns.includes(c.columnKey)) : audit_log_columns; const handleSort = (index, key, dir, func) => { - getAuditLog(entityFilter, term, currentPage, perPage, key, dir); + getAuditLog(entityFilter, term, currentPage, perPage, key, dir, auditLogFilters); }; const handlePageChange = (page) => { - getAuditLog(entityFilter, term, page, perPage, order, orderDir); + getAuditLog(entityFilter, term, page, perPage, order, orderDir, auditLogFilters); }; const handleSearch = (newTerm) => { - getAuditLog(entityFilter, newTerm, currentPage, perPage, order, orderDir); + getAuditLog(entityFilter, newTerm, currentPage, perPage, order, orderDir, auditLogFilters); }; + const handleDDLSortByLabel = (ddlArray) => { + return ddlArray.sort((a, b) => a.label.localeCompare(b.label)); + } + + const handleFiltersChange = (ev) => { + const { value } = ev.target; + if (value.length < enabledFilters.length) { + if (value.length === 0) { + setEnabledFilters(value); + setAuditLogFilters(defaultFilters); + } else { + const removedFilter = enabledFilters.filter(e => !value.includes(e))[0]; + const defaultValue = Array.isArray(auditLogFilters[removedFilter]) ? [] : ''; + let newEventFilters = { ...auditLogFilters, [removedFilter]: defaultValue }; + setEnabledFilters(value); + setAuditLogFilters(newEventFilters); + } + } else { + setEnabledFilters(value); + } + } + + const handleChangeDateFilter = (ev, lastDate) => { + const { value, id } = ev.target; + const newDateFilter = auditLogFilters[id]; + + setAuditLogFilters({ ...auditLogFilters, [id]: lastDate ? [newDateFilter[0], value.unix()] : [value.unix(), newDateFilter[1]] }) + } + + const handleAuditLogFilterChange = (ev) => { + let { value, type, id } = ev.target; + setAuditLogFilters({ ...auditLogFilters, [id]: value }); + } + + const handleApplyAuditLogFilters = () => { + getAuditLog(entityFilter, term, currentPage, perPage, order, orderDir, auditLogFilters); + } + useEffect(() => { - getAuditLog(entityFilter, term, 1, perPage, order, orderDir); + getAuditLog(entityFilter, term, 1, perPage, order, orderDir, filters); return () => { clearAuditLogParams(); @@ -54,6 +107,73 @@ const AuditLogs = ({entityFilter = [], term, logEntries, perPage, lastPage, curr +
+ +
+
+ +
+
+ +
+
+
+ {enabledFilters.includes('user_id_filter') && +
+ { + return member.hasOwnProperty("email") ? + `${member.first_name} ${member.last_name} (${member.email})` : + `${member.first_name} ${member.last_name} (${member.id})`; + } + } + placeholder={T.translate("audit_log.placeholders.user_id")} + value={auditLogFilters.user_id_filter} + isMulti + isClearable + onChange={handleAuditLogFilterChange} + /> +
} + {enabledFilters.includes('created_date_filter') && + <> +
+ handleChangeDateFilter(ev, false)} + timezone={'UTC'} + value={epochToMomentTimeZone(auditLogFilters.created_date_filter[0], 'UTC')} + className={'event-list-date-picker'} + /> +
+
+ handleChangeDateFilter(ev, true)} + timezone={'UTC'} + value={epochToMomentTimeZone(auditLogFilters.created_date_filter[1], 'UTC')} + className={'event-list-date-picker'} + /> +
+ + } +
+ {logEntries.length === 0 &&
{T.translate("audit_log.no_log_entries")}
} @@ -85,7 +205,7 @@ const AuditLogs = ({entityFilter = [], term, logEntries, perPage, lastPage, curr ) } -const mapStateToProps = ({auditLogState}) => ({ +const mapStateToProps = ({ auditLogState }) => ({ ...auditLogState, }); diff --git a/src/i18n/en.json b/src/i18n/en.json index 159f92e33..40b898dce 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -2825,12 +2825,16 @@ "sponsorship_type": "Tier", "no_log_entries": "No log entries found for current search criteria.", "action": "Action", - "date": "Date", + "date": "Created", "user": "User", "event": "Event", "order": "Order", + "apply_filters": "Apply Filters", "placeholders": { - "search_log": "Search By Action / User Full Name" + "search_log": "Search By Action / User Full Name", + "user_id": "Filter by User", + "created_date_from": "Filter by Created Date From", + "created_date_to": "Filter by Created Date To" } }, "signage": { diff --git a/src/reducers/audit_log/audit-log-reducer.js b/src/reducers/audit_log/audit-log-reducer.js index 1550cdf45..32bb82d74 100644 --- a/src/reducers/audit_log/audit-log-reducer.js +++ b/src/reducers/audit_log/audit-log-reducer.js @@ -25,13 +25,16 @@ import { LOGOUT_USER } from 'openstack-uicore-foundation/lib/security/actions'; import { formatAuditLog, parseSpeakerAuditLog } from '../../utils/methods'; const DEFAULT_STATE = { + term : '', logEntries : [], currentPage : 1, lastPage : 1, perPage : 10, order : 'created', orderDir : 1, - totalLogEntries : 0 + totalLogEntries : 0, + summitTZ : '', + filters : {} }; const auditLogReducer = (state = DEFAULT_STATE, action) => { @@ -43,20 +46,19 @@ const auditLogReducer = (state = DEFAULT_STATE, action) => { return DEFAULT_STATE; } case REQUEST_LOG: { - let {order, orderDir} = payload; - return {...state, order, orderDir} + let {term, order, orderDir, summitTZ} = payload; + return {...state, term, order, orderDir, summitTZ} } case RECEIVE_LOG: { let { current_page, total, last_page } = payload.response; let logEntries = payload.response.data.map(e => { const logEntryAction = e.action.startsWith('Speaker') ? parseSpeakerAuditLog(e.action) : e.action - const userTimeZone = moment.tz.guess(); return { ...e, event: e.event_id, user: `${e.user.first_name} ${e.user.last_name} (${e.user.id})`, - created: moment(epochToMomentTimeZone(e.created, userTimeZone)).format('MMMM Do YYYY, h:mm a'), + created: moment(epochToMomentTimeZone(e.created, state.summitTZ)).format('MMMM Do YYYY, h:mm a'), action: formatAuditLog(logEntryAction) }; });