diff options
17 files changed, 230 insertions, 60 deletions
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.html b/client/src/app/+admin/follows/followers-list/followers-list.component.html index f50828bb9..050fe40fb 100644 --- a/client/src/app/+admin/follows/followers-list/followers-list.component.html +++ b/client/src/app/+admin/follows/followers-list/followers-list.component.html | |||
@@ -41,8 +41,12 @@ | |||
41 | </a> | 41 | </a> |
42 | </td> | 42 | </td> |
43 | 43 | ||
44 | <td *ngIf="follow.state === 'accepted'" i18n>Accepted</td> | 44 | <td *ngIf="follow.state === 'accepted'"> |
45 | <td *ngIf="follow.state === 'pending'" i18n>Pending</td> | 45 | <span class="badge badge-green" i18n>Accepted</span> |
46 | </td> | ||
47 | <td *ngIf="follow.state === 'pending'"> | ||
48 | <span class="badge badge-yellow" i18n>Pending</span> | ||
49 | </td> | ||
46 | 50 | ||
47 | <td>{{ follow.score }}</td> | 51 | <td>{{ follow.score }}</td> |
48 | <td>{{ follow.createdAt | date: 'short' }}</td> | 52 | <td>{{ follow.createdAt | date: 'short' }}</td> |
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.html b/client/src/app/+admin/follows/following-list/following-list.component.html index 7d1a3d7f3..9dead2557 100644 --- a/client/src/app/+admin/follows/following-list/following-list.component.html +++ b/client/src/app/+admin/follows/following-list/following-list.component.html | |||
@@ -45,8 +45,12 @@ | |||
45 | </a> | 45 | </a> |
46 | </td> | 46 | </td> |
47 | 47 | ||
48 | <td *ngIf="follow.state === 'accepted'" i18n>Accepted</td> | 48 | <td *ngIf="follow.state === 'accepted'"> |
49 | <td *ngIf="follow.state === 'pending'" i18n>Pending</td> | 49 | <span class="badge badge-green" i18n>Accepted</span> |
50 | </td> | ||
51 | <td *ngIf="follow.state === 'pending'"> | ||
52 | <span class="badge badge-yellow" i18n>Pending</span> | ||
53 | </td> | ||
50 | 54 | ||
51 | <td>{{ follow.createdAt | date: 'short' }}</td> | 55 | <td>{{ follow.createdAt | date: 'short' }}</td> |
52 | <td> | 56 | <td> |
diff --git a/client/src/app/+admin/follows/follows.component.scss b/client/src/app/+admin/follows/follows.component.scss index 0cffcb555..33ff17539 100644 --- a/client/src/app/+admin/follows/follows.component.scss +++ b/client/src/app/+admin/follows/follows.component.scss | |||
@@ -4,3 +4,7 @@ | |||
4 | flex-grow: 0; | 4 | flex-grow: 0; |
5 | margin-right: 30px; | 5 | margin-right: 30px; |
6 | } | 6 | } |
7 | |||
8 | .badge { | ||
9 | @include table-badge; | ||
10 | } | ||
diff --git a/client/src/app/+admin/system/jobs/jobs.component.html b/client/src/app/+admin/system/jobs/jobs.component.html index 596117ab7..185fae220 100644 --- a/client/src/app/+admin/system/jobs/jobs.component.html +++ b/client/src/app/+admin/system/jobs/jobs.component.html | |||
@@ -26,10 +26,10 @@ | |||
26 | <ng-template pTemplate="header"> | 26 | <ng-template pTemplate="header"> |
27 | <tr> | 27 | <tr> |
28 | <th style="width: 40px"></th> | 28 | <th style="width: 40px"></th> |
29 | <th class="job-id" i18n>ID</th> | 29 | <th style="width: 100%" class="job-id" i18n>ID</th> |
30 | <th class="job-type" i18n>Type</th> | 30 | <th style="width: 200px" class="job-type" i18n>Type</th> |
31 | <th class="job-date" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> | 31 | <th style="width: 150px" class="job-date" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> |
32 | <th class="job-state" i18n>State</th> | 32 | <th style="width: 150px" class="job-state" i18n>State</th> |
33 | </tr> | 33 | </tr> |
34 | </ng-template> | 34 | </ng-template> |
35 | 35 | ||
@@ -43,7 +43,7 @@ | |||
43 | 43 | ||
44 | <td class="job-id" [title]="job.id">{{ job.id }}</td> | 44 | <td class="job-id" [title]="job.id">{{ job.id }}</td> |
45 | <td class="job-type">{{ job.type }}</td> | 45 | <td class="job-type">{{ job.type }}</td> |
46 | <td class="job-date">{{ job.createdAt }}</td> | 46 | <td class="job-date">{{ job.createdAt | date: 'short' }}</td> |
47 | <td class="job-state" *ngIf="job.state === 'delayed'" class="text-muted"><span class="glyphicon glyphicon-repeat"></span> <span i18n>Delayed</span></td> | 47 | <td class="job-state" *ngIf="job.state === 'delayed'" class="text-muted"><span class="glyphicon glyphicon-repeat"></span> <span i18n>Delayed</span></td> |
48 | <td class="job-state" *ngIf="job.state === 'waiting'" class="text-warning"><span class="glyphicon glyphicon-hourglass"></span> <span i18n>Will start soon...</span></td> | 48 | <td class="job-state" *ngIf="job.state === 'waiting'" class="text-warning"><span class="glyphicon glyphicon-hourglass"></span> <span i18n>Will start soon...</span></td> |
49 | <td class="job-state" *ngIf="job.state === 'active'" class="text-warning"><span class="glyphicon glyphicon-cog"></span> <span i18n>Running...</span></td> | 49 | <td class="job-state" *ngIf="job.state === 'active'" class="text-warning"><span class="glyphicon glyphicon-cog"></span> <span i18n>Running...</span></td> |
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') { |
diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html index 0727f90e8..9b5f2dd2f 100644 --- a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html +++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html | |||
@@ -6,7 +6,7 @@ | |||
6 | </a> | 6 | </a> |
7 | 7 | ||
8 | <div class="peertube-select-container peertube-select-button ml-2"> | 8 | <div class="peertube-select-container peertube-select-button ml-2"> |
9 | <select [(ngModel)]="notificationSortType" (ngModelChange)="onNotificationSortTypeChanged()" class="form-control"> | 9 | <select [(ngModel)]="notificationSortType" class="form-control"> |
10 | <option value="undefined" disabled>Sort by</option> | 10 | <option value="undefined" disabled>Sort by</option> |
11 | <option value="created" i18n>Newest first</option> | 11 | <option value="created" i18n>Newest first</option> |
12 | <option value="unread-created" i18n>Unread first</option> | 12 | <option value="unread-created" i18n>Unread first</option> |
diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts index 03b91e050..8a51319fe 100644 --- a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts +++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts | |||
@@ -17,6 +17,4 @@ export class MyAccountNotificationsComponent { | |||
17 | hasUnreadNotifications () { | 17 | hasUnreadNotifications () { |
18 | return this.userNotification.notifications.filter(n => n.read === false).length !== 0 | 18 | return this.userNotification.notifications.filter(n => n.read === false).length !== 0 |
19 | } | 19 | } |
20 | |||
21 | onNotificationSortTypeChanged () {} | ||
22 | } | 20 | } |
diff --git a/client/src/app/core/users/user.service.ts b/client/src/app/core/users/user.service.ts index 2c817d45e..5f9300bec 100644 --- a/client/src/app/core/users/user.service.ts +++ b/client/src/app/core/users/user.service.ts | |||
@@ -374,13 +374,23 @@ export class UserService { | |||
374 | private formatUser (user: UserServerModel) { | 374 | private formatUser (user: UserServerModel) { |
375 | let videoQuota | 375 | let videoQuota |
376 | if (user.videoQuota === -1) { | 376 | if (user.videoQuota === -1) { |
377 | videoQuota = this.i18n('Unlimited') | 377 | videoQuota = '∞' |
378 | } else { | 378 | } else { |
379 | videoQuota = this.bytesPipe.transform(user.videoQuota, 0) | 379 | videoQuota = this.bytesPipe.transform(user.videoQuota, 0) |
380 | } | 380 | } |
381 | 381 | ||
382 | const videoQuotaUsed = this.bytesPipe.transform(user.videoQuotaUsed, 0) | 382 | const videoQuotaUsed = this.bytesPipe.transform(user.videoQuotaUsed, 0) |
383 | 383 | ||
384 | let videoQuotaDaily | ||
385 | let videoQuotaUsedDaily | ||
386 | if (user.videoQuotaDaily === -1) { | ||
387 | videoQuotaDaily = '∞' | ||
388 | videoQuotaUsedDaily = this.bytesPipe.transform(0, 0) | ||
389 | } else { | ||
390 | videoQuotaDaily = this.bytesPipe.transform(user.videoQuotaDaily, 0) | ||
391 | videoQuotaUsedDaily = this.bytesPipe.transform(user.videoQuotaUsedDaily || 0, 0) | ||
392 | } | ||
393 | |||
384 | const roleLabels: { [ id in UserRole ]: string } = { | 394 | const roleLabels: { [ id in UserRole ]: string } = { |
385 | [UserRole.USER]: this.i18n('User'), | 395 | [UserRole.USER]: this.i18n('User'), |
386 | [UserRole.ADMINISTRATOR]: this.i18n('Administrator'), | 396 | [UserRole.ADMINISTRATOR]: this.i18n('Administrator'), |
@@ -390,7 +400,13 @@ export class UserService { | |||
390 | return Object.assign(user, { | 400 | return Object.assign(user, { |
391 | roleLabel: roleLabels[user.role], | 401 | roleLabel: roleLabels[user.role], |
392 | videoQuota, | 402 | videoQuota, |
393 | videoQuotaUsed | 403 | videoQuotaUsed, |
404 | rawVideoQuota: user.videoQuota, | ||
405 | rawVideoQuotaUsed: user.videoQuotaUsed, | ||
406 | videoQuotaDaily, | ||
407 | videoQuotaUsedDaily, | ||
408 | rawVideoQuotaDaily: user.videoQuotaDaily, | ||
409 | rawVideoQuotaUsedDaily: user.videoQuotaUsedDaily | ||
394 | }) | 410 | }) |
395 | } | 411 | } |
396 | } | 412 | } |
diff --git a/client/src/app/shared/shared-icons/global-icon.component.ts b/client/src/app/shared/shared-icons/global-icon.component.ts index 66adb7579..c58ef29fa 100644 --- a/client/src/app/shared/shared-icons/global-icon.component.ts +++ b/client/src/app/shared/shared-icons/global-icon.component.ts | |||
@@ -64,7 +64,8 @@ const icons = { | |||
64 | 'go': require('!!raw-loader?!../../../assets/images/feather/arrow-up-right.svg').default, | 64 | 'go': require('!!raw-loader?!../../../assets/images/feather/arrow-up-right.svg').default, |
65 | 'cross': require('!!raw-loader?!../../../assets/images/feather/x.svg').default, | 65 | 'cross': require('!!raw-loader?!../../../assets/images/feather/x.svg').default, |
66 | 'tick': require('!!raw-loader?!../../../assets/images/feather/check.svg').default, | 66 | 'tick': require('!!raw-loader?!../../../assets/images/feather/check.svg').default, |
67 | 'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default | 67 | 'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default, |
68 | 'columns': require('!!raw-loader?!../../../assets/images/feather/columns.svg').default | ||
68 | } | 69 | } |
69 | 70 | ||
70 | export type GlobalIconName = keyof typeof icons | 71 | export type GlobalIconName = keyof typeof icons |
diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts index 5483e305f..64dcf638a 100644 --- a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts +++ b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts | |||
@@ -43,7 +43,8 @@ export class VideoChannelService { | |||
43 | listAccountVideoChannels ( | 43 | listAccountVideoChannels ( |
44 | account: Account, | 44 | account: Account, |
45 | componentPagination?: ComponentPaginationLight, | 45 | componentPagination?: ComponentPaginationLight, |
46 | withStats = false | 46 | withStats = false, |
47 | search?: string | ||
47 | ): Observable<ResultList<VideoChannel>> { | 48 | ): Observable<ResultList<VideoChannel>> { |
48 | const pagination = componentPagination | 49 | const pagination = componentPagination |
49 | ? this.restService.componentPaginationToRestPagination(componentPagination) | 50 | ? this.restService.componentPaginationToRestPagination(componentPagination) |
@@ -53,6 +54,10 @@ export class VideoChannelService { | |||
53 | params = this.restService.addRestGetParams(params, pagination) | 54 | params = this.restService.addRestGetParams(params, pagination) |
54 | params = params.set('withStats', withStats + '') | 55 | params = params.set('withStats', withStats + '') |
55 | 56 | ||
57 | if (search) { | ||
58 | params = params.set('search', search) | ||
59 | } | ||
60 | |||
56 | const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-channels' | 61 | const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-channels' |
57 | return this.authHttp.get<ResultList<VideoChannelServer>>(url, { params }) | 62 | return this.authHttp.get<ResultList<VideoChannelServer>>(url, { params }) |
58 | .pipe( | 63 | .pipe( |
diff --git a/client/src/assets/images/feather/columns.svg b/client/src/assets/images/feather/columns.svg new file mode 100644 index 000000000..d264b557b --- /dev/null +++ b/client/src/assets/images/feather/columns.svg | |||
@@ -0,0 +1 @@ | |||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-columns"><path d="M12 3h7a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-7m0-18H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h7m0-18v18"></path></svg> \ No newline at end of file | |||
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index 2de5ce7f1..75fe2ab11 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss | |||
@@ -332,9 +332,7 @@ | |||
332 | 332 | ||
333 | select { | 333 | select { |
334 | padding: 0 35px 0 12px; | 334 | padding: 0 35px 0 12px; |
335 | width: calc(100% + 2px); | ||
336 | position: relative; | 335 | position: relative; |
337 | left: 1px; | ||
338 | border: 1px solid #C6C6C6; | 336 | border: 1px solid #C6C6C6; |
339 | background: transparent none; | 337 | background: transparent none; |
340 | appearance: none; | 338 | appearance: none; |
@@ -692,7 +690,21 @@ | |||
692 | overflow: hidden; | 690 | overflow: hidden; |
693 | font-size: 0.75rem; | 691 | font-size: 0.75rem; |
694 | border-radius: 0.25rem; | 692 | border-radius: 0.25rem; |
695 | color: gray; | 693 | isolation: isolate; |
694 | position: relative; | ||
695 | |||
696 | span { | ||
697 | position: absolute; | ||
698 | color: rgb(92, 92, 92); | ||
699 | top: -1px; | ||
700 | |||
701 | &:nth-of-type(1) { | ||
702 | left: .2rem; | ||
703 | } | ||
704 | &:nth-of-type(2) { | ||
705 | right: .2rem; | ||
706 | } | ||
707 | } | ||
696 | 708 | ||
697 | .progress-bar { | 709 | .progress-bar { |
698 | color: pvar(--mainBackgroundColor); | 710 | color: pvar(--mainBackgroundColor); |
@@ -703,25 +715,11 @@ | |||
703 | text-align: center; | 715 | text-align: center; |
704 | white-space: nowrap; | 716 | white-space: nowrap; |
705 | transition: width 0.6s ease; | 717 | transition: width 0.6s ease; |
706 | isolation: isolate; | ||
707 | |||
708 | &:after { | ||
709 | content: attr(valuenow-formatted); | ||
710 | position: absolute; | ||
711 | margin-left: .2rem; | ||
712 | mix-blend-mode: screen; | ||
713 | color: gray; | ||
714 | } | ||
715 | 718 | ||
716 | &.secondary { | 719 | &.secondary { |
717 | background-color: pvar(--secondaryColor); | 720 | background-color: pvar(--secondaryColor); |
718 | } | 721 | } |
719 | } | 722 | } |
720 | |||
721 | .progress-bar + span { | ||
722 | position: relative; | ||
723 | top: -1px; | ||
724 | } | ||
725 | } | 723 | } |
726 | 724 | ||
727 | @mixin breadcrumb { | 725 | @mixin breadcrumb { |
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts index 4d8cfa340..d96998209 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts | |||
@@ -16,7 +16,7 @@ import { | |||
16 | videoPlaylistsSortValidator | 16 | videoPlaylistsSortValidator |
17 | } from '../../middlewares' | 17 | } from '../../middlewares' |
18 | import { VideoChannelModel } from '../../models/video/video-channel' | 18 | import { VideoChannelModel } from '../../models/video/video-channel' |
19 | import { videoChannelsNameWithHostValidator, videosSortValidator } from '../../middlewares/validators' | 19 | import { videoChannelsNameWithHostValidator, videosSortValidator, videoChannelsOwnSearchValidator } from '../../middlewares/validators' |
20 | import { sendUpdateActor } from '../../lib/activitypub/send' | 20 | import { sendUpdateActor } from '../../lib/activitypub/send' |
21 | import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared' | 21 | import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared' |
22 | import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' | 22 | import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' |
@@ -48,6 +48,7 @@ videoChannelRouter.get('/', | |||
48 | videoChannelsSortValidator, | 48 | videoChannelsSortValidator, |
49 | setDefaultSort, | 49 | setDefaultSort, |
50 | setDefaultPagination, | 50 | setDefaultPagination, |
51 | videoChannelsOwnSearchValidator, | ||
51 | asyncMiddleware(listVideoChannels) | 52 | asyncMiddleware(listVideoChannels) |
52 | ) | 53 | ) |
53 | 54 | ||
@@ -114,7 +115,13 @@ export { | |||
114 | 115 | ||
115 | async function listVideoChannels (req: express.Request, res: express.Response) { | 116 | async function listVideoChannels (req: express.Request, res: express.Response) { |
116 | const serverActor = await getServerActor() | 117 | const serverActor = await getServerActor() |
117 | const resultList = await VideoChannelModel.listForApi(serverActor.id, req.query.start, req.query.count, req.query.sort) | 118 | const resultList = await VideoChannelModel.listForApi({ |
119 | actorId: serverActor.id, | ||
120 | start: req.query.start, | ||
121 | count: req.query.count, | ||
122 | sort: req.query.sort, | ||
123 | search: req.query.search | ||
124 | }) | ||
118 | 125 | ||
119 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 126 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
120 | } | 127 | } |
diff --git a/server/middlewares/validators/search.ts b/server/middlewares/validators/search.ts index b4faa8894..7313bc055 100644 --- a/server/middlewares/validators/search.ts +++ b/server/middlewares/validators/search.ts | |||
@@ -41,9 +41,22 @@ const videoChannelsSearchValidator = [ | |||
41 | } | 41 | } |
42 | ] | 42 | ] |
43 | 43 | ||
44 | const videoChannelsOwnSearchValidator = [ | ||
45 | query('search').optional().not().isEmpty().withMessage('Should have a valid search'), | ||
46 | |||
47 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
48 | logger.debug('Checking video channels search query', { parameters: req.query }) | ||
49 | |||
50 | if (areValidationErrors(req, res)) return | ||
51 | |||
52 | return next() | ||
53 | } | ||
54 | ] | ||
55 | |||
44 | // --------------------------------------------------------------------------- | 56 | // --------------------------------------------------------------------------- |
45 | 57 | ||
46 | export { | 58 | export { |
59 | videosSearchValidator, | ||
47 | videoChannelsSearchValidator, | 60 | videoChannelsSearchValidator, |
48 | videosSearchValidator | 61 | videoChannelsOwnSearchValidator |
49 | } | 62 | } |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 03a3cdf81..f3401fb9c 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -54,6 +54,7 @@ export enum ScopeNames { | |||
54 | 54 | ||
55 | type AvailableForListOptions = { | 55 | type AvailableForListOptions = { |
56 | actorId: number | 56 | actorId: number |
57 | search?: string | ||
57 | } | 58 | } |
58 | 59 | ||
59 | type AvailableWithStatsOptions = { | 60 | type AvailableWithStatsOptions = { |
@@ -309,15 +310,23 @@ export class VideoChannelModel extends Model<VideoChannelModel> { | |||
309 | return VideoChannelModel.count(query) | 310 | return VideoChannelModel.count(query) |
310 | } | 311 | } |
311 | 312 | ||
312 | static listForApi (actorId: number, start: number, count: number, sort: string) { | 313 | static listForApi (parameters: { |
314 | actorId: number | ||
315 | start: number | ||
316 | count: number | ||
317 | sort: string | ||
318 | search?: string | ||
319 | }) { | ||
320 | const { actorId, search } = parameters | ||
321 | |||
313 | const query = { | 322 | const query = { |
314 | offset: start, | 323 | offset: parameters.start, |
315 | limit: count, | 324 | limit: parameters.count, |
316 | order: getSort(sort) | 325 | order: getSort(parameters.sort) |
317 | } | 326 | } |
318 | 327 | ||
319 | const scopes = { | 328 | const scopes = { |
320 | method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ] | 329 | method: [ ScopeNames.FOR_API, { actorId, search } as AvailableForListOptions ] |
321 | } | 330 | } |
322 | return VideoChannelModel | 331 | return VideoChannelModel |
323 | .scope(scopes) | 332 | .scope(scopes) |