Improve admin users list table

* Fix last login sort with null values
 * Remember last selected columns
 * Display last login date by default
This commit is contained in:
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 <p-table
[value]="users" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions" [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" [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true"
[showCurrentPageReport]="true" i18n-currentPageReportTemplate [showCurrentPageReport]="true" i18n-currentPageReportTemplate
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} users" currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} users"

View File

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

View File

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

View File

@ -32,7 +32,7 @@ import {
usersListValidator, usersListValidator,
usersRegisterValidator, usersRegisterValidator,
usersRemoveValidator, usersRemoveValidator,
usersSortValidator, adminUsersSortValidator,
usersUpdateValidator usersUpdateValidator
} from '../../../middlewares' } from '../../../middlewares'
import { import {
@ -84,7 +84,7 @@ usersRouter.get('/',
authenticate, authenticate,
ensureUserHasRight(UserRight.MANAGE_USERS), ensureUserHasRight(UserRight.MANAGE_USERS),
paginationValidator, paginationValidator,
usersSortValidator, adminUsersSortValidator,
setDefaultSort, setDefaultSort,
setDefaultPagination, setDefaultPagination,
usersListValidator, usersListValidator,
@ -277,7 +277,7 @@ async function autocompleteUsers (req: express.Request, res: express.Response) {
} }
async function listUsers (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, start: req.query.start,
count: req.query.count, count: req.query.count,
sort: req.query.sort, sort: req.query.sort,

View File

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

View File

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

View File

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

View File

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