diff options
Diffstat (limited to 'client/src/app/+admin/overview/users')
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 | ||
7 | label { | ||
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 | ||
25 | my-input-toggle-hidden { | 20 | my-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 | ||
4 | input:not([type=submit]):not([type=checkbox]) { | 4 | input[type=text], |
5 | input[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 | ||
13 | input[type=submit] { | 9 | input[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 | ||
37 | p-tableCheckbox { | 31 | p-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 @@ | |||
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 { prepareIcu, 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' |
8 | import { AccountMutedStatus, BlocklistService, UserBanModalComponent, UserModerationDisplayType } from '@app/shared/shared-moderation' | 8 | import { 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 | }) |
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) | ||
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 | ||