diff options
author | Rigel Kent <sendmemail@rigelk.eu> | 2020-07-15 11:17:03 +0200 |
---|---|---|
committer | Rigel Kent <par@rigelk.eu> | 2020-07-29 18:15:53 +0200 |
commit | bc99dfe54e093e69ba8fd06d36b36fbbda3f45de (patch) | |
tree | 2c13497b77928c2593310746e3ec33333e2b4d66 /client/src/app/+admin/users | |
parent | 654a188f80fc1f089aa14837084664c908fe27d2 (diff) | |
download | PeerTube-bc99dfe54e093e69ba8fd06d36b36fbbda3f45de.tar.gz PeerTube-bc99dfe54e093e69ba8fd06d36b36fbbda3f45de.tar.zst PeerTube-bc99dfe54e093e69ba8fd06d36b36fbbda3f45de.zip |
variable columns for users list, more columns possible, badge display for statuses
Diffstat (limited to 'client/src/app/+admin/users')
3 files changed, 129 insertions, 19 deletions
diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html index b022331db..571c780d6 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.html +++ b/client/src/app/+admin/users/user-list/user-list.component.html | |||
@@ -50,19 +50,41 @@ | |||
50 | <p-tableHeaderCheckbox></p-tableHeaderCheckbox> | 50 | <p-tableHeaderCheckbox></p-tableHeaderCheckbox> |
51 | </th> | 51 | </th> |
52 | <th style="width: 40px"></th> | 52 | <th style="width: 40px"></th> |
53 | <th pResizableColumn i18n pSortableColumn="username">Username <p-sortIcon field="username"></p-sortIcon></th> | 53 | <th *ngIf="getColumn('username')" pResizableColumn i18n pSortableColumn="username">{{ getColumn('username').label }} <p-sortIcon field="username"></p-sortIcon></th> |
54 | <th i18n>Email</th> | 54 | <th *ngIf="getColumn('email')" i18n>{{ getColumn('email').label }}</th> |
55 | <th style="width: 140px;" i18n pSortableColumn="videoQuotaUsed">Video quota <p-sortIcon field="videoQuotaUsed"></p-sortIcon></th> | 55 | <th *ngIf="getColumn('quota')" style="width: 160px;" i18n pSortableColumn="videoQuotaUsed">{{ getColumn('quota').label }} <p-sortIcon field="videoQuotaUsed"></p-sortIcon></th> |
56 | <th style="width: 120px;" i18n>Role</th> | 56 | <th *ngIf="getColumn('quotaDaily')" style="width: 160px;" i18n>{{ getColumn('quotaDaily').label }}</th> |
57 | <th style="width: 140px;" pResizableColumn i18n>Auth plugin</th> | 57 | <th *ngIf="getColumn('role')" style="width: 120px;" i18n pSortableColumn="role">{{ getColumn('role').label }} <p-sortIcon field="role"></p-sortIcon></th> |
58 | <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> | 58 | <th *ngIf="getColumn('pluginAuth')" style="width: 140px;" pResizableColumn i18n>{{ getColumn('pluginAuth').label }}</th> |
59 | <th style="width: 60px;"></th> | 59 | <th *ngIf="getColumn('createdAt')" style="width: 150px;" i18n pSortableColumn="createdAt">{{ getColumn('createdAt').label }} <p-sortIcon field="createdAt"></p-sortIcon></th> |
60 | <th *ngIf="getColumn('lastLoginDate')" style="width: 150px;" i18n pSortableColumn="lastLoginDate">{{ getColumn('lastLoginDate').label }} <p-sortIcon field="lastLoginDate"></p-sortIcon></th> | ||
61 | <th style="width: 60px;"> | ||
62 | <div class="c-hand" ngbDropdown placement="bottom-right auto" container="body" autoClose="outside"> | ||
63 | <my-global-icon iconName="columns" ngbDropdownToggle></my-global-icon> | ||
64 | |||
65 | <div role="menu" class="dropdown-menu" ngbDropdownMenu> | ||
66 | <div class="dropdown-header" i18n>Table parameters</div> | ||
67 | <div ngbDropdownItem class="dropdown-item"> | ||
68 | <p-multiSelect | ||
69 | [options]="columns" [showToggleAll]="true" [(ngModel)]="selectedColumns" optionLabel="label" | ||
70 | emptyFilterMessage="No matching column found" i18n-emptyFilterMessage [filter]="false" | ||
71 | selectedItemsLabel="{0} columns displayed" i18n-emptyFilterMessage [showHeader]="false" | ||
72 | [maxSelectedLabels]="4" | ||
73 | ></p-multiSelect> | ||
74 | </div> | ||
75 | <div ngbDropdownItem class="dropdown-item"> | ||
76 | <my-peertube-checkbox inputName="highlightBannedUsers" [(ngModel)]="highlightBannedUsers" | ||
77 | i18n-labelText labelText="Highlight banned users"></my-peertube-checkbox> | ||
78 | </div> | ||
79 | </div> | ||
80 | </div> | ||
81 | </th> | ||
60 | </tr> | 82 | </tr> |
61 | </ng-template> | 83 | </ng-template> |
62 | 84 | ||
63 | <ng-template pTemplate="body" let-expanded="expanded" let-user> | 85 | <ng-template pTemplate="body" let-expanded="expanded" let-user> |
64 | 86 | ||
65 | <tr [pSelectableRow]="user" [ngClass]="{ banned: user.blocked }"> | 87 | <tr [pSelectableRow]="user" [ngClass]="{ banned: highlightBannedUsers && user.blocked }"> |
66 | <td> | 88 | <td> |
67 | <p-tableCheckbox [value]="user"></p-tableCheckbox> | 89 | <p-tableCheckbox [value]="user"></p-tableCheckbox> |
68 | </td> | 90 | </td> |
@@ -73,7 +95,7 @@ | |||
73 | </span> | 95 | </span> |
74 | </td> | 96 | </td> |
75 | 97 | ||
76 | <td> | 98 | <td *ngIf="getColumn('username')"> |
77 | <a i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/accounts/' + user.username ]"> | 99 | <a i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/accounts/' + user.username ]"> |
78 | <div class="chip two-lines"> | 100 | <div class="chip two-lines"> |
79 | <img | 101 | <img |
@@ -83,17 +105,16 @@ | |||
83 | alt="Avatar" | 105 | alt="Avatar" |
84 | > | 106 | > |
85 | <div> | 107 | <div> |
86 | <span class="user-table-primary-text"> | 108 | <span class="user-table-primary-text">{{ user.account.displayName }}</span> |
87 | <span *ngIf="user.blocked" i18n-title title="The user was banned" class="glyphicon glyphicon-ban-circle"></span> | ||
88 | {{ user.account.displayName }} | ||
89 | </span> | ||
90 | <span class="text-muted">{{ user.username }}</span> | 109 | <span class="text-muted">{{ user.username }}</span> |
91 | </div> | 110 | </div> |
92 | </div> | 111 | </div> |
93 | </a> | 112 | </a> |
94 | </td> | 113 | </td> |
95 | 114 | ||
96 | <td *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus" [title]="user.email">{{ user.email }}</td> | 115 | <td *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus" [title]="user.email"> |
116 | <a class="table-email" [href]="'mailto:' + user.email">{{ user.email }}</a> | ||
117 | </td> | ||
97 | 118 | ||
98 | <ng-template #emailWithVerificationStatus> | 119 | <ng-template #emailWithVerificationStatus> |
99 | <td *ngIf="user.emailVerified === false; else emailVerifiedNotFalse" i18n-title title="User's email must be verified to login"> | 120 | <td *ngIf="user.emailVerified === false; else emailVerifiedNotFalse" i18n-title title="User's email must be verified to login"> |
@@ -106,14 +127,38 @@ | |||
106 | </ng-template> | 127 | </ng-template> |
107 | </ng-template> | 128 | </ng-template> |
108 | 129 | ||
109 | <td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td> | 130 | <td *ngIf="getColumn('quota')"> |
110 | <td>{{ user.roleLabel }}</td> | 131 | <div class="progress" i18n-title title="Total video quota"> |
132 | <div class="progress-bar" role="progressbar" [style]="{ width: getUserVideoQuotaPercentage(user) + '%' }" | ||
133 | [attr.aria-valuenow]="user.rawVideoQuotaUsed" aria-valuemin="0" [attr.aria-valuemax]="user.rawVideoQuota"> | ||
134 | </div> | ||
135 | <span>{{ user.videoQuotaUsed }}</span> | ||
136 | <span>{{ user.videoQuota }}</span> | ||
137 | </div> | ||
138 | </td> | ||
139 | |||
140 | <td *ngIf="getColumn('quotaDaily')"> | ||
141 | <div class="progress" i18n-title title="Total daily video quota"> | ||
142 | <div class="progress-bar secondary" role="progressbar" [style]="{ width: getUserVideoQuotaDailyPercentage(user) + '%' }" | ||
143 | [attr.aria-valuenow]="user.rawVideoQuotaUsedDaily" aria-valuemin="0" [attr.aria-valuemax]="user.rawVideoQuotaDaily"> | ||
144 | </div> | ||
145 | <span>{{ user.videoQuotaUsedDaily }}</span> | ||
146 | <span>{{ user.videoQuotaDaily }}</span> | ||
147 | </div> | ||
148 | </td> | ||
111 | 149 | ||
112 | <td> | 150 | <td *ngIf="getColumn('role')"> |
151 | <span *ngIf="user.blocked" class="badge badge-banned" i18n-title title="The user was banned">{{ user.roleLabel }}</span> | ||
152 | <span *ngIf="!user.blocked" class="badge" [ngClass]="getRoleClass(user.role)">{{ user.roleLabel }}</span> | ||
153 | </td> | ||
154 | |||
155 | <td *ngIf="getColumn('pluginAuth')"> | ||
113 | <ng-container *ngIf="user.pluginAuth">{{ user.pluginAuth }}</ng-container> | 156 | <ng-container *ngIf="user.pluginAuth">{{ user.pluginAuth }}</ng-container> |
114 | </td> | 157 | </td> |
115 | 158 | ||
116 | <td [title]="user.createdAt">{{ user.createdAt | date: 'short' }}</td> | 159 | <td *ngIf="getColumn('createdAt')" [title]="user.createdAt">{{ user.createdAt | date: 'short' }}</td> |
160 | |||
161 | <td *ngIf="getColumn('lastLoginDate')" [title]="user.lastLoginDate">{{ user.lastLoginDate | date: 'short' }}</td> | ||
117 | 162 | ||
118 | <td class="action-cell"> | 163 | <td class="action-cell"> |
119 | <my-user-moderation-dropdown | 164 | <my-user-moderation-dropdown |
diff --git a/client/src/app/+admin/users/user-list/user-list.component.scss b/client/src/app/+admin/users/user-list/user-list.component.scss index 2b84dec75..59ad7da35 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.scss +++ b/client/src/app/+admin/users/user-list/user-list.component.scss | |||
@@ -9,6 +9,11 @@ tr.banned > td { | |||
9 | background-color: lighten($color: $red, $amount: 40) !important; | 9 | background-color: lighten($color: $red, $amount: 40) !important; |
10 | } | 10 | } |
11 | 11 | ||
12 | .table-email { | ||
13 | @include disable-default-a-behaviour; | ||
14 | color: pvar(--mainForegroundColor); | ||
15 | } | ||
16 | |||
12 | .banned-info { | 17 | .banned-info { |
13 | font-style: italic; | 18 | font-style: italic; |
14 | } | 19 | } |
@@ -36,10 +41,24 @@ p-tableCheckbox { | |||
36 | top: -2.5px; | 41 | top: -2.5px; |
37 | } | 42 | } |
38 | 43 | ||
44 | my-global-icon { | ||
45 | width: 18px; | ||
46 | } | ||
47 | |||
39 | .chip { | 48 | .chip { |
40 | @include chip; | 49 | @include chip; |
41 | } | 50 | } |
42 | 51 | ||
52 | .badge { | ||
53 | @include table-badge; | ||
54 | } | ||
55 | |||
56 | .progress { | ||
57 | @include progressbar; | ||
58 | width: auto; | ||
59 | max-width: 100%; | ||
60 | } | ||
61 | |||
43 | .input-group { | 62 | .input-group { |
44 | @include peertube-input-group(300px); | 63 | @include peertube-input-group(300px); |
45 | input { | 64 | input { |
diff --git a/client/src/app/+admin/users/user-list/user-list.component.ts b/client/src/app/+admin/users/user-list/user-list.component.ts index 0b72b07c1..b2978212e 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.ts +++ b/client/src/app/+admin/users/user-list/user-list.component.ts | |||
@@ -4,7 +4,7 @@ import { AuthService, ConfirmService, Notifier, RestPagination, RestTable, Serve | |||
4 | import { Actor, DropdownAction } from '@app/shared/shared-main' | 4 | import { Actor, DropdownAction } from '@app/shared/shared-main' |
5 | import { UserBanModalComponent } from '@app/shared/shared-moderation' | 5 | import { UserBanModalComponent } from '@app/shared/shared-moderation' |
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | 6 | import { I18n } from '@ngx-translate/i18n-polyfill' |
7 | import { ServerConfig, User } from '@shared/models' | 7 | import { ServerConfig, User, UserRole } from '@shared/models' |
8 | import { Params, Router, ActivatedRoute } from '@angular/router' | 8 | import { Params, Router, ActivatedRoute } from '@angular/router' |
9 | 9 | ||
10 | @Component({ | 10 | @Component({ |
@@ -19,9 +19,12 @@ export class UserListComponent extends RestTable implements OnInit { | |||
19 | totalRecords = 0 | 19 | totalRecords = 0 |
20 | sort: SortMeta = { field: 'createdAt', order: 1 } | 20 | sort: SortMeta = { field: 'createdAt', order: 1 } |
21 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | 21 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } |
22 | highlightBannedUsers = false | ||
22 | 23 | ||
23 | selectedUsers: User[] = [] | 24 | selectedUsers: User[] = [] |
24 | bulkUserActions: DropdownAction<User[]>[][] = [] | 25 | bulkUserActions: DropdownAction<User[]>[][] = [] |
26 | columns: { key: string, label: string }[] | ||
27 | _selectedColumns: { key: string, label: string }[] | ||
25 | 28 | ||
26 | private serverConfig: ServerConfig | 29 | private serverConfig: ServerConfig |
27 | 30 | ||
@@ -46,6 +49,14 @@ export class UserListComponent extends RestTable implements OnInit { | |||
46 | return this.serverConfig.signup.requiresEmailVerification | 49 | return this.serverConfig.signup.requiresEmailVerification |
47 | } | 50 | } |
48 | 51 | ||
52 | get selectedColumns () { | ||
53 | return this._selectedColumns | ||
54 | } | ||
55 | |||
56 | set selectedColumns (val) { | ||
57 | this._selectedColumns = val | ||
58 | } | ||
59 | |||
49 | ngOnInit () { | 60 | ngOnInit () { |
50 | this.serverConfig = this.serverService.getTmpConfig() | 61 | this.serverConfig = this.serverService.getTmpConfig() |
51 | this.serverService.getConfig() | 62 | this.serverService.getConfig() |
@@ -92,12 +103,47 @@ export class UserListComponent extends RestTable implements OnInit { | |||
92 | } | 103 | } |
93 | ] | 104 | ] |
94 | ] | 105 | ] |
106 | |||
107 | this.columns = [ | ||
108 | { key: 'username', label: 'Username' }, | ||
109 | { key: 'email', label: 'Email' }, | ||
110 | { key: 'quota', label: 'Video quota' }, | ||
111 | { key: 'role', label: 'Role' }, | ||
112 | { key: 'createdAt', label: 'Created' } | ||
113 | ] | ||
114 | this.selectedColumns = [...this.columns] | ||
115 | this.columns.push({ key: 'quotaDaily', label: 'Daily quota' }) | ||
116 | this.columns.push({ key: 'pluginAuth', label: 'Auth plugin' }) | ||
117 | this.columns.push({ key: 'lastLoginDate', label: 'Last login' }) | ||
95 | } | 118 | } |
96 | 119 | ||
97 | getIdentifier () { | 120 | getIdentifier () { |
98 | return 'UserListComponent' | 121 | return 'UserListComponent' |
99 | } | 122 | } |
100 | 123 | ||
124 | getRoleClass (role: UserRole) { | ||
125 | switch (role) { | ||
126 | case UserRole.ADMINISTRATOR: | ||
127 | return 'badge-purple' | ||
128 | case UserRole.MODERATOR: | ||
129 | return 'badge-blue' | ||
130 | default: | ||
131 | return 'badge-yellow' | ||
132 | } | ||
133 | } | ||
134 | |||
135 | getColumn (key: string) { | ||
136 | return this.selectedColumns.find((col: any) => col.key === key) | ||
137 | } | ||
138 | |||
139 | getUserVideoQuotaPercentage (user: User & { rawVideoQuota: number, rawVideoQuotaUsed: number}) { | ||
140 | return user.rawVideoQuotaUsed * 100 / user.rawVideoQuota | ||
141 | } | ||
142 | |||
143 | getUserVideoQuotaDailyPercentage (user: User & { rawVideoQuotaDaily: number, rawVideoQuotaUsedDaily: number}) { | ||
144 | return user.rawVideoQuotaUsedDaily * 100 / user.rawVideoQuotaDaily | ||
145 | } | ||
146 | |||
101 | openBanUserModal (users: User[]) { | 147 | openBanUserModal (users: User[]) { |
102 | for (const user of users) { | 148 | for (const user of users) { |
103 | if (user.username === 'root') { | 149 | if (user.username === 'root') { |