aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-05-24 15:05:39 +0200
committerChocobozzz <me@florianbigard.com>2022-05-24 15:29:17 +0200
commit87a0cac618c8ed4a09408273d0f5a468530e8062 (patch)
treeadba163a7628c5d1604c0741c82b6dc22b751249
parent3eba7ab8154027621989cdd4814bc749d79bf0c9 (diff)
downloadPeerTube-87a0cac618c8ed4a09408273d0f5a468530e8062.tar.gz
PeerTube-87a0cac618c8ed4a09408273d0f5a468530e8062.tar.zst
PeerTube-87a0cac618c8ed4a09408273d0f5a468530e8062.zip
Improve admin users list table
* Fix last login sort with null values * Remember last selected columns * Display last login date by default
-rw-r--r--client/src/app/+admin/overview/users/user-list/user-list.component.html2
-rw-r--r--client/src/app/+admin/overview/users/user-list/user-list.component.ts44
-rw-r--r--client/src/app/core/rest/rest-table.ts8
-rw-r--r--server/controllers/api/users/index.ts6
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/middlewares/validators/sort.ts4
-rw-r--r--server/models/user/user.ts6
-rw-r--r--server/models/utils.ts22
8 files changed, 69 insertions, 25 deletions
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.html b/client/src/app/+admin/overview/users/user-list/user-list.component.html
index 30d10e3cf..62eeef8fe 100644
--- a/client/src/app/+admin/overview/users/user-list/user-list.component.html
+++ b/client/src/app/+admin/overview/users/user-list/user-list.component.html
@@ -5,7 +5,7 @@
5 5
6<p-table 6<p-table
7 [value]="users" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions" 7 [value]="users" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
8 [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true" [(selection)]="selectedUsers" 8 [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true" [(selection)]="selectedUsers"
9 [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true" 9 [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true"
10 [showCurrentPageReport]="true" i18n-currentPageReportTemplate 10 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
11 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} users" 11 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} users"
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.ts b/client/src/app/+admin/overview/users/user-list/user-list.component.ts
index d22e1355e..9d11bd02e 100644
--- a/client/src/app/+admin/overview/users/user-list/user-list.component.ts
+++ b/client/src/app/+admin/overview/users/user-list/user-list.component.ts
@@ -1,7 +1,7 @@
1import { SortMeta } from 'primeng/api' 1import { SortMeta } from 'primeng/api'
2import { Component, OnInit, ViewChild } from '@angular/core' 2import { Component, OnInit, ViewChild } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { AuthService, ConfirmService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' 4import { AuthService, ConfirmService, LocalStorageService, Notifier, RestPagination, RestTable, ServerService } from '@app/core'
5import { getAPIHost } from '@app/helpers' 5import { getAPIHost } from '@app/helpers'
6import { AdvancedInputFilter } from '@app/shared/shared-forms' 6import { AdvancedInputFilter } from '@app/shared/shared-forms'
7import { Actor, DropdownAction } from '@app/shared/shared-main' 7import { Actor, DropdownAction } from '@app/shared/shared-main'
@@ -22,6 +22,8 @@ type UserForList = User & {
22 styleUrls: [ './user-list.component.scss' ] 22 styleUrls: [ './user-list.component.scss' ]
23}) 23})
24export class UserListComponent extends RestTable implements OnInit { 24export class UserListComponent extends RestTable implements OnInit {
25 private static readonly LOCAL_STORAGE_SELECTED_COLUMNS_KEY = 'admin-user-list-selected-columns'
26
25 @ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent 27 @ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent
26 28
27 users: (User & { accountMutedStatus: AccountMutedStatus })[] = [] 29 users: (User & { accountMutedStatus: AccountMutedStatus })[] = []
@@ -56,7 +58,7 @@ export class UserListComponent extends RestTable implements OnInit {
56 58
57 requiresEmailVerification = false 59 requiresEmailVerification = false
58 60
59 private _selectedColumns: string[] 61 private _selectedColumns: string[] = []
60 62
61 constructor ( 63 constructor (
62 protected route: ActivatedRoute, 64 protected route: ActivatedRoute,
@@ -66,7 +68,8 @@ export class UserListComponent extends RestTable implements OnInit {
66 private serverService: ServerService, 68 private serverService: ServerService,
67 private auth: AuthService, 69 private auth: AuthService,
68 private blocklist: BlocklistService, 70 private blocklist: BlocklistService,
69 private userAdminService: UserAdminService 71 private userAdminService: UserAdminService,
72 private peertubeLocalStorage: LocalStorageService
70 ) { 73 ) {
71 super() 74 super()
72 } 75 }
@@ -76,11 +79,13 @@ export class UserListComponent extends RestTable implements OnInit {
76 } 79 }
77 80
78 get selectedColumns () { 81 get selectedColumns () {
79 return this._selectedColumns 82 return this._selectedColumns || []
80 } 83 }
81 84
82 set selectedColumns (val: string[]) { 85 set selectedColumns (val: string[]) {
83 this._selectedColumns = val 86 this._selectedColumns = val
87
88 this.saveSelectedColumns()
84 } 89 }
85 90
86 ngOnInit () { 91 ngOnInit () {
@@ -126,14 +131,35 @@ export class UserListComponent extends RestTable implements OnInit {
126 { id: 'role', label: $localize`Role` }, 131 { id: 'role', label: $localize`Role` },
127 { id: 'email', label: $localize`Email` }, 132 { id: 'email', label: $localize`Email` },
128 { id: 'quota', label: $localize`Video quota` }, 133 { id: 'quota', label: $localize`Video quota` },
129 { id: 'createdAt', label: $localize`Created` } 134 { id: 'createdAt', label: $localize`Created` },
135 { id: 'lastLoginDate', label: $localize`Last login` },
136
137 { id: 'quotaDaily', label: $localize`Daily quota` },
138 { id: 'pluginAuth', label: $localize`Auth plugin` }
130 ] 139 ]
131 140
132 this.selectedColumns = this.columns.map(c => c.id) 141 this.loadSelectedColumns()
142 }
143
144 loadSelectedColumns () {
145 const result = this.peertubeLocalStorage.getItem(UserListComponent.LOCAL_STORAGE_SELECTED_COLUMNS_KEY)
146
147 if (result) {
148 try {
149 this.selectedColumns = JSON.parse(result)
150 return
151 } catch (err) {
152 console.error('Cannot load selected columns.', err)
153 }
154 }
155
156 // Default behaviour
157 this.selectedColumns = [ 'username', 'role', 'email', 'quota', 'createdAt', 'lastLoginDate' ]
158 return
159 }
133 160
134 this.columns.push({ id: 'quotaDaily', label: $localize`Daily quota` }) 161 saveSelectedColumns () {
135 this.columns.push({ id: 'pluginAuth', label: $localize`Auth plugin` }) 162 this.peertubeLocalStorage.setItem(UserListComponent.LOCAL_STORAGE_SELECTED_COLUMNS_KEY, JSON.stringify(this.selectedColumns))
136 this.columns.push({ id: 'lastLoginDate', label: $localize`Last login` })
137 } 163 }
138 164
139 getIdentifier () { 165 getIdentifier () {
diff --git a/client/src/app/core/rest/rest-table.ts b/client/src/app/core/rest/rest-table.ts
index d8b039187..7b765f7fc 100644
--- a/client/src/app/core/rest/rest-table.ts
+++ b/client/src/app/core/rest/rest-table.ts
@@ -39,6 +39,10 @@ export abstract class RestTable {
39 } 39 }
40 } 40 }
41 41
42 saveSort () {
43 peertubeLocalStorage.setItem(this.getSortLocalStorageKey(), JSON.stringify(this.sort))
44 }
45
42 loadLazy (event: LazyLoadEvent) { 46 loadLazy (event: LazyLoadEvent) {
43 logger('Load lazy %o.', event) 47 logger('Load lazy %o.', event)
44 48
@@ -60,10 +64,6 @@ export abstract class RestTable {
60 this.saveSort() 64 this.saveSort()
61 } 65 }
62 66
63 saveSort () {
64 peertubeLocalStorage.setItem(this.getSortLocalStorageKey(), JSON.stringify(this.sort))
65 }
66
67 onSearch (search: string) { 67 onSearch (search: string) {
68 this.search = search 68 this.search = search
69 this.reloadData() 69 this.reloadData()
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index 8a06bfe93..e13e31aaf 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -32,7 +32,7 @@ import {
32 usersListValidator, 32 usersListValidator,
33 usersRegisterValidator, 33 usersRegisterValidator,
34 usersRemoveValidator, 34 usersRemoveValidator,
35 usersSortValidator, 35 adminUsersSortValidator,
36 usersUpdateValidator 36 usersUpdateValidator
37} from '../../../middlewares' 37} from '../../../middlewares'
38import { 38import {
@@ -84,7 +84,7 @@ usersRouter.get('/',
84 authenticate, 84 authenticate,
85 ensureUserHasRight(UserRight.MANAGE_USERS), 85 ensureUserHasRight(UserRight.MANAGE_USERS),
86 paginationValidator, 86 paginationValidator,
87 usersSortValidator, 87 adminUsersSortValidator,
88 setDefaultSort, 88 setDefaultSort,
89 setDefaultPagination, 89 setDefaultPagination,
90 usersListValidator, 90 usersListValidator,
@@ -277,7 +277,7 @@ async function autocompleteUsers (req: express.Request, res: express.Response) {
277} 277}
278 278
279async function listUsers (req: express.Request, res: express.Response) { 279async function listUsers (req: express.Request, res: express.Response) {
280 const resultList = await UserModel.listForApi({ 280 const resultList = await UserModel.listForAdminApi({
281 start: req.query.start, 281 start: req.query.start,
282 count: req.query.count, 282 count: req.query.count,
283 sort: req.query.sort, 283 sort: req.query.sort,
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 02c1ef251..231b86821 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -58,7 +58,7 @@ const WEBSERVER = {
58 58
59// Sortable columns per schema 59// Sortable columns per schema
60const SORTABLE_COLUMNS = { 60const SORTABLE_COLUMNS = {
61 USERS: [ 'id', 'username', 'videoQuotaUsed', 'createdAt', 'lastLoginDate', 'role' ], 61 ADMIN_USERS: [ 'id', 'username', 'videoQuotaUsed', 'createdAt', 'lastLoginDate', 'role' ],
62 USER_SUBSCRIPTIONS: [ 'id', 'createdAt' ], 62 USER_SUBSCRIPTIONS: [ 'id', 'createdAt' ],
63 ACCOUNTS: [ 'createdAt' ], 63 ACCOUNTS: [ 'createdAt' ],
64 JOBS: [ 'createdAt' ], 64 JOBS: [ 'createdAt' ],
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts
index 3ba668460..c9978e3b4 100644
--- a/server/middlewares/validators/sort.ts
+++ b/server/middlewares/validators/sort.ts
@@ -28,7 +28,7 @@ function createSortableColumns (sortableColumns: string[]) {
28 return sortableColumns.concat(sortableColumnDesc) 28 return sortableColumns.concat(sortableColumnDesc)
29} 29}
30 30
31const usersSortValidator = checkSortFactory(SORTABLE_COLUMNS.USERS) 31const adminUsersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ADMIN_USERS)
32const accountsSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS) 32const accountsSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS)
33const jobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.JOBS, [ 'jobs' ]) 33const jobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.JOBS, [ 'jobs' ])
34const abusesSortValidator = checkSortFactory(SORTABLE_COLUMNS.ABUSES) 34const abusesSortValidator = checkSortFactory(SORTABLE_COLUMNS.ABUSES)
@@ -59,7 +59,7 @@ const videoChannelsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CH
59// --------------------------------------------------------------------------- 59// ---------------------------------------------------------------------------
60 60
61export { 61export {
62 usersSortValidator, 62 adminUsersSortValidator,
63 abusesSortValidator, 63 abusesSortValidator,
64 videoChannelsSortValidator, 64 videoChannelsSortValidator,
65 videoImportsSortValidator, 65 videoImportsSortValidator,
diff --git a/server/models/user/user.ts b/server/models/user/user.ts
index 326b2e789..20c2222a7 100644
--- a/server/models/user/user.ts
+++ b/server/models/user/user.ts
@@ -66,7 +66,7 @@ import { ActorModel } from '../actor/actor'
66import { ActorFollowModel } from '../actor/actor-follow' 66import { ActorFollowModel } from '../actor/actor-follow'
67import { ActorImageModel } from '../actor/actor-image' 67import { ActorImageModel } from '../actor/actor-image'
68import { OAuthTokenModel } from '../oauth/oauth-token' 68import { OAuthTokenModel } from '../oauth/oauth-token'
69import { getSort, throwIfNotValid } from '../utils' 69import { getAdminUsersSort, throwIfNotValid } from '../utils'
70import { VideoModel } from '../video/video' 70import { VideoModel } from '../video/video'
71import { VideoChannelModel } from '../video/video-channel' 71import { VideoChannelModel } from '../video/video-channel'
72import { VideoImportModel } from '../video/video-import' 72import { VideoImportModel } from '../video/video-import'
@@ -461,7 +461,7 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
461 return this.count() 461 return this.count()
462 } 462 }
463 463
464 static listForApi (parameters: { 464 static listForAdminApi (parameters: {
465 start: number 465 start: number
466 count: number 466 count: number
467 sort: string 467 sort: string
@@ -497,7 +497,7 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
497 const query: FindOptions = { 497 const query: FindOptions = {
498 offset: start, 498 offset: start,
499 limit: count, 499 limit: count,
500 order: getSort(sort), 500 order: getAdminUsersSort(sort),
501 where 501 where
502 } 502 }
503 503
diff --git a/server/models/utils.ts b/server/models/utils.ts
index b57290aff..88e31f22e 100644
--- a/server/models/utils.ts
+++ b/server/models/utils.ts
@@ -11,8 +11,6 @@ function getSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderIt
11 11
12 if (field.toLowerCase() === 'match') { // Search 12 if (field.toLowerCase() === 'match') { // Search
13 finalField = Sequelize.col('similarity') 13 finalField = Sequelize.col('similarity')
14 } else if (field === 'videoQuotaUsed') { // Users list
15 finalField = Sequelize.col('videoQuotaUsed')
16 } else { 14 } else {
17 finalField = field 15 finalField = field
18 } 16 }
@@ -20,6 +18,25 @@ function getSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderIt
20 return [ [ finalField, direction ], lastSort ] 18 return [ [ finalField, direction ], lastSort ]
21} 19}
22 20
21function getAdminUsersSort (value: string): OrderItem[] {
22 const { direction, field } = buildDirectionAndField(value)
23
24 let finalField: string | ReturnType<typeof Sequelize.col>
25
26 if (field === 'videoQuotaUsed') { // Users list
27 finalField = Sequelize.col('videoQuotaUsed')
28 } else {
29 finalField = field
30 }
31
32 const nullPolicy = direction === 'ASC'
33 ? 'NULLS FIRST'
34 : 'NULLS LAST'
35
36 // FIXME: typings
37 return [ [ finalField as any, direction, nullPolicy ], [ 'id', 'ASC' ] ]
38}
39
23function getPlaylistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { 40function getPlaylistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
24 const { direction, field } = buildDirectionAndField(value) 41 const { direction, field } = buildDirectionAndField(value)
25 42
@@ -260,6 +277,7 @@ export {
260 buildLocalAccountIdsIn, 277 buildLocalAccountIdsIn,
261 getSort, 278 getSort,
262 getCommentSort, 279 getCommentSort,
280 getAdminUsersSort,
263 getVideoSort, 281 getVideoSort,
264 getBlacklistSort, 282 getBlacklistSort,
265 createSimilarityAttribute, 283 createSimilarityAttribute,