### Features
* Add ability for admin to inject custom JavaScript/CSS
+ * Add help tooltip on some fields
### Bug fixes
import { ConfigComponent, EditCustomConfigComponent } from '@app/+admin/config'
import { ConfigService } from '@app/+admin/config/shared/config.service'
import { TabsModule } from 'ngx-bootstrap/tabs'
-import { DataTableModule } from 'primeng/components/datatable/datatable'
+import { TableModule } from 'primeng/table'
import { SharedModule } from '../shared'
import { AdminRoutingModule } from './admin-routing.module'
import { AdminComponent } from './admin.component'
imports: [
AdminRoutingModule,
TabsModule.forRoot(),
- DataTableModule,
+ TableModule,
SharedModule
],
-<p-dataTable
- [value]="followers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
- sortField="createdAt" (onLazyLoad)="loadLazy($event)"
+<p-table
+ [value]="followers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
+ [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
>
- <p-column field="id" header="ID" [style]="{ width: '60px' }"></p-column>
- <p-column field="score" header="Score"></p-column>
- <p-column field="follower.name" header="Name"></p-column>
- <p-column field="follower.host" header="Host"></p-column>
- <p-column field="state" header="State"></p-column>
- <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
-</p-dataTable>
+ <ng-template pTemplate="header">
+ <tr>
+ <th style="width: 60px">ID</th>
+ <th>Score</th>
+ <th>Name</th>
+ <th>Host</th>
+ <th>State</th>
+ <th pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
+ </tr>
+ </ng-template>
+
+ <ng-template pTemplate="body" let-follow>
+ <tr>
+ <td>{{ follow.id }}</td>
+ <td>{{ follow.score }}</td>
+ <td>{{ follow.follower.name }}</td>
+ <td>{{ follow.follower.host }}</td>
+ <td>{{ follow.state }}</td>
+ <td>{{ follow.createdAt }}</td>
+ </tr>
+ </ng-template>
+</p-table>
-import { Component } from '@angular/core'
+import { Component, OnInit } from '@angular/core'
import { NotificationsService } from 'angular2-notifications'
import { SortMeta } from 'primeng/primeng'
templateUrl: './followers-list.component.html',
styleUrls: [ './followers-list.component.scss' ]
})
-export class FollowersListComponent extends RestTable {
+export class FollowersListComponent extends RestTable implements OnInit {
followers: AccountFollow[] = []
totalRecords = 0
rowsPerPage = 10
super()
}
+ ngOnInit () {
+ this.loadSort()
+ }
+
protected loadData () {
this.followService.getFollowers(this.pagination, this.sort)
.subscribe(
-<p-dataTable
- [value]="following" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
- sortField="createdAt" (onLazyLoad)="loadLazy($event)"
+<p-table
+ [value]="following" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
+ [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
>
- <p-column field="id" header="ID" [style]="{ width: '60px' }"></p-column>
- <p-column field="following.host" header="Host"></p-column>
- <p-column field="state" header="State"></p-column>
- <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
- <p-column styleClass="action-cell">
- <ng-template pTemplate="body" let-following="rowData">
- <my-delete-button (click)="removeFollowing(following)"></my-delete-button>
- </ng-template>
- </p-column>
-</p-dataTable>
+ <ng-template pTemplate="header">
+ <tr>
+ <th style="width: 60px">ID</th>
+ <th>Host</th>
+ <th>State</th>
+ <th pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
+ <th></th>
+ </tr>
+ </ng-template>
+
+ <ng-template pTemplate="body" let-follow>
+ <tr>
+ <td>{{ follow.id }}</td>
+ <td>{{ follow.following.host }}</td>
+ <td>{{ follow.state }}</td>
+ <td>{{ follow.createdAt }}</td>
+ <td class="action-cell">
+ <my-delete-button (click)="removeFollowing(follow)"></my-delete-button>
+ </td>
+ </tr>
+ </ng-template>
+</p-table>
-import { Component } from '@angular/core'
+import { Component, OnInit } from '@angular/core'
import { NotificationsService } from 'angular2-notifications'
import { SortMeta } from 'primeng/primeng'
import { AccountFollow } from '../../../../../../shared/models/actors/follow.model'
selector: 'my-followers-list',
templateUrl: './following-list.component.html'
})
-export class FollowingListComponent extends RestTable {
+export class FollowingListComponent extends RestTable implements OnInit {
following: AccountFollow[] = []
totalRecords = 0
rowsPerPage = 10
super()
}
+ ngOnInit () {
+ this.loadSort()
+ }
+
async removeFollowing (follow: AccountFollow) {
const res = await this.confirmService.confirm(`Do you really want to unfollow ${follow.following.host}?`, 'Unfollow')
if (res === false) return
</div>
</div>
+<p-table
+ [value]="jobs" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" dataKey="id"
+ sortField="createdAt" (onLazyLoad)="loadLazy($event)"
+>
+ <ng-template pTemplate="header">
+ <tr>
+ <th style="width: 27px"></th>
+ <th style="width: 60px">ID</th>
+ <th style="width: 210px">Type</th>
+ <th style="width: 130px">State</th>
+ <th style="width: 250px" pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
+ <th style="width: 250px">Updated</th>
+ </tr>
+ </ng-template>
+ <ng-template pTemplate="body" let-expanded="expanded" let-job>
+ <tr>
+ <td>
+ <span class="expander" [pRowToggler]="job">
+ <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
+ </span>
+ </td>
+ <td>{{ job.id }}</td>
+ <td>{{ job.type }}</td>
+ <td>{{ job.state }}</td>
+ <td>{{ job.createdAt }}</td>
+ <td>{{ job.updatedAt }}</td>
+ </tr>
+ </ng-template>
+
+ <ng-template pTemplate="rowexpansion" let-job>
+ <tr>
+ <td colspan="6">
+ <pre>{{ job.data }}</pre>
+ </td>
+ </tr>
+ </ng-template>
+</p-table>
-<p-dataTable
- [value]="jobs" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
- sortField="createdAt" (onLazyLoad)="loadLazy($event)" [scrollable]="true" [virtualScroll]="true" [scrollHeight]="scrollHeight"
->
- <p-column field="id" header="ID" [style]="{ width: '60px' }"></p-column>
- <p-column field="type" header="Type" [style]="{ width: '210px' }"></p-column>
- <p-column field="state" header="State" [style]="{ width: '130px' }"></p-column>
- <p-column header="Payload">
- <ng-template pTemplate="body" let-job="rowData">
- <pre>{{ job.data }}</pre>
- </ng-template>
- </p-column>
- <p-column field="createdAt" header="Created date" [sortable]="true" [style]="{ width: '250px' }"></p-column>
- <p-column field="updatedAt" header="Updated date" [style]="{ width: '250px' }"></p-column>
-</p-dataTable>
styleUrls: [ './jobs-list.component.scss' ]
})
export class JobsListComponent extends RestTable implements OnInit {
+ private static JOB_STATE_LOCAL_STORAGE_STATE = 'jobs-list-state'
+
jobState: JobState = 'inactive'
jobStates: JobState[] = [ 'active', 'complete', 'failed', 'inactive', 'delayed' ]
jobs: Job[] = []
- totalRecords = 0
- rowsPerPage = 20
+ totalRecords: number
+ rowsPerPage = 10
sort: SortMeta = { field: 'createdAt', order: -1 }
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
scrollHeight = ''
}
ngOnInit () {
- // 270 -> headers + footer...
+ // 380 -> headers + footer...
this.scrollHeight = (viewportHeight() - 380) + 'px'
+
+ this.loadJobState()
+ this.loadSort()
}
onJobStateChanged () {
this.loadData()
+ this.saveJobState()
}
protected loadData () {
err => this.notificationsService.error('Error', err.message)
)
}
+
+ private loadJobState () {
+ const result = localStorage.getItem(JobsListComponent.JOB_STATE_LOCAL_STORAGE_STATE)
+
+ if (result) this.jobState = result as JobState
+ }
+
+ private saveJobState () {
+ localStorage.setItem(JobsListComponent.JOB_STATE_LOCAL_STORAGE_STATE, this.jobState)
+ }
}
</a>
</div>
-<p-dataTable
- [value]="users" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
- sortField="id" (onLazyLoad)="loadLazy($event)"
+<p-table
+ [value]="users" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
+ [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
>
- <p-column field="id" header="ID" [sortable]="true" [style]="{ width: '60px' }"></p-column>
- <p-column field="username" header="Username" [sortable]="true"></p-column>
- <p-column field="email" header="Email"></p-column>
- <p-column field="videoQuota" header="Video quota"></p-column>
- <p-column field="roleLabel" header="Role"></p-column>
- <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
- <p-column styleClass="action-cell">
- <ng-template pTemplate="body" let-user="rowData">
- <my-edit-button [routerLink]="getRouterUserEditLink(user)"></my-edit-button>
- <my-delete-button (click)="removeUser(user)"></my-delete-button>
- </ng-template>
- </p-column>
-</p-dataTable>
+ <ng-template pTemplate="header">
+ <tr>
+ <th pSortableColumn="username">Username <p-sortIcon field="username"></p-sortIcon></th>
+ <th>Email</th>
+ <th>Video quota</th>
+ <th>Role</th>
+ <th pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
+ <th></th>
+ </tr>
+ </ng-template>
+
+ <ng-template pTemplate="body" let-user>
+ <tr>
+ <td>{{ user.username }}</td>
+ <td>{{ user.email }}</td>
+ <td>{{ user.videoQuota }}</td>
+ <td>{{ user.roleLabel }}</td>
+ <td>{{ user.createdAt }}</td>
+ <td class="action-cell">
+ <my-edit-button [routerLink]="getRouterUserEditLink(user)"></my-edit-button>
+ <my-delete-button (click)="removeUser(user)"></my-delete-button>
+ </td>
+ </tr>
+ </ng-template>
+</p-table>
-import { Component } from '@angular/core'
+import { Component, OnInit } from '@angular/core'
import { NotificationsService } from 'angular2-notifications'
import { SortMeta } from 'primeng/components/common/sortmeta'
templateUrl: './user-list.component.html',
styleUrls: [ './user-list.component.scss' ]
})
-export class UserListComponent extends RestTable {
+export class UserListComponent extends RestTable implements OnInit {
users: User[] = []
totalRecords = 0
rowsPerPage = 10
- sort: SortMeta = { field: 'id', order: 1 }
+ sort: SortMeta = { field: 'createdAt', order: 1 }
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
constructor (
super()
}
+ ngOnInit () {
+ this.loadSort()
+ }
+
async removeUser (user: User) {
if (user.username === 'root') {
this.notificationsService.error('Error', 'You cannot delete root.')
<div class="admin-sub-title">Video abuses list</div>
</div>
-<p-dataTable
- [value]="videoAbuses" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
- sortField="id" (onLazyLoad)="loadLazy($event)"
+<p-table
+ [value]="videoAbuses" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
+ [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
>
- <p-column field="id" header="ID" [sortable]="true" [style]="{ width: '60px' }"></p-column>
- <p-column field="reason" header="Reason"></p-column>
- <p-column field="reporterServerHost" header="Reporter server host"></p-column>
- <p-column field="reporterUsername" header="Reporter username"></p-column>
- <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
- <p-column header="Video">
- <ng-template pTemplate="body" let-videoAbuse="rowData">
- <a [routerLink]="getRouterVideoLink(videoAbuse.videoId)" title="Go to the video">{{ videoAbuse.videoName }}</a>
- </ng-template>
- </p-column>
-</p-dataTable>
+ <ng-template pTemplate="header">
+ <tr>
+ <th>Reason</th>
+ <th>Reporter</th>
+ <th pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
+ <th>Video</th>
+ </tr>
+ </ng-template>
+
+ <ng-template pTemplate="body" let-videoAbuse>
+ <tr>
+ <td>{{ videoAbuse.reason }}</td>
+ <td>{{ videoAbuse.reporterServerHost + '@' + videoAbuse.reporterUsername }}</td>
+ <td>{{ videoAbuse.createdAt }}</td>
+ <td>
+ <a [routerLink]="getRouterVideoLink(videoAbuse.videoUUID)" title="Go to the video">{{ videoAbuse.videoName }}</a>
+ </td>
+ </tr>
+ </ng-template>
+</p-table>
videoAbuses: VideoAbuse[] = []
totalRecords = 0
rowsPerPage = 10
- sort: SortMeta = { field: 'id', order: 1 }
+ sort: SortMeta = { field: 'createdAt', order: 1 }
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
constructor (
}
ngOnInit () {
- this.loadData()
+ this.loadSort()
}
- getRouterVideoLink (videoId: number) {
- return [ '/videos', videoId ]
+ getRouterVideoLink (videoUUID: string) {
+ return [ '/videos', videoUUID ]
}
protected loadData () {
-<div class="row">
- <div class="content-padding">
- <h3>Blacklisted videos</h3>
-
- <p-dataTable
- [value]="blacklist" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
- sortField="id" (onLazyLoad)="loadLazy($event)"
- >
- <p-column field="id" header="ID" [sortable]="true"></p-column>
- <p-column field="name" header="Name" [sortable]="true"></p-column>
- <p-column field="description" header="Description"></p-column>
- <p-column field="views" header="Views" [sortable]="true"></p-column>
- <p-column field="nsfw" header="NSFW"></p-column>
- <p-column field="uuid" header="UUID" [sortable]="true"></p-column>
- <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
- <p-column header="Delete" styleClass="action-cell">
- <ng-template pTemplate="body" let-entry="rowData">
- <my-delete-button (click)="removeVideoFromBlacklist(entry)"></my-delete-button>
- </ng-template>
- </p-column>
- </p-dataTable>
- </div>
+<div class="admin-sub-header">
+ <div class="admin-sub-title">Blacklisted videos</div>
</div>
+
+<p-table
+ [value]="blacklist" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
+ [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
+>
+ <ng-template pTemplate="header">
+ <tr>
+ <th pSortableColumn="name">Name <p-sortIcon field="name"></p-sortIcon></th>
+ <th>Description</th>
+ <th pSortableColumn="views">Views <p-sortIcon field="views"></p-sortIcon></th>
+ <th>NSFW</th>
+ <th>UUID</th>
+ <th pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
+ <th></th>
+ </tr>
+ </ng-template>
+
+ <ng-template pTemplate="body" let-videoBlacklist>
+ <tr>
+ <td>{{ videoBlacklist.name }}</td>
+ <td>{{ videoBlacklist.description }}</td>
+ <td>{{ videoBlacklist.views }}</td>
+ <td>{{ videoBlacklist.nsfw }}</td>
+ <td>{{ videoBlacklist.uuid }}</td>
+ <td>{{ videoBlacklist.createdAt }}</td>
+ <td class="action-cell">
+ <my-delete-button label="Unblacklist" (click)="removeVideoFromBlacklist(videoBlacklist)"></my-delete-button>
+ </td>
+ </tr>
+ </ng-template>
+</p-table>
+
blacklist: BlacklistedVideo[] = []
totalRecords = 0
rowsPerPage = 10
- sort: SortMeta = { field: 'id', order: 1 }
+ sort: SortMeta = { field: 'createdAt', order: 1 }
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
constructor (
}
ngOnInit () {
- this.loadData()
+ this.loadSort()
}
async removeVideoFromBlacklist (entry: BlacklistedVideo) {
- const confirmMessage = 'Do you really want to remove this video from the blacklist ? It will be available again in the video list.'
+ const confirmMessage = 'Do you really want to remove this video from the blacklist ? It will be available again in the videos list.'
- const res = await this.confirmService.confirm(confirmMessage, 'Remove')
+ const res = await this.confirmService.confirm(confirmMessage, 'Unblacklist')
if (res === false) return
this.videoBlacklistService.removeVideoFromBlacklist(entry.videoId).subscribe(
<span class="action-button action-button-delete" >
<span class="icon icon-delete-grey"></span>
- Delete
+ {{ label }}
</span>
-import { Component } from '@angular/core'
+import { Component, Input } from '@angular/core'
@Component({
selector: 'my-delete-button',
})
export class DeleteButtonComponent {
+ @Input() label = 'Delete'
}
import { RestPagination } from './rest-pagination'
export abstract class RestTable {
+
abstract totalRecords: number
abstract rowsPerPage: number
abstract sort: SortMeta
abstract pagination: RestPagination
+ private sortLocalStorageKey = 'rest-table-sort-' + this.constructor.name
+
protected abstract loadData (): void
+ loadSort () {
+ const result = localStorage.getItem(this.sortLocalStorageKey)
+
+ if (result) {
+ try {
+ this.sort = JSON.parse(result)
+ } catch (err) {
+ console.error('Cannot load sort of local storage key ' + this.sortLocalStorageKey, err)
+ }
+ }
+ }
+
loadLazy (event: LazyLoadEvent) {
this.sort = {
order: event.sortOrder,
}
this.loadData()
+ this.saveSort()
+ }
+
+ saveSort () {
+ localStorage.setItem(this.sortLocalStorageKey, JSON.stringify(this.sort))
}
}
}
// ngprime data table customizations
-p-datatable {
+p-table {
font-size: 15px !important;
- .ui-datatable-scrollable-header {
- background-color: #fff !important;
- }
-
- .ui-widget-content {
- border: none !important;
- }
-
- .ui-datatable-virtual-table {
- border-top: none !important;
- }
-
td {
border: 1px solid #E5E5E5 !important;
padding-left: 15px !important;
tr {
background-color: #fff !important;
height: 46px;
+ }
- &:hover {
- background-color: #f0f0f0 !important;
- }
+ .ui-table-tbody {
+ tr {
+ &:hover {
+ background-color: #f0f0f0 !important;
+ }
- &:not(:hover) {
- .action-cell * {
- display: none !important;
+ &:not(:hover) {
+ .action-cell * {
+ display: none !important;
+ }
}
- }
- &:first-child td {
- border-top: none !important;
+ &:first-child td {
+ border-top: none !important;
+ }
+
+ &:last-child td {
+ border-bottom: none !important;
+ }
}
- &:last-child td {
- border-bottom: none !important;
+ .expander {
+ cursor: pointer;
+ position: relative;
+ top: 1px;
}
}
}
}
- &.ui-state-active {
+ &.ui-state-highlight {
background-color: #fff !important;
.fa {