forked from AkkomaGang/admin-fe
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
|
## Unreleased
|
||||||
|
|
||||||
|
## [1.2.0] - 2019-09-27
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Emoji pack configuration
|
- Emoji pack configuration
|
||||||
- Ability to require user's password reset
|
- 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
|
## [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
|
#### 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"]'`,
|
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.
|
|
||||||
|
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
|
## 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',
|
users: 'Users',
|
||||||
reports: 'Reports',
|
reports: 'Reports',
|
||||||
settings: 'Settings',
|
settings: 'Settings',
|
||||||
|
moderationLog: 'Moderation Log',
|
||||||
'emoji-packs': 'Emoji packs'
|
'emoji-packs': 'Emoji packs'
|
||||||
},
|
},
|
||||||
navbar: {
|
navbar: {
|
||||||
|
@ -284,6 +285,9 @@ export default {
|
||||||
closed: 'Closed',
|
closed: 'Closed',
|
||||||
resolved: 'Resolved'
|
resolved: 'Resolved'
|
||||||
},
|
},
|
||||||
|
moderationLog: {
|
||||||
|
moderationLog: 'Moderation Log'
|
||||||
|
},
|
||||||
settings: {
|
settings: {
|
||||||
settings: 'Settings',
|
settings: 'Settings',
|
||||||
instance: 'Instance',
|
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 emojiPacksDisabled = disabledFeatures.includes('emoji-packs')
|
||||||
const emojiPacks = {
|
const emojiPacks = {
|
||||||
path: '/emoji-packs',
|
path: '/emoji-packs',
|
||||||
|
@ -122,13 +136,14 @@ export const asyncRouterMap = [
|
||||||
path: 'index',
|
path: 'index',
|
||||||
component: () => import('@/views/users/index'),
|
component: () => import('@/views/users/index'),
|
||||||
name: 'Users',
|
name: 'Users',
|
||||||
meta: { title: 'Users', icon: 'peoples', noCache: true }
|
meta: { title: 'users', icon: 'peoples', noCache: true }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
...(settingsDisabled ? [] : [settings]),
|
|
||||||
...(reportsDisabled ? [] : [reports]),
|
...(reportsDisabled ? [] : [reports]),
|
||||||
...(invitesDisabled ? [] : [invites]),
|
...(invitesDisabled ? [] : [invites]),
|
||||||
|
...(moderationLogDisabled ? [] : [moderationLog]),
|
||||||
|
...(settingsDisabled ? [] : [settings]),
|
||||||
...(emojiPacksDisabled ? [] : [emojiPacks]),
|
...(emojiPacksDisabled ? [] : [emojiPacks]),
|
||||||
{
|
{
|
||||||
path: '/users/:id',
|
path: '/users/:id',
|
||||||
|
|
|
@ -2,6 +2,7 @@ import Vue from 'vue'
|
||||||
import Vuex from 'vuex'
|
import Vuex from 'vuex'
|
||||||
import app from './modules/app'
|
import app from './modules/app'
|
||||||
import errorLog from './modules/errorLog'
|
import errorLog from './modules/errorLog'
|
||||||
|
import moderationLog from './modules/moderationLog'
|
||||||
import invites from './modules/invites'
|
import invites from './modules/invites'
|
||||||
import permission from './modules/permission'
|
import permission from './modules/permission'
|
||||||
import reports from './modules/reports'
|
import reports from './modules/reports'
|
||||||
|
@ -19,6 +20,7 @@ const store = new Vuex.Store({
|
||||||
modules: {
|
modules: {
|
||||||
app,
|
app,
|
||||||
errorLog,
|
errorLog,
|
||||||
|
moderationLog,
|
||||||
invites,
|
invites,
|
||||||
permission,
|
permission,
|
||||||
reports,
|
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