Improve admin users list table

* Fix last login sort with null values
 * Remember last selected columns
 * Display last login date by default
pull/5004/head
Chocobozzz 2022-05-24 15:05:39 +02:00
parent 3eba7ab815
commit 87a0cac618
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
8 changed files with 69 additions and 25 deletions

View File

@ -5,7 +5,7 @@
<p-table
[value]="users" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
[sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true" [(selection)]="selectedUsers"
[sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true" [(selection)]="selectedUsers"
[lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true"
[showCurrentPageReport]="true" i18n-currentPageReportTemplate
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} users"

View File

@ -1,7 +1,7 @@
import { SortMeta } from 'primeng/api'
import { Component, OnInit, ViewChild } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { AuthService, ConfirmService, Notifier, RestPagination, RestTable, ServerService } from '@app/core'
import { AuthService, ConfirmService, LocalStorageService, Notifier, RestPagination, RestTable, ServerService } from '@app/core'
import { getAPIHost } from '@app/helpers'
import { AdvancedInputFilter } from '@app/shared/shared-forms'
import { Actor, DropdownAction } from '@app/shared/shared-main'
@ -22,6 +22,8 @@ type UserForList = User & {
styleUrls: [ './user-list.component.scss' ]
})
export class UserListComponent extends RestTable implements OnInit {
private static readonly LOCAL_STORAGE_SELECTED_COLUMNS_KEY = 'admin-user-list-selected-columns'
@ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent
users: (User & { accountMutedStatus: AccountMutedStatus })[] = []
@ -56,7 +58,7 @@ export class UserListComponent extends RestTable implements OnInit {
requiresEmailVerification = false
private _selectedColumns: string[]
private _selectedColumns: string[] = []
constructor (
protected route: ActivatedRoute,
@ -66,7 +68,8 @@ export class UserListComponent extends RestTable implements OnInit {
private serverService: ServerService,
private auth: AuthService,
private blocklist: BlocklistService,
private userAdminService: UserAdminService
private userAdminService: UserAdminService,
private peertubeLocalStorage: LocalStorageService
) {
super()
}
@ -76,11 +79,13 @@ export class UserListComponent extends RestTable implements OnInit {
}
get selectedColumns () {
return this._selectedColumns
return this._selectedColumns || []
}
set selectedColumns (val: string[]) {
this._selectedColumns = val
this.saveSelectedColumns()
}
ngOnInit () {
@ -126,14 +131,35 @@ export class UserListComponent extends RestTable implements OnInit {
{ id: 'role', label: $localize`Role` },
{ id: 'email', label: $localize`Email` },
{ id: 'quota', label: $localize`Video quota` },
{ id: 'createdAt', label: $localize`Created` }
{ id: 'createdAt', label: $localize`Created` },
{ id: 'lastLoginDate', label: $localize`Last login` },
{ id: 'quotaDaily', label: $localize`Daily quota` },
{ id: 'pluginAuth', label: $localize`Auth plugin` }
]
this.selectedColumns = this.columns.map(c => c.id)
this.loadSelectedColumns()
}
this.columns.push({ id: 'quotaDaily', label: $localize`Daily quota` })
this.columns.push({ id: 'pluginAuth', label: $localize`Auth plugin` })
this.columns.push({ id: 'lastLoginDate', label: $localize`Last login` })
loadSelectedColumns () {
const result = this.peertubeLocalStorage.getItem(UserListComponent.LOCAL_STORAGE_SELECTED_COLUMNS_KEY)
if (result) {
try {
this.selectedColumns = JSON.parse(result)
return
} catch (err) {
console.error('Cannot load selected columns.', err)
}
}
// Default behaviour
this.selectedColumns = [ 'username', 'role', 'email', 'quota', 'createdAt', 'lastLoginDate' ]
return
}
saveSelectedColumns () {
this.peertubeLocalStorage.setItem(UserListComponent.LOCAL_STORAGE_SELECTED_COLUMNS_KEY, JSON.stringify(this.selectedColumns))
}
getIdentifier () {

View File

@ -39,6 +39,10 @@ export abstract class RestTable {
}
}
saveSort () {
peertubeLocalStorage.setItem(this.getSortLocalStorageKey(), JSON.stringify(this.sort))
}
loadLazy (event: LazyLoadEvent) {
logger('Load lazy %o.', event)
@ -60,10 +64,6 @@ export abstract class RestTable {
this.saveSort()
}
saveSort () {
peertubeLocalStorage.setItem(this.getSortLocalStorageKey(), JSON.stringify(this.sort))
}
onSearch (search: string) {
this.search = search
this.reloadData()

View File

@ -32,7 +32,7 @@ import {
usersListValidator,
usersRegisterValidator,
usersRemoveValidator,
usersSortValidator,
adminUsersSortValidator,
usersUpdateValidator
} from '../../../middlewares'
import {
@ -84,7 +84,7 @@ usersRouter.get('/',
authenticate,
ensureUserHasRight(UserRight.MANAGE_USERS),
paginationValidator,
usersSortValidator,
adminUsersSortValidator,
setDefaultSort,
setDefaultPagination,
usersListValidator,
@ -277,7 +277,7 @@ async function autocompleteUsers (req: express.Request, res: express.Response) {
}
async function listUsers (req: express.Request, res: express.Response) {
const resultList = await UserModel.listForApi({
const resultList = await UserModel.listForAdminApi({
start: req.query.start,
count: req.query.count,
sort: req.query.sort,

View File

@ -58,7 +58,7 @@ const WEBSERVER = {
// Sortable columns per schema
const SORTABLE_COLUMNS = {
USERS: [ 'id', 'username', 'videoQuotaUsed', 'createdAt', 'lastLoginDate', 'role' ],
ADMIN_USERS: [ 'id', 'username', 'videoQuotaUsed', 'createdAt', 'lastLoginDate', 'role' ],
USER_SUBSCRIPTIONS: [ 'id', 'createdAt' ],
ACCOUNTS: [ 'createdAt' ],
JOBS: [ 'createdAt' ],

View File

@ -28,7 +28,7 @@ function createSortableColumns (sortableColumns: string[]) {
return sortableColumns.concat(sortableColumnDesc)
}
const usersSortValidator = checkSortFactory(SORTABLE_COLUMNS.USERS)
const adminUsersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ADMIN_USERS)
const accountsSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS)
const jobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.JOBS, [ 'jobs' ])
const abusesSortValidator = checkSortFactory(SORTABLE_COLUMNS.ABUSES)
@ -59,7 +59,7 @@ const videoChannelsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CH
// ---------------------------------------------------------------------------
export {
usersSortValidator,
adminUsersSortValidator,
abusesSortValidator,
videoChannelsSortValidator,
videoImportsSortValidator,

View File

@ -66,7 +66,7 @@ import { ActorModel } from '../actor/actor'
import { ActorFollowModel } from '../actor/actor-follow'
import { ActorImageModel } from '../actor/actor-image'
import { OAuthTokenModel } from '../oauth/oauth-token'
import { getSort, throwIfNotValid } from '../utils'
import { getAdminUsersSort, throwIfNotValid } from '../utils'
import { VideoModel } from '../video/video'
import { VideoChannelModel } from '../video/video-channel'
import { VideoImportModel } from '../video/video-import'
@ -461,7 +461,7 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
return this.count()
}
static listForApi (parameters: {
static listForAdminApi (parameters: {
start: number
count: number
sort: string
@ -497,7 +497,7 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
const query: FindOptions = {
offset: start,
limit: count,
order: getSort(sort),
order: getAdminUsersSort(sort),
where
}

View File

@ -11,8 +11,6 @@ function getSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderIt
if (field.toLowerCase() === 'match') { // Search
finalField = Sequelize.col('similarity')
} else if (field === 'videoQuotaUsed') { // Users list
finalField = Sequelize.col('videoQuotaUsed')
} else {
finalField = field
}
@ -20,6 +18,25 @@ function getSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderIt
return [ [ finalField, direction ], lastSort ]
}
function getAdminUsersSort (value: string): OrderItem[] {
const { direction, field } = buildDirectionAndField(value)
let finalField: string | ReturnType<typeof Sequelize.col>
if (field === 'videoQuotaUsed') { // Users list
finalField = Sequelize.col('videoQuotaUsed')
} else {
finalField = field
}
const nullPolicy = direction === 'ASC'
? 'NULLS FIRST'
: 'NULLS LAST'
// FIXME: typings
return [ [ finalField as any, direction, nullPolicy ], [ 'id', 'ASC' ] ]
}
function getPlaylistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
const { direction, field } = buildDirectionAndField(value)
@ -260,6 +277,7 @@ export {
buildLocalAccountIdsIn,
getSort,
getCommentSort,
getAdminUsersSort,
getVideoSort,
getBlacklistSort,
createSimilarityAttribute,