From bc99dfe54e093e69ba8fd06d36b36fbbda3f45de Mon Sep 17 00:00:00 2001 From: Rigel Kent Date: Wed, 15 Jul 2020 11:17:03 +0200 Subject: [PATCH] variable columns for users list, more columns possible, badge display for statuses --- .../followers-list.component.html | 8 +- .../following-list.component.html | 8 +- .../app/+admin/follows/follows.component.scss | 4 + .../+admin/system/jobs/jobs.component.html | 10 +-- .../users/user-list/user-list.component.html | 81 ++++++++++++++----- .../users/user-list/user-list.component.scss | 19 +++++ .../users/user-list/user-list.component.ts | 48 ++++++++++- .../my-account-notifications.component.html | 2 +- .../my-account-notifications.component.ts | 2 - client/src/app/core/users/user.service.ts | 20 ++++- .../shared-icons/global-icon.component.ts | 3 +- .../video-channel/video-channel.service.ts | 7 +- client/src/assets/images/feather/columns.svg | 1 + client/src/sass/include/_mixins.scss | 32 ++++---- server/controllers/api/video-channel.ts | 11 ++- server/middlewares/validators/search.ts | 15 +++- server/models/video/video-channel.ts | 19 +++-- 17 files changed, 230 insertions(+), 60 deletions(-) create mode 100644 client/src/assets/images/feather/columns.svg 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 @@ - Accepted - Pending + + Accepted + + + Pending + {{ follow.score }} {{ follow.createdAt | date: 'short' }} 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 @@ - Accepted - Pending + + Accepted + + + Pending + {{ follow.createdAt | date: 'short' }} 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 @@ flex-grow: 0; margin-right: 30px; } + +.badge { + @include table-badge; +} 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 @@ - ID - Type - Created - State + ID + Type + Created + State @@ -43,7 +43,7 @@ {{ job.id }} {{ job.type }} - {{ job.createdAt }} + {{ job.createdAt | date: 'short' }} Delayed Will start soon... Running... 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 @@ - Username - Email - Video quota - Role - Auth plugin - Created - + {{ getColumn('username').label }} + {{ getColumn('email').label }} + {{ getColumn('quota').label }} + {{ getColumn('quotaDaily').label }} + {{ getColumn('role').label }} + {{ getColumn('pluginAuth').label }} + {{ getColumn('createdAt').label }} + {{ getColumn('lastLoginDate').label }} + +
+ + + +
+ - + @@ -73,7 +95,7 @@ - +
Avatar
- - - {{ user.account.displayName }} - + {{ user.account.displayName }} {{ user.username }}
- {{ user.email }} + + {{ user.email }} + @@ -106,14 +127,38 @@
- {{ user.videoQuotaUsed }} / {{ user.videoQuota }} - {{ user.roleLabel }} + +
+
+
+ {{ user.videoQuotaUsed }} + {{ user.videoQuota }} +
+ - + +
+
+
+ {{ user.videoQuotaUsedDaily }} + {{ user.videoQuotaDaily }} +
+ + + + {{ user.roleLabel }} + {{ user.roleLabel }} + + + {{ user.pluginAuth }} - {{ user.createdAt | date: 'short' }} + {{ user.createdAt | date: 'short' }} + + {{ user.lastLoginDate | date: 'short' }} td { background-color: lighten($color: $red, $amount: 40) !important; } +.table-email { + @include disable-default-a-behaviour; + color: pvar(--mainForegroundColor); +} + .banned-info { font-style: italic; } @@ -36,10 +41,24 @@ p-tableCheckbox { top: -2.5px; } +my-global-icon { + width: 18px; +} + .chip { @include chip; } +.badge { + @include table-badge; +} + +.progress { + @include progressbar; + width: auto; + max-width: 100%; +} + .input-group { @include peertube-input-group(300px); 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 import { Actor, DropdownAction } from '@app/shared/shared-main' import { UserBanModalComponent } from '@app/shared/shared-moderation' import { I18n } from '@ngx-translate/i18n-polyfill' -import { ServerConfig, User } from '@shared/models' +import { ServerConfig, User, UserRole } from '@shared/models' import { Params, Router, ActivatedRoute } from '@angular/router' @Component({ @@ -19,9 +19,12 @@ export class UserListComponent extends RestTable implements OnInit { totalRecords = 0 sort: SortMeta = { field: 'createdAt', order: 1 } pagination: RestPagination = { count: this.rowsPerPage, start: 0 } + highlightBannedUsers = false selectedUsers: User[] = [] bulkUserActions: DropdownAction[][] = [] + columns: { key: string, label: string }[] + _selectedColumns: { key: string, label: string }[] private serverConfig: ServerConfig @@ -46,6 +49,14 @@ export class UserListComponent extends RestTable implements OnInit { return this.serverConfig.signup.requiresEmailVerification } + get selectedColumns () { + return this._selectedColumns + } + + set selectedColumns (val) { + this._selectedColumns = val + } + ngOnInit () { this.serverConfig = this.serverService.getTmpConfig() this.serverService.getConfig() @@ -92,12 +103,47 @@ export class UserListComponent extends RestTable implements OnInit { } ] ] + + this.columns = [ + { key: 'username', label: 'Username' }, + { key: 'email', label: 'Email' }, + { key: 'quota', label: 'Video quota' }, + { key: 'role', label: 'Role' }, + { key: 'createdAt', label: 'Created' } + ] + this.selectedColumns = [...this.columns] + this.columns.push({ key: 'quotaDaily', label: 'Daily quota' }) + this.columns.push({ key: 'pluginAuth', label: 'Auth plugin' }) + this.columns.push({ key: 'lastLoginDate', label: 'Last login' }) } getIdentifier () { return 'UserListComponent' } + getRoleClass (role: UserRole) { + switch (role) { + case UserRole.ADMINISTRATOR: + return 'badge-purple' + case UserRole.MODERATOR: + return 'badge-blue' + default: + return 'badge-yellow' + } + } + + getColumn (key: string) { + return this.selectedColumns.find((col: any) => col.key === key) + } + + getUserVideoQuotaPercentage (user: User & { rawVideoQuota: number, rawVideoQuotaUsed: number}) { + return user.rawVideoQuotaUsed * 100 / user.rawVideoQuota + } + + getUserVideoQuotaDailyPercentage (user: User & { rawVideoQuotaDaily: number, rawVideoQuotaUsedDaily: number}) { + return user.rawVideoQuotaUsedDaily * 100 / user.rawVideoQuotaDaily + } + openBanUserModal (users: User[]) { for (const user of users) { 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 @@
- 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 { hasUnreadNotifications () { return this.userNotification.notifications.filter(n => n.read === false).length !== 0 } - - onNotificationSortTypeChanged () {} } 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 { private formatUser (user: UserServerModel) { let videoQuota if (user.videoQuota === -1) { - videoQuota = this.i18n('Unlimited') + videoQuota = '∞' } else { videoQuota = this.bytesPipe.transform(user.videoQuota, 0) } const videoQuotaUsed = this.bytesPipe.transform(user.videoQuotaUsed, 0) + let videoQuotaDaily + let videoQuotaUsedDaily + if (user.videoQuotaDaily === -1) { + videoQuotaDaily = '∞' + videoQuotaUsedDaily = this.bytesPipe.transform(0, 0) + } else { + videoQuotaDaily = this.bytesPipe.transform(user.videoQuotaDaily, 0) + videoQuotaUsedDaily = this.bytesPipe.transform(user.videoQuotaUsedDaily || 0, 0) + } + const roleLabels: { [ id in UserRole ]: string } = { [UserRole.USER]: this.i18n('User'), [UserRole.ADMINISTRATOR]: this.i18n('Administrator'), @@ -390,7 +400,13 @@ export class UserService { return Object.assign(user, { roleLabel: roleLabels[user.role], videoQuota, - videoQuotaUsed + videoQuotaUsed, + rawVideoQuota: user.videoQuota, + rawVideoQuotaUsed: user.videoQuotaUsed, + videoQuotaDaily, + videoQuotaUsedDaily, + rawVideoQuotaDaily: user.videoQuotaDaily, + rawVideoQuotaUsedDaily: user.videoQuotaUsedDaily }) } } 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 = { 'go': require('!!raw-loader?!../../../assets/images/feather/arrow-up-right.svg').default, 'cross': require('!!raw-loader?!../../../assets/images/feather/x.svg').default, 'tick': require('!!raw-loader?!../../../assets/images/feather/check.svg').default, - 'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default + 'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default, + 'columns': require('!!raw-loader?!../../../assets/images/feather/columns.svg').default } 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 { listAccountVideoChannels ( account: Account, componentPagination?: ComponentPaginationLight, - withStats = false + withStats = false, + search?: string ): Observable> { const pagination = componentPagination ? this.restService.componentPaginationToRestPagination(componentPagination) @@ -53,6 +54,10 @@ export class VideoChannelService { params = this.restService.addRestGetParams(params, pagination) params = params.set('withStats', withStats + '') + if (search) { + params = params.set('search', search) + } + const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-channels' return this.authHttp.get>(url, { params }) .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 @@ + \ 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 @@ select { padding: 0 35px 0 12px; - width: calc(100% + 2px); position: relative; - left: 1px; border: 1px solid #C6C6C6; background: transparent none; appearance: none; @@ -692,7 +690,21 @@ overflow: hidden; font-size: 0.75rem; border-radius: 0.25rem; - color: gray; + isolation: isolate; + position: relative; + + span { + position: absolute; + color: rgb(92, 92, 92); + top: -1px; + + &:nth-of-type(1) { + left: .2rem; + } + &:nth-of-type(2) { + right: .2rem; + } + } .progress-bar { color: pvar(--mainBackgroundColor); @@ -703,25 +715,11 @@ text-align: center; white-space: nowrap; transition: width 0.6s ease; - isolation: isolate; - - &:after { - content: attr(valuenow-formatted); - position: absolute; - margin-left: .2rem; - mix-blend-mode: screen; - color: gray; - } &.secondary { background-color: pvar(--secondaryColor); } } - - .progress-bar + span { - position: relative; - top: -1px; - } } @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 { videoPlaylistsSortValidator } from '../../middlewares' import { VideoChannelModel } from '../../models/video/video-channel' -import { videoChannelsNameWithHostValidator, videosSortValidator } from '../../middlewares/validators' +import { videoChannelsNameWithHostValidator, videosSortValidator, videoChannelsOwnSearchValidator } from '../../middlewares/validators' import { sendUpdateActor } from '../../lib/activitypub/send' import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared' import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' @@ -48,6 +48,7 @@ videoChannelRouter.get('/', videoChannelsSortValidator, setDefaultSort, setDefaultPagination, + videoChannelsOwnSearchValidator, asyncMiddleware(listVideoChannels) ) @@ -114,7 +115,13 @@ export { async function listVideoChannels (req: express.Request, res: express.Response) { const serverActor = await getServerActor() - const resultList = await VideoChannelModel.listForApi(serverActor.id, req.query.start, req.query.count, req.query.sort) + const resultList = await VideoChannelModel.listForApi({ + actorId: serverActor.id, + start: req.query.start, + count: req.query.count, + sort: req.query.sort, + search: req.query.search + }) return res.json(getFormattedObjects(resultList.data, resultList.total)) } 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 = [ } ] +const videoChannelsOwnSearchValidator = [ + query('search').optional().not().isEmpty().withMessage('Should have a valid search'), + + (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking video channels search query', { parameters: req.query }) + + if (areValidationErrors(req, res)) return + + return next() + } +] + // --------------------------------------------------------------------------- export { + videosSearchValidator, videoChannelsSearchValidator, - videosSearchValidator + videoChannelsOwnSearchValidator } 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 { type AvailableForListOptions = { actorId: number + search?: string } type AvailableWithStatsOptions = { @@ -309,15 +310,23 @@ export class VideoChannelModel extends Model { return VideoChannelModel.count(query) } - static listForApi (actorId: number, start: number, count: number, sort: string) { + static listForApi (parameters: { + actorId: number + start: number + count: number + sort: string + search?: string + }) { + const { actorId, search } = parameters + const query = { - offset: start, - limit: count, - order: getSort(sort) + offset: parameters.start, + limit: parameters.count, + order: getSort(parameters.sort) } const scopes = { - method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ] + method: [ ScopeNames.FOR_API, { actorId, search } as AvailableForListOptions ] } return VideoChannelModel .scope(scopes)