aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/+admin/overview/users
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/+admin/overview/users')
-rw-r--r--client/src/app/+admin/overview/users/user-edit/user-edit.component.html228
-rw-r--r--client/src/app/+admin/overview/users/user-edit/user-edit.component.scss7
-rw-r--r--client/src/app/+admin/overview/users/user-edit/user-edit.ts4
-rw-r--r--client/src/app/+admin/overview/users/user-edit/user-password.component.html27
-rw-r--r--client/src/app/+admin/overview/users/user-edit/user-password.component.scss12
-rw-r--r--client/src/app/+admin/overview/users/user-list/user-list.component.html16
-rw-r--r--client/src/app/+admin/overview/users/user-list/user-list.component.scss12
-rw-r--r--client/src/app/+admin/overview/users/user-list/user-list.component.ts92
8 files changed, 209 insertions, 189 deletions
diff --git a/client/src/app/+admin/overview/users/user-edit/user-edit.component.html b/client/src/app/+admin/overview/users/user-edit/user-edit.component.html
index 772ebf272..e484ab8b0 100644
--- a/client/src/app/+admin/overview/users/user-edit/user-edit.component.html
+++ b/client/src/app/+admin/overview/users/user-edit/user-edit.component.html
@@ -57,7 +57,7 @@
57 </div> 57 </div>
58</ng-template> 58</ng-template>
59 59
60<div class="form-row" *ngIf="!isInBigView()"> <!-- hidden on large screens, as it is then displayed on the right side of the form --> 60<div class="row d-xxl-none"> <!-- hidden on large screens, as it is then displayed on the right side of the form -->
61 <div class="col-12 col-xl-3"></div> 61 <div class="col-12 col-xl-3"></div>
62 62
63 <div class="col-12 col-xl-9"> 63 <div class="col-12 col-xl-9">
@@ -67,8 +67,8 @@
67 67
68<div *ngIf="error" class="alert alert-danger">{{ error }}</div> 68<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
69 69
70<div class="form-row mt-4"> <!-- user grid --> 70<div class="row mt-4"> <!-- user grid -->
71 <div class="form-group col-12 col-lg-4 col-xl-3"> 71 <div class="col-12 col-lg-4 col-xl-3">
72 <div class="anchor" id="user"></div> <!-- user anchor --> 72 <div class="anchor" id="user"></div> <!-- user anchor -->
73 <div *ngIf="isCreation()" class="account-title" i18n>NEW USER</div> 73 <div *ngIf="isCreation()" class="account-title" i18n>NEW USER</div>
74 <div *ngIf="!isCreation() && user" class="account-title"> 74 <div *ngIf="!isCreation() && user" class="account-title">
@@ -76,150 +76,144 @@
76 </div> 76 </div>
77 </div> 77 </div>
78 78
79 <div class="form-group col-12 col-lg-8 col-xl-9" [ngClass]="{ 'form-row': isInBigView() }"> 79 <div class="col-12 col-lg-8 col-xl-9">
80 80 <div class="row">
81 <form role="form" (ngSubmit)="formValidated()" [formGroup]="form" [ngClass]="{ 'col-5': isInBigView() }"> 81 <form class="col" role="form" (ngSubmit)="formValidated()" [formGroup]="form">
82 <div class="form-group" *ngIf="isCreation()"> 82 <div class="form-group" *ngIf="isCreation()">
83 <label i18n for="username">Username</label> 83 <label i18n for="username">Username</label>
84 <input 84 <input
85 type="text" id="username" i18n-placeholder placeholder="john" class="form-control" 85 type="text" id="username" i18n-placeholder placeholder="john" class="form-control"
86 formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }" 86 formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }"
87 > 87 >
88 <div *ngIf="formErrors.username" class="form-error"> 88 <div *ngIf="formErrors.username" class="form-error">
89 {{ formErrors.username }} 89 {{ formErrors.username }}
90 </div>
90 </div> 91 </div>
91 </div>
92 92
93 <div class="form-group" *ngIf="isCreation()"> 93 <div class="form-group" *ngIf="isCreation()">
94 <label i18n for="channelName">Channel name</label> 94 <label i18n for="channelName">Channel name</label>
95 <input 95 <input
96 type="text" id="channelName" i18n-placeholder placeholder="john_channel" class="form-control" 96 type="text" id="channelName" i18n-placeholder placeholder="john_channel" class="form-control"
97 formControlName="channelName" [ngClass]="{ 'input-error': formErrors['channelName'] }" 97 formControlName="channelName" [ngClass]="{ 'input-error': formErrors['channelName'] }"
98 > 98 >
99 <div *ngIf="formErrors.channelName" class="form-error"> 99 <div *ngIf="formErrors.channelName" class="form-error">
100 {{ formErrors.channelName }} 100 {{ formErrors.channelName }}
101 </div>
101 </div> 102 </div>
102 </div>
103 103
104 <div class="form-group"> 104 <div class="form-group">
105 <label i18n for="email">Email</label> 105 <label i18n for="email">Email</label>
106 <input 106 <input
107 type="text" id="email" i18n-placeholder placeholder="mail@example.com" class="form-control" 107 type="text" id="email" i18n-placeholder placeholder="mail@example.com" class="form-control"
108 formControlName="email" [ngClass]="{ 'input-error': formErrors['email'] }" 108 formControlName="email" [ngClass]="{ 'input-error': formErrors['email'] }"
109 autocomplete="off" [readonly]="user && user.pluginAuth !== null" 109 autocomplete="off" [readonly]="user && user.pluginAuth !== null"
110 > 110 >
111 <div *ngIf="formErrors.email" class="form-error"> 111 <div *ngIf="formErrors.email" class="form-error">
112 {{ formErrors.email }} 112 {{ formErrors.email }}
113 </div>
113 </div> 114 </div>
114 </div>
115 115
116 <div class="form-group" *ngIf="isCreation()"> 116 <div class="form-group" *ngIf="isCreation()">
117 <label i18n for="password">Password</label> 117 <label i18n for="password">Password</label>
118 <my-help *ngIf="isPasswordOptional()"> 118 <my-help *ngIf="isPasswordOptional()">
119 <ng-template ptTemplate="customHtml"> 119 <ng-template ptTemplate="customHtml">
120 <ng-container i18n> 120 <ng-container i18n>
121 If you leave the password empty, an email will be sent to the user. 121 If you leave the password empty, an email will be sent to the user.
122 </ng-container> 122 </ng-container>
123 </ng-template> 123 </ng-template>
124 </my-help> 124 </my-help>
125 125
126 <my-input-toggle-hidden 126 <my-input-text formControlName="password" inputId="password" [formError]="formErrors['password']" autocomplete="new-password"></my-input-text>
127 formControlName="password" inputId="password" [ngClass]="{ 'input-error': formErrors['password'] }" autocomplete="new-password"
128 ></my-input-toggle-hidden>
129
130 <div *ngIf="formErrors.password" class="form-error">
131 {{ formErrors.password }}
132 </div> 127 </div>
133 </div>
134 128
135 <div class="form-group"> 129 <div class="form-group">
136 <label i18n for="role">Role</label> 130 <label i18n for="role">Role</label>
137 <div class="peertube-select-container"> 131 <div class="peertube-select-container">
138 <select id="role" formControlName="role" class="form-control"> 132 <select id="role" formControlName="role" class="form-control">
139 <option *ngFor="let role of roles" [value]="role.value"> 133 <option *ngFor="let role of roles" [value]="role.value">
140 {{ role.label }} 134 {{ role.label }}
141 </option> 135 </option>
142 </select> 136 </select>
137 </div>
138
139 <div *ngIf="formErrors.role" class="form-error">
140 {{ formErrors.role }}
141 </div>
143 </div> 142 </div>
144 143
145 <div *ngIf="formErrors.role" class="form-error"> 144 <div class="form-group">
146 {{ formErrors.role }} 145 <label i18n for="videoQuota">Video quota</label>
146
147 <my-select-custom-value
148 id="videoQuota"
149 [items]="videoQuotaOptions"
150 formControlName="videoQuota"
151 i18n-inputSuffix inputSuffix="bytes" inputType="number"
152 [clearable]="false"
153 ></my-select-custom-value>
154
155 <div i18n class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()">
156 Transcoding is enabled. The video quota only takes into account <strong>original</strong> video size. <br />
157 At most, this user could upload ~ {{ computeQuotaWithTranscoding() | bytes: 0 }}.
158 </div>
159
160 <div *ngIf="formErrors.videoQuota" class="form-error">
161 {{ formErrors.videoQuota }}
162 </div>
147 </div> 163 </div>
148 </div>
149 164
150 <div class="form-group"> 165 <div class="form-group">
151 <label i18n for="videoQuota">Video quota</label> 166 <label i18n for="videoQuotaDaily">Daily video quota</label>
152
153 <my-select-custom-value
154 id="videoQuota"
155 [items]="videoQuotaOptions"
156 formControlName="videoQuota"
157 i18n-inputSuffix inputSuffix="bytes" inputType="number"
158 [clearable]="false"
159 ></my-select-custom-value>
160
161 <div i18n class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()">
162 Transcoding is enabled. The video quota only takes into account <strong>original</strong> video size. <br />
163 At most, this user could upload ~ {{ computeQuotaWithTranscoding() | bytes: 0 }}.
164 </div>
165 167
166 <div *ngIf="formErrors.videoQuota" class="form-error"> 168 <my-select-custom-value
167 {{ formErrors.videoQuota }} 169 id="videoQuotaDaily"
168 </div> 170 [items]="videoQuotaDailyOptions"
169 </div> 171 formControlName="videoQuotaDaily"
172 i18n-inputSuffix inputSuffix="bytes" inputType="number"
173 [clearable]="false"
174 ></my-select-custom-value>
170 175
171 <div class="form-group"> 176 <div *ngIf="formErrors.videoQuotaDaily" class="form-error">
172 <label i18n for="videoQuotaDaily">Daily video quota</label> 177 {{ formErrors.videoQuotaDaily }}
173 178 </div>
174 <my-select-custom-value
175 id="videoQuotaDaily"
176 [items]="videoQuotaDailyOptions"
177 formControlName="videoQuotaDaily"
178 i18n-inputSuffix inputSuffix="bytes" inputType="number"
179 [clearable]="false"
180 ></my-select-custom-value>
181
182 <div *ngIf="formErrors.videoQuotaDaily" class="form-error">
183 {{ formErrors.videoQuotaDaily }}
184 </div> 179 </div>
185 </div>
186 180
187 <div class="form-group" *ngIf="!isCreation() && getAuthPlugins().length !== 0"> 181 <div class="form-group" *ngIf="!isCreation() && getAuthPlugins().length !== 0">
188 <label i18n for="pluginAuth">Auth plugin</label> 182 <label i18n for="pluginAuth">Auth plugin</label>
189 183
190 <div class="peertube-select-container"> 184 <div class="peertube-select-container">
191 <select id="pluginAuth" formControlName="pluginAuth" class="form-control"> 185 <select id="pluginAuth" formControlName="pluginAuth" class="form-control">
192 <option [value]="null" i18n>None (local authentication)</option> 186 <option [value]="null" i18n>None (local authentication)</option>
193 <option *ngFor="let authPlugin of getAuthPlugins()" [value]="authPlugin">{{ authPlugin }}</option> 187 <option *ngFor="let authPlugin of getAuthPlugins()" [value]="authPlugin">{{ authPlugin }}</option>
194 </select> 188 </select>
189 </div>
195 </div> 190 </div>
196 </div>
197 191
198 <div class="form-group"> 192 <div class="form-group">
199 <my-peertube-checkbox 193 <my-peertube-checkbox
200 inputName="byPassAutoBlock" formControlName="byPassAutoBlock" 194 inputName="byPassAutoBlock" formControlName="byPassAutoBlock"
201 i18n-labelText labelText="Doesn't need review before a video goes public" 195 i18n-labelText labelText="Doesn't need review before a video goes public"
202 ></my-peertube-checkbox> 196 ></my-peertube-checkbox>
203 </div> 197 </div>
204 198
205 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> 199 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
206 </form> 200 </form>
207 201
208 <div *ngIf="isInBigView()" class="col-7"> 202 <div class="d-none d-xxl-block col-7">
209 <ng-template *ngTemplateOutlet="dashboard"></ng-template> 203 <ng-template *ngTemplateOutlet="dashboard"></ng-template>
204 </div>
210 </div> 205 </div>
211
212 </div> 206 </div>
213</div> 207</div>
214 208
215 209
216<div *ngIf="!isCreation() && user && user.pluginAuth === null" class="form-row mt-4"> <!-- danger zone grid --> 210<div *ngIf="!isCreation() && user && user.pluginAuth === null" class="row mt-4"> <!-- danger zone grid -->
217 <div class="form-group col-12 col-lg-4 col-xl-3"> 211 <div class="col-12 col-lg-4 col-xl-3">
218 <div class="anchor" id="danger"></div> <!-- danger zone anchor --> 212 <div class="anchor" id="danger"></div> <!-- danger zone anchor -->
219 <div i18n class="account-title account-title-danger">DANGER ZONE</div> 213 <div i18n class="account-title account-title-danger">DANGER ZONE</div>
220 </div> 214 </div>
221 215
222 <div class="form-group col-12 col-lg-8 col-xl-9" [ngClass]="{ 'form-row': isInBigView() }"> 216 <div class="col-12 col-lg-8 col-xl-9">
223 217
224 <div class="danger-zone"> 218 <div class="danger-zone">
225 <div class="form-group reset-password-email"> 219 <div class="form-group reset-password-email">
diff --git a/client/src/app/+admin/overview/users/user-edit/user-edit.component.scss b/client/src/app/+admin/overview/users/user-edit/user-edit.component.scss
index d7932154b..254286ae3 100644
--- a/client/src/app/+admin/overview/users/user-edit/user-edit.component.scss
+++ b/client/src/app/+admin/overview/users/user-edit/user-edit.component.scss
@@ -4,11 +4,6 @@
4 4
5$form-base-input-width: 340px; 5$form-base-input-width: 340px;
6 6
7label {
8 font-weight: $font-regular;
9 font-size: 100%;
10}
11
12.account-title { 7.account-title {
13 @include settings-big-title; 8 @include settings-big-title;
14 9
@@ -22,7 +17,7 @@ input:not([type=submit]) {
22 display: block; 17 display: block;
23} 18}
24 19
25my-input-toggle-hidden { 20my-input-text {
26 @include responsive-width($form-base-input-width); 21 @include responsive-width($form-base-input-width);
27 22
28 display: block; 23 display: block;
diff --git a/client/src/app/+admin/overview/users/user-edit/user-edit.ts b/client/src/app/+admin/overview/users/user-edit/user-edit.ts
index 069b62a53..395d07423 100644
--- a/client/src/app/+admin/overview/users/user-edit/user-edit.ts
+++ b/client/src/app/+admin/overview/users/user-edit/user-edit.ts
@@ -46,10 +46,6 @@ export abstract class UserEdit extends FormReactive implements OnInit {
46 .concat(this.serverConfig.plugin.registeredExternalAuths.map(p => p.npmName)) 46 .concat(this.serverConfig.plugin.registeredExternalAuths.map(p => p.npmName))
47 } 47 }
48 48
49 isInBigView () {
50 return this.screenService.getWindowInnerWidth() > 1600
51 }
52
53 buildRoles () { 49 buildRoles () {
54 const authUser = this.auth.getUser() 50 const authUser = this.auth.getUser()
55 51
diff --git a/client/src/app/+admin/overview/users/user-edit/user-password.component.html b/client/src/app/+admin/overview/users/user-edit/user-password.component.html
index 1238d1839..13f57024b 100644
--- a/client/src/app/+admin/overview/users/user-edit/user-password.component.html
+++ b/client/src/app/+admin/overview/users/user-edit/user-password.component.html
@@ -1,20 +1,17 @@
1<form role="form" (ngSubmit)="formValidated()" [formGroup]="form"> 1<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
2 <div class="form-group">
3 2
4 <div class="input-group"> 3 <div class="input-group">
5 <input id="password" [attr.type]="showPassword ? 'text' : 'password'" class="form-control" 4 <input id="password" [attr.type]="showPassword ? 'text' : 'password'" class="form-control"
6 formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" 5 formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
7 > 6 >
8 <div class="input-group-append"> 7 <button class="btn btn-sm btn-outline-secondary" (click)="togglePasswordVisibility()" type="button">
9 <button class="btn btn-sm btn-outline-secondary" (click)="togglePasswordVisibility()" type="button"> 8 <ng-container *ngIf="!showPassword" i18n>Show</ng-container>
10 <ng-container *ngIf="!showPassword" i18n>Show</ng-container> 9 <ng-container *ngIf="!!showPassword" i18n>Hide</ng-container>
11 <ng-container *ngIf="!!showPassword" i18n>Hide</ng-container> 10 </button>
12 </button> 11 </div>
13 </div> 12
14 </div> 13 <div *ngIf="formErrors.password" class="form-error">
15 <div *ngIf="formErrors.password" class="form-error"> 14 {{ formErrors.password }}
16 {{ formErrors.password }}
17 </div>
18 </div> 15 </div>
19 16
20 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> 17 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
diff --git a/client/src/app/+admin/overview/users/user-edit/user-password.component.scss b/client/src/app/+admin/overview/users/user-edit/user-password.component.scss
index acb680682..54f782086 100644
--- a/client/src/app/+admin/overview/users/user-edit/user-password.component.scss
+++ b/client/src/app/+admin/overview/users/user-edit/user-password.component.scss
@@ -1,13 +1,9 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4input:not([type=submit]):not([type=checkbox]) { 4input[type=text],
5input[type=password] {
5 @include peertube-input-text(340px); 6 @include peertube-input-text(340px);
6
7 display: block;
8 border-top-right-radius: 0;
9 border-bottom-right-radius: 0;
10 border-right: 0;
11} 7}
12 8
13input[type=submit] { 9input[type=submit] {
@@ -17,7 +13,3 @@ input[type=submit] {
17 13
18 margin-top: 10px; 14 margin-top: 10px;
19} 15}
20
21.input-group-append {
22 height: 30px;
23}
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..4da46dc7b 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"
@@ -26,7 +26,7 @@
26 </a> 26 </a>
27 </div> 27 </div>
28 28
29 <div class="ml-auto"> 29 <div class="ms-auto">
30 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter> 30 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter>
31 </div> 31 </div>
32 32
@@ -90,7 +90,7 @@
90 </my-user-moderation-dropdown> 90 </my-user-moderation-dropdown>
91 </td> 91 </td>
92 92
93 <td *ngIf="isSelected('username')"> 93 <td *ngIf="isSelected('username')" class="cell-username">
94 <a i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/a/' + user.username ]"> 94 <a i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/a/' + user.username ]">
95 <div class="chip two-lines"> 95 <div class="chip two-lines">
96 <my-actor-avatar [account]="user?.account" size="32"></my-actor-avatar> 96 <my-actor-avatar [account]="user?.account" size="32"></my-actor-avatar>
@@ -101,13 +101,13 @@
101 </div> 101 </div>
102 </a> 102 </a>
103 103
104 <div *ngIf="user.accountMutedStatus.mutedByInstance" class="badges-username badge badge-red" i18n>Muted</div> 104 <div *ngIf="user.accountMutedStatus.mutedByInstance" class="pt-badge badge-red" i18n>Muted</div>
105 <div *ngIf="user.blocked" class="badges-username badge badge-red" i18n>Banned</div> 105 <div *ngIf="user.blocked" class="pt-badge badge-red" i18n>Banned</div>
106 </td> 106 </td>
107 107
108 <td *ngIf="isSelected('role')"> 108 <td *ngIf="isSelected('role')">
109 <span *ngIf="user.blocked" class="badge badge-banned" i18n-title title="The user was banned">{{ user.roleLabel }}</span> 109 <span *ngIf="user.blocked" class="pt-badge badge-banned" i18n-title title="The user was banned">{{ user.roleLabel }}</span>
110 <span *ngIf="!user.blocked" class="badge" [ngClass]="getRoleClass(user.role)">{{ user.roleLabel }}</span> 110 <span *ngIf="!user.blocked" class="pt-badge" [ngClass]="getRoleClass(user.role)">{{ user.roleLabel }}</span>
111 </td> 111 </td>
112 112
113 <td *ngIf="isSelected('email')" [title]="user.email"> 113 <td *ngIf="isSelected('email')" [title]="user.email">
@@ -139,7 +139,7 @@
139 139
140 <td *ngIf="isSelected('quotaDaily')"> 140 <td *ngIf="isSelected('quotaDaily')">
141 <div class="progress" i18n-title title="Total daily video quota"> 141 <div class="progress" i18n-title title="Total daily video quota">
142 <div class="progress-bar secondary" role="progressbar" [style]="{ width: getUserVideoQuotaDailyPercentage(user) + '%' }" 142 <div class="progress-bar" role="progressbar" [style]="{ width: getUserVideoQuotaDailyPercentage(user) + '%' }"
143 [attr.aria-valuenow]="user.rawVideoQuotaUsedDaily" aria-valuemin="0" [attr.aria-valuemax]="user.rawVideoQuotaDaily"> 143 [attr.aria-valuenow]="user.rawVideoQuotaUsedDaily" aria-valuemin="0" [attr.aria-valuemax]="user.rawVideoQuotaDaily">
144 </div> 144 </div>
145 <span>{{ user.videoQuotaUsedDaily }}</span> 145 <span>{{ user.videoQuotaUsedDaily }}</span>
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.scss b/client/src/app/+admin/overview/users/user-list/user-list.component.scss
index 8160703f0..3c775cac5 100644
--- a/client/src/app/+admin/overview/users/user-list/user-list.component.scss
+++ b/client/src/app/+admin/overview/users/user-list/user-list.component.scss
@@ -1,5 +1,6 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3@use '~bootstrap/scss/functions' as *;
3 4
4.add-button { 5.add-button {
5 @include create-button; 6 @include create-button;
@@ -23,15 +24,8 @@ tr.banned > td {
23 font-weight: $font-semibold; 24 font-weight: $font-semibold;
24} 25}
25 26
26.badges-username { 27.cell-username .pt-badge {
27 margin-left: 15px; 28 @include margin-left(15px);
28}
29
30.user-table-primary-text .glyphicon {
31 @include margin-left(0.1rem);
32
33 font-size: 80%;
34 color: #808080;
35} 29}
36 30
37p-tableCheckbox { 31p-tableCheckbox {
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..91f42546b 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,8 +1,8 @@
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 { prepareIcu, 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'
8import { AccountMutedStatus, BlocklistService, UserBanModalComponent, UserModerationDisplayType } from '@app/shared/shared-moderation' 8import { AccountMutedStatus, BlocklistService, UserBanModalComponent, UserModerationDisplayType } from '@app/shared/shared-moderation'
@@ -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)
133 146
134 this.columns.push({ id: 'quotaDaily', label: $localize`Daily quota` }) 147 if (result) {
135 this.columns.push({ id: 'pluginAuth', label: $localize`Auth plugin` }) 148 try {
136 this.columns.push({ id: 'lastLoginDate', label: $localize`Last login` }) 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 }
160
161 saveSelectedColumns () {
162 this.peertubeLocalStorage.setItem(UserListComponent.LOCAL_STORAGE_SELECTED_COLUMNS_KEY, JSON.stringify(this.selectedColumns))
137 } 163 }
138 164
139 getIdentifier () { 165 getIdentifier () {
@@ -164,6 +190,7 @@ export class UserListComponent extends RestTable implements OnInit {
164 } 190 }
165 191
166 getUserVideoQuotaDailyPercentage (user: UserForList) { 192 getUserVideoQuotaDailyPercentage (user: UserForList) {
193 console.log(user.rawVideoQuotaUsedDaily * 100 / user.rawVideoQuotaDaily)
167 return user.rawVideoQuotaUsedDaily * 100 / user.rawVideoQuotaDaily 194 return user.rawVideoQuotaUsedDaily * 100 / user.rawVideoQuotaDaily
168 } 195 }
169 196
@@ -183,13 +210,25 @@ export class UserListComponent extends RestTable implements OnInit {
183 } 210 }
184 211
185 async unbanUsers (users: User[]) { 212 async unbanUsers (users: User[]) {
186 const res = await this.confirmService.confirm($localize`Do you really want to unban ${users.length} users?`, $localize`Unban`) 213 const res = await this.confirmService.confirm(
214 prepareIcu($localize`Do you really want to unban {count, plural, =1 {1 user} other {{count} users}}?`)(
215 { count: users.length },
216 $localize`Do you really want to unban ${users.length} users?`
217 ),
218 $localize`Unban`
219 )
220
187 if (res === false) return 221 if (res === false) return
188 222
189 this.userAdminService.unbanUsers(users) 223 this.userAdminService.unbanUsers(users)
190 .subscribe({ 224 .subscribe({
191 next: () => { 225 next: () => {
192 this.notifier.success($localize`${users.length} users unbanned.`) 226 this.notifier.success(
227 prepareIcu($localize`{count, plural, =1 {1 user unbanned.} other {{count} users unbanned.}}`)(
228 { count: users.length },
229 $localize`${users.length} users unbanned.`
230 )
231 )
193 this.reloadData() 232 this.reloadData()
194 }, 233 },
195 234
@@ -198,21 +237,28 @@ export class UserListComponent extends RestTable implements OnInit {
198 } 237 }
199 238
200 async removeUsers (users: User[]) { 239 async removeUsers (users: User[]) {
201 for (const user of users) { 240 if (users.some(u => u.username === 'root')) {
202 if (user.username === 'root') { 241 this.notifier.error($localize`You cannot delete root.`)
203 this.notifier.error($localize`You cannot delete root.`) 242 return
204 return
205 }
206 } 243 }
207 244
208 const message = $localize`If you remove these users, you will not be able to create others with the same username!` 245 const message = $localize`<p>You can't create users or channels with a username that already used by a deleted user/channel.</p>` +
246 $localize`It means the following usernames will be permanently deleted and cannot be recovered:` +
247 '<ul>' + users.map(u => '<li>' + u.username + '</li>').join('') + '</ul>'
248
209 const res = await this.confirmService.confirm(message, $localize`Delete`) 249 const res = await this.confirmService.confirm(message, $localize`Delete`)
210 if (res === false) return 250 if (res === false) return
211 251
212 this.userAdminService.removeUser(users) 252 this.userAdminService.removeUser(users)
213 .subscribe({ 253 .subscribe({
214 next: () => { 254 next: () => {
215 this.notifier.success($localize`${users.length} users deleted.`) 255 this.notifier.success(
256 prepareIcu($localize`{count, plural, =1 {1 user deleted.} other {{count} users deleted.}}`)(
257 { count: users.length },
258 $localize`${users.length} users deleted.`
259 )
260 )
261
216 this.reloadData() 262 this.reloadData()
217 }, 263 },
218 264
@@ -224,7 +270,13 @@ export class UserListComponent extends RestTable implements OnInit {
224 this.userAdminService.updateUsers(users, { emailVerified: true }) 270 this.userAdminService.updateUsers(users, { emailVerified: true })
225 .subscribe({ 271 .subscribe({
226 next: () => { 272 next: () => {
227 this.notifier.success($localize`${users.length} users email set as verified.`) 273 this.notifier.success(
274 prepareIcu($localize`{count, plural, =1 {1 user email set as verified.} other {{count} user emails set as verified.}}`)(
275 { count: users.length },
276 $localize`${users.length} users email set as verified.`
277 )
278 )
279
228 this.reloadData() 280 this.reloadData()
229 }, 281 },
230 282