aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRigel Kent <sendmemail@rigelk.eu>2020-07-15 11:17:03 +0200
committerRigel Kent <par@rigelk.eu>2020-07-29 18:15:53 +0200
commitbc99dfe54e093e69ba8fd06d36b36fbbda3f45de (patch)
tree2c13497b77928c2593310746e3ec33333e2b4d66
parent654a188f80fc1f089aa14837084664c908fe27d2 (diff)
downloadPeerTube-bc99dfe54e093e69ba8fd06d36b36fbbda3f45de.tar.gz
PeerTube-bc99dfe54e093e69ba8fd06d36b36fbbda3f45de.tar.zst
PeerTube-bc99dfe54e093e69ba8fd06d36b36fbbda3f45de.zip
variable columns for users list, more columns possible, badge display for statuses
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.html8
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.html8
-rw-r--r--client/src/app/+admin/follows/follows.component.scss4
-rw-r--r--client/src/app/+admin/system/jobs/jobs.component.html10
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.html81
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.scss19
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.ts48
-rw-r--r--client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html2
-rw-r--r--client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts2
-rw-r--r--client/src/app/core/users/user.service.ts20
-rw-r--r--client/src/app/shared/shared-icons/global-icon.component.ts3
-rw-r--r--client/src/app/shared/shared-main/video-channel/video-channel.service.ts7
-rw-r--r--client/src/assets/images/feather/columns.svg1
-rw-r--r--client/src/sass/include/_mixins.scss32
-rw-r--r--server/controllers/api/video-channel.ts11
-rw-r--r--server/middlewares/validators/search.ts15
-rw-r--r--server/models/video/video-channel.ts19
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
44my-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
4import { Actor, DropdownAction } from '@app/shared/shared-main' 4import { Actor, DropdownAction } from '@app/shared/shared-main'
5import { UserBanModalComponent } from '@app/shared/shared-moderation' 5import { UserBanModalComponent } from '@app/shared/shared-moderation'
6import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
7import { ServerConfig, User } from '@shared/models' 7import { ServerConfig, User, UserRole } from '@shared/models'
8import { Params, Router, ActivatedRoute } from '@angular/router' 8import { 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
70export type GlobalIconName = keyof typeof icons 71export 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'
18import { VideoChannelModel } from '../../models/video/video-channel' 18import { VideoChannelModel } from '../../models/video/video-channel'
19import { videoChannelsNameWithHostValidator, videosSortValidator } from '../../middlewares/validators' 19import { videoChannelsNameWithHostValidator, videosSortValidator, videoChannelsOwnSearchValidator } from '../../middlewares/validators'
20import { sendUpdateActor } from '../../lib/activitypub/send' 20import { sendUpdateActor } from '../../lib/activitypub/send'
21import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared' 21import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
22import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' 22import { 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
115async function listVideoChannels (req: express.Request, res: express.Response) { 116async 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
44const 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
46export { 58export {
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
55type AvailableForListOptions = { 55type AvailableForListOptions = {
56 actorId: number 56 actorId: number
57 search?: string
57} 58}
58 59
59type AvailableWithStatsOptions = { 60type 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)