diff options
-rw-r--r-- | client/src/app/+admin/overview/users/user-list/user-list.component.html | 2 | ||||
-rw-r--r-- | client/src/app/+admin/overview/users/user-list/user-list.component.ts | 44 | ||||
-rw-r--r-- | client/src/app/core/rest/rest-table.ts | 8 | ||||
-rw-r--r-- | server/controllers/api/users/index.ts | 6 | ||||
-rw-r--r-- | server/initializers/constants.ts | 2 | ||||
-rw-r--r-- | server/middlewares/validators/sort.ts | 4 | ||||
-rw-r--r-- | server/models/user/user.ts | 6 | ||||
-rw-r--r-- | server/models/utils.ts | 22 |
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 @@ | |||
1 | import { SortMeta } from 'primeng/api' | 1 | import { SortMeta } from 'primeng/api' |
2 | import { Component, OnInit, ViewChild } from '@angular/core' | 2 | import { Component, OnInit, ViewChild } from '@angular/core' |
3 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
4 | import { AuthService, ConfirmService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' | 4 | import { AuthService, ConfirmService, LocalStorageService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' |
5 | import { getAPIHost } from '@app/helpers' | 5 | import { getAPIHost } from '@app/helpers' |
6 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | 6 | import { AdvancedInputFilter } from '@app/shared/shared-forms' |
7 | import { Actor, DropdownAction } from '@app/shared/shared-main' | 7 | import { 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 | }) |
24 | export class UserListComponent extends RestTable implements OnInit { | 24 | export 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' |
38 | import { | 38 | import { |
@@ -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 | ||
279 | async function listUsers (req: express.Request, res: express.Response) { | 279 | async 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 |
60 | const SORTABLE_COLUMNS = { | 60 | const 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 | ||
31 | const usersSortValidator = checkSortFactory(SORTABLE_COLUMNS.USERS) | 31 | const adminUsersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ADMIN_USERS) |
32 | const accountsSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS) | 32 | const accountsSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS) |
33 | const jobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.JOBS, [ 'jobs' ]) | 33 | const jobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.JOBS, [ 'jobs' ]) |
34 | const abusesSortValidator = checkSortFactory(SORTABLE_COLUMNS.ABUSES) | 34 | const abusesSortValidator = checkSortFactory(SORTABLE_COLUMNS.ABUSES) |
@@ -59,7 +59,7 @@ const videoChannelsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CH | |||
59 | // --------------------------------------------------------------------------- | 59 | // --------------------------------------------------------------------------- |
60 | 60 | ||
61 | export { | 61 | export { |
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' | |||
66 | import { ActorFollowModel } from '../actor/actor-follow' | 66 | import { ActorFollowModel } from '../actor/actor-follow' |
67 | import { ActorImageModel } from '../actor/actor-image' | 67 | import { ActorImageModel } from '../actor/actor-image' |
68 | import { OAuthTokenModel } from '../oauth/oauth-token' | 68 | import { OAuthTokenModel } from '../oauth/oauth-token' |
69 | import { getSort, throwIfNotValid } from '../utils' | 69 | import { getAdminUsersSort, throwIfNotValid } from '../utils' |
70 | import { VideoModel } from '../video/video' | 70 | import { VideoModel } from '../video/video' |
71 | import { VideoChannelModel } from '../video/video-channel' | 71 | import { VideoChannelModel } from '../video/video-channel' |
72 | import { VideoImportModel } from '../video/video-import' | 72 | import { 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 | ||
21 | function 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 | |||
23 | function getPlaylistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | 40 | function 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, |