Merge branch 'feature/moderation-log' into 'master'
Add moderation log * [x] filtering by specific admin/moderator * [x] filtering by date range * [x] searching by log message * [x] pagination See merge request pleroma/admin-fe!38
This commit is contained in:
commit
6290d06e20
8 changed files with 279 additions and 4 deletions
|
@ -6,10 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
|
||||
## Unreleased
|
||||
|
||||
## [1.2.0] - 2019-09-27
|
||||
|
||||
### Added
|
||||
|
||||
- Emoji pack configuration
|
||||
- Ability to require user's password reset
|
||||
– Ability to track admin/moderator actions, a.k.a. "the moderation log"
|
||||
|
||||
## [1.1.0] - 2019-09-15
|
||||
|
||||
|
|
15
README.md
15
README.md
|
@ -18,8 +18,19 @@ To compile everything for production run `yarn build:prod`.
|
|||
|
||||
#### Disabling features
|
||||
|
||||
You can disable certain AdminFE features, like reports or settings by modifying `config/prod.env.js` env variable `DISABLED_FEATURES`, e.g. if you want to compile AdminFE without "Settings" you'll need to set it to: `DISABLED_FEATURES: '["settings"]'`,
|
||||
to disable emoji pack settings add `"emoji-packs"` to the list.
|
||||
You can disable certain AdminFE features, like reports or settings by modifying `config/prod.env.js` env variable `DISABLED_FEATURES`, e.g. if you want to compile AdminFE without "Settings" you'll need to set it to: `DISABLED_FEATURES: '["settings"]'`.
|
||||
|
||||
Features, that can be disabled:
|
||||
|
||||
- reports: `DISABLED_FEATURES: '["reports"]'`
|
||||
- invites: `DISABLED_FEATURES: '["invites"]'`
|
||||
- moderation log: `DISABLED_FEATURES: '["moderationLog"]'`
|
||||
- settings: `DISABLED_FEATURES: '["settings"]'`
|
||||
- emoji packs: `DISABLED_FEATURES: '["emojiPacks"]'`
|
||||
|
||||
Of course, you can disable multiple features just by adding to the array, e.g. `DISABLED_FEATURES: '["emojiPacks", "settings"]'` will have both emoji packs and settings disabled.
|
||||
|
||||
Users administration cannot be disabled.
|
||||
|
||||
## Changelog
|
||||
|
||||
|
|
38
src/api/moderationLog.js
Normal file
38
src/api/moderationLog.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
import _ from 'lodash'
|
||||
|
||||
import request from '@/utils/request'
|
||||
import { getToken } from '@/utils/auth'
|
||||
import { baseName } from './utils'
|
||||
|
||||
export async function fetchLog(authHost, token, params, page = 1) {
|
||||
const normalizedParams = new URLSearchParams(
|
||||
_.omitBy({ ...params, page }, _.isUndefined)
|
||||
).toString()
|
||||
|
||||
return await request({
|
||||
baseURL: baseName(authHost),
|
||||
url: `/api/pleroma/admin/moderation_log?${normalizedParams}`,
|
||||
method: 'get',
|
||||
headers: authHeaders(token)
|
||||
})
|
||||
}
|
||||
|
||||
export async function fetchAdmins(authHost, token) {
|
||||
return await request({
|
||||
baseURL: baseName(authHost),
|
||||
url: `/api/pleroma/admin/users?filters=is_admin`,
|
||||
method: 'get',
|
||||
headers: authHeaders(token)
|
||||
})
|
||||
}
|
||||
|
||||
export async function fetchModerators(authHost, token) {
|
||||
return await request({
|
||||
baseURL: baseName(authHost),
|
||||
url: `/api/pleroma/admin/users?filters=is_moderator`,
|
||||
method: 'get',
|
||||
headers: authHeaders(token)
|
||||
})
|
||||
}
|
||||
|
||||
const authHeaders = (token) => token ? { 'Authorization': `Bearer ${getToken()}` } : {}
|
|
@ -67,6 +67,7 @@ export default {
|
|||
users: 'Users',
|
||||
reports: 'Reports',
|
||||
settings: 'Settings',
|
||||
moderationLog: 'Moderation Log',
|
||||
'emoji-packs': 'Emoji packs'
|
||||
},
|
||||
navbar: {
|
||||
|
@ -284,6 +285,9 @@ export default {
|
|||
closed: 'Closed',
|
||||
resolved: 'Resolved'
|
||||
},
|
||||
moderationLog: {
|
||||
moderationLog: 'Moderation Log'
|
||||
},
|
||||
settings: {
|
||||
settings: 'Settings',
|
||||
instance: 'Instance',
|
||||
|
|
|
@ -49,6 +49,20 @@ const invites = {
|
|||
]
|
||||
}
|
||||
|
||||
const moderationLogDisabled = disabledFeatures.includes('moderation-log')
|
||||
const moderationLog = {
|
||||
path: '/moderation_log',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
component: () => import('@/views/moderation_log/index'),
|
||||
name: 'Moderation Log',
|
||||
meta: { title: 'moderationLog', icon: 'list', noCache: true }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const emojiPacksDisabled = disabledFeatures.includes('emoji-packs')
|
||||
const emojiPacks = {
|
||||
path: '/emoji-packs',
|
||||
|
@ -122,13 +136,14 @@ export const asyncRouterMap = [
|
|||
path: 'index',
|
||||
component: () => import('@/views/users/index'),
|
||||
name: 'Users',
|
||||
meta: { title: 'Users', icon: 'peoples', noCache: true }
|
||||
meta: { title: 'users', icon: 'peoples', noCache: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
...(settingsDisabled ? [] : [settings]),
|
||||
...(reportsDisabled ? [] : [reports]),
|
||||
...(invitesDisabled ? [] : [invites]),
|
||||
...(moderationLogDisabled ? [] : [moderationLog]),
|
||||
...(settingsDisabled ? [] : [settings]),
|
||||
...(emojiPacksDisabled ? [] : [emojiPacks]),
|
||||
{
|
||||
path: '/users/:id',
|
||||
|
|
|
@ -2,6 +2,7 @@ import Vue from 'vue'
|
|||
import Vuex from 'vuex'
|
||||
import app from './modules/app'
|
||||
import errorLog from './modules/errorLog'
|
||||
import moderationLog from './modules/moderationLog'
|
||||
import invites from './modules/invites'
|
||||
import permission from './modules/permission'
|
||||
import reports from './modules/reports'
|
||||
|
@ -19,6 +20,7 @@ const store = new Vuex.Store({
|
|||
modules: {
|
||||
app,
|
||||
errorLog,
|
||||
moderationLog,
|
||||
invites,
|
||||
permission,
|
||||
reports,
|
||||
|
|
51
src/store/modules/moderationLog.js
Normal file
51
src/store/modules/moderationLog.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
import { fetchLog, fetchAdmins, fetchModerators } from '@/api/moderationLog'
|
||||
|
||||
const moderationLog = {
|
||||
state: {
|
||||
fetchedLog: [],
|
||||
logItemsCount: 0,
|
||||
admins: [],
|
||||
moderators: [],
|
||||
logLoading: true,
|
||||
adminsLoading: true
|
||||
},
|
||||
mutations: {
|
||||
SET_LOG_LOADING: (state, status) => {
|
||||
state.logLoading = status
|
||||
},
|
||||
SET_ADMINS_LOADING: (state, status) => {
|
||||
state.adminsLoading = status
|
||||
},
|
||||
SET_MODERATION_LOG: (state, log) => {
|
||||
state.fetchedLog = log
|
||||
},
|
||||
SET_MODERATION_LOG_COUNT: (state, count) => {
|
||||
state.logItemsCount = count
|
||||
},
|
||||
SET_ADMINS: (state, admins) => {
|
||||
state.admins = admins
|
||||
},
|
||||
SET_MODERATORS: (state, moderators) => {
|
||||
state.moderators = moderators
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async FetchModerationLog({ commit, getters }, opts = {}) {
|
||||
const response = await fetchLog(getters.authHost, getters.token, opts)
|
||||
|
||||
commit('SET_MODERATION_LOG', response.data.items)
|
||||
commit('SET_MODERATION_LOG_COUNT', response.data.total)
|
||||
commit('SET_LOG_LOADING', false)
|
||||
},
|
||||
async FetchAdmins({ commit, getters }) {
|
||||
const adminsResponse = await fetchAdmins(getters.authHost, getters.token)
|
||||
const moderatorsResponse = await fetchModerators(getters.authHost, getters.token)
|
||||
|
||||
commit('SET_ADMINS', adminsResponse.data)
|
||||
commit('SET_MODERATORS', moderatorsResponse.data)
|
||||
commit('SET_ADMINS_LOADING', false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default moderationLog
|
151
src/views/moderation_log/index.vue
Normal file
151
src/views/moderation_log/index.vue
Normal file
|
@ -0,0 +1,151 @@
|
|||
<template>
|
||||
<div v-if="!loading" class="moderation-log-container">
|
||||
<h1>{{ $t('moderationLog.moderationLog') }}</h1>
|
||||
<el-row type="flex" class="row-bg" justify="space-between">
|
||||
<el-col :span="9">
|
||||
<el-select
|
||||
v-model="user"
|
||||
class="user-select"
|
||||
clearable
|
||||
placeholder="Filter by admin/moderator"
|
||||
@change="fetchLogWithFilters">
|
||||
<el-option-group
|
||||
v-for="group in users"
|
||||
:key="group.label"
|
||||
:label="group.label">
|
||||
<el-option
|
||||
v-for="item in group.options"
|
||||
:key="item.id"
|
||||
:label="item.nickname"
|
||||
:value="item.id" />
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="6" class="search-container">
|
||||
<el-input
|
||||
v-model="search"
|
||||
placeholder="Search logs"
|
||||
clearable
|
||||
@input="handleDebounceSearchInput" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row type="flex" class="row-bg" justify="space-between">
|
||||
<el-col :span="9" class="date-container">
|
||||
<el-date-picker
|
||||
:default-time="['00:00:00', '23:59:59']"
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
start-placeholder="Start date"
|
||||
end-placeholder="End date"
|
||||
unlink-panels
|
||||
@change="fetchLogWithFilters" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-timeline>
|
||||
<el-timeline-item
|
||||
v-for="(logEntry, index) in log"
|
||||
:key="index"
|
||||
:timestamp="normalizeTimestamp(logEntry.time)">
|
||||
{{ logEntry.message }}
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
:current-page.sync="currentPage"
|
||||
:hide-on-single-page="true"
|
||||
:page-size="50"
|
||||
:total="total"
|
||||
layout="prev, pager, next"
|
||||
@current-change="fetchLogWithFilters" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment'
|
||||
import _ from 'lodash'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
dateRange: '',
|
||||
search: '',
|
||||
user: '',
|
||||
currentPage: 1
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
loading() {
|
||||
return this.$store.state.moderationLog.logLoading &&
|
||||
this.$store.state.moderationLog.adminsLoading
|
||||
},
|
||||
log() {
|
||||
return this.$store.state.moderationLog.fetchedLog
|
||||
},
|
||||
total() {
|
||||
return this.$store.state.moderationLog.logItemsCount
|
||||
},
|
||||
users() {
|
||||
return [
|
||||
{
|
||||
label: 'Admins',
|
||||
options: this.$store.state.moderationLog.admins.users
|
||||
},
|
||||
{
|
||||
label: 'Moderators',
|
||||
options: this.$store.state.moderationLog.moderators.users
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.handleDebounceSearchInput = debounce((query) => {
|
||||
this.fetchLogWithFilters()
|
||||
}, 500)
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('FetchModerationLog')
|
||||
this.$store.dispatch('FetchAdmins')
|
||||
},
|
||||
methods: {
|
||||
normalizeTimestamp(timestamp) {
|
||||
return moment(timestamp * 1000).format('YYYY-MM-DD HH:mm')
|
||||
},
|
||||
fetchLogWithFilters() {
|
||||
const filters = _.omitBy({
|
||||
start_date: this.dateRange ? this.dateRange[0].toISOString() : null,
|
||||
end_date: this.dateRange ? this.dateRange[1].toISOString() : null,
|
||||
user_id: this.user,
|
||||
search: this.search,
|
||||
page: this.currentPage
|
||||
}, val => val === '' || val === null)
|
||||
|
||||
this.$store.dispatch('FetchModerationLog', filters)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style rel='stylesheet/scss' lang='scss' scoped>
|
||||
.moderation-log-container {
|
||||
margin: 0 15px;
|
||||
}
|
||||
h1 {
|
||||
margin: 22px 0 20px 0;
|
||||
}
|
||||
.el-timeline {
|
||||
margin: 25px 45px 0 0;
|
||||
padding: 0px;
|
||||
}
|
||||
.user-select {
|
||||
margin: 0 0 20px;
|
||||
width: 350px;
|
||||
}
|
||||
.search-container {
|
||||
text-align: right;
|
||||
}
|
||||
.pagination {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
Loading…
Reference in a new issue