From: Chocobozzz <florian.bigard@gmail.com>
Date: Thu, 14 Sep 2017 09:57:49 +0000 (+0200)
Subject: Move to HttpClient and PrimeNG data table
X-Git-Tag: v0.0.1-alpha~324
X-Git-Url: https://git.immae.eu/?a=commitdiff_plain;h=d592e0a9b2931c7c9cbedb27fb8efc9aaacad9bb;p=github%2FChocobozzz%2FPeerTube.git

Move to HttpClient and PrimeNG data table
---

diff --git a/client/package.json b/client/package.json
index d69a7b10b..caec34e44 100644
--- a/client/package.json
+++ b/client/package.json
@@ -62,7 +62,6 @@
     "json-loader": "^0.5.4",
     "ng-router-loader": "^2.0.0",
     "ng2-file-upload": "^1.1.4-2",
-    "ng2-smart-table": "1.2.1",
     "ngc-webpack": "3.2.2",
     "ngx-bootstrap": "1.9.1",
     "ngx-chips": "1.5.3",
@@ -97,6 +96,7 @@
     "add-asset-html-webpack-plugin": "^2.0.1",
     "codelyzer": "^3.0.0-beta.4",
     "extract-text-webpack-plugin": "^3.0.0",
+    "primeng": "^4.2.0",
     "purify-css": "^1.2.5",
     "purifycss-webpack": "^0.7.0",
     "standard": "^10.0.0",
diff --git a/client/src/app/+admin/friends/friend-add/friend-add.component.ts b/client/src/app/+admin/friends/friend-add/friend-add.component.ts
index 0449d26a9..02543d393 100644
--- a/client/src/app/+admin/friends/friend-add/friend-add.component.ts
+++ b/client/src/app/+admin/friends/friend-add/friend-add.component.ts
@@ -93,8 +93,9 @@ export class FriendAddComponent implements OnInit {
 
         this.friendService.makeFriends(notEmptyHosts).subscribe(
           status => {
-            this.notificationsService.success('Sucess', 'Make friends request sent!')
-            this.router.navigate([ '/admin/friends/list' ])
+            this.notificationsService.success('Success', 'Make friends request sent!')
+            // Wait requests between pods
+            setTimeout(() => this.router.navigate([ '/admin/friends/list' ]), 1000)
           },
 
           err => this.notificationsService.error('Error', err.text)
diff --git a/client/src/app/+admin/friends/friend-list/friend-list.component.html b/client/src/app/+admin/friends/friend-list/friend-list.component.html
index 7b9fff304..7887bc5e3 100644
--- a/client/src/app/+admin/friends/friend-list/friend-list.component.html
+++ b/client/src/app/+admin/friends/friend-list/friend-list.component.html
@@ -2,13 +2,24 @@
   <div class="content-padding">
     <h3>Friends list</h3>
 
-    <ng2-smart-table [settings]="tableSettings" [source]="friendsSource" (delete)="removeFriend($event)"></ng2-smart-table>
+    <p-dataTable [value]="friends">
+      <p-column field="id" header="ID"></p-column>
+      <p-column field="host" header="Host"></p-column>
+      <p-column field="email" header="Email"></p-column>
+      <p-column field="score" header="Score"></p-column>
+      <p-column field="createdAt" header="Created date"></p-column>
+      <p-column header="Delete" styleClass="action-cell">
+        <ng-template pTemplate="body" let-pod="rowData">
+          <span (click)="removeFriend(pod)" class="glyphicon glyphicon-remove glyphicon-black" title="Remove this pod"></span>
+        </ng-template>
+      </p-column>
+    </p-dataTable>
 
     <a *ngIf="hasFriends()" class="btn btn-danger pull-left" (click)="quitFriends()">
       Quit friends
     </a>
 
-    <a *ngIf="!hasFriends()" class="btn btn-success pull-right" [routerLink]="['/admin/friends/add']">
+    <a *ngIf="!hasFriends()" class="btn btn-success pull-right" [routerLink]="[ '/admin/friends/add' ]">
       Make friends
     </a>
   </div>
diff --git a/client/src/app/+admin/friends/friend-list/friend-list.component.ts b/client/src/app/+admin/friends/friend-list/friend-list.component.ts
index 822a112cc..6a8bd492c 100644
--- a/client/src/app/+admin/friends/friend-list/friend-list.component.ts
+++ b/client/src/app/+admin/friends/friend-list/friend-list.component.ts
@@ -1,71 +1,31 @@
-import { Component } from '@angular/core'
+import { Component, OnInit } from '@angular/core'
 
 import { NotificationsService } from 'angular2-notifications'
-import { ServerDataSource } from 'ng2-smart-table'
 
 import { ConfirmService } from '../../../core'
-import { Utils } from '../../../shared'
 import { FriendService } from '../shared'
 import { Pod } from '../../../../../../shared'
 
 @Component({
   selector: 'my-friend-list',
   templateUrl: './friend-list.component.html',
-  styleUrls: [ './friend-list.component.scss' ]
+  styleUrls: ['./friend-list.component.scss']
 })
-export class FriendListComponent {
-  friendsSource = null
-  tableSettings = {
-    mode: 'external',
-    attr: {
-      class: 'table-hover'
-    },
-    hideSubHeader: true,
-    actions: {
-      position: 'right',
-      add: false,
-      edit: false,
-      delete: true
-    },
-    delete: {
-      deleteButtonContent: Utils.getRowDeleteButton()
-    },
-    columns: {
-      id: {
-        title: 'ID',
-        sort: false,
-        sortDirection: 'asc'
-      },
-      host: {
-        title: 'Host',
-        sort: false
-      },
-      email: {
-        title: 'Email',
-        sort: false
-      },
-      score: {
-        title: 'Score',
-        sort: false
-      },
-      createdAt: {
-        title: 'Created Date',
-        sort: false,
-        valuePrepareFunction: Utils.dateToHuman
-      }
-    }
-  }
+export class FriendListComponent implements OnInit {
+  friends: Pod[] = []
 
   constructor (
     private notificationsService: NotificationsService,
     private confirmService: ConfirmService,
     private friendService: FriendService
-  ) {
-    this.friendsSource = this.friendService.getDataSource()
+  ) {}
+
+  ngOnInit () {
+    this.loadData()
   }
 
   hasFriends () {
-    return this.friendsSource.count() !== 0
+    return this.friends.length !== 0
   }
 
   quitFriends () {
@@ -77,32 +37,42 @@ export class FriendListComponent {
         this.friendService.quitFriends().subscribe(
           status => {
             this.notificationsService.success('Success', 'Friends left!')
-            this.friendsSource.refresh()
+            this.loadData()
           },
 
-          err => this.notificationsService.error('Error', err.text)
+          err => this.notificationsService.error('Error', err)
         )
       }
     )
   }
 
-  removeFriend ({ data }) {
+  removeFriend (friend: Pod) {
     const confirmMessage = 'Do you really want to remove this friend ? All its videos will be deleted.'
-    const friend: Pod = data
 
     this.confirmService.confirm(confirmMessage, 'Remove').subscribe(
       res => {
         if (res === false) return
 
         this.friendService.removeFriend(friend).subscribe(
-	  status => {
-	    this.notificationsService.success('Success', 'Friend removed')
-	    this.friendsSource.refresh()
-	  },
+          status => {
+            this.notificationsService.success('Success', 'Friend removed')
+            this.loadData()
+          },
 
-	  err => this.notificationsService.error('Error', err.text)
-	)
+          err => this.notificationsService.error('Error', err)
+        )
       }
     )
   }
+
+  private loadData () {
+    this.friendService.getFriends()
+                      .subscribe(
+                        resultList => {
+                          this.friends = resultList.data
+                        },
+
+                        err => this.notificationsService.error('Error', err)
+                      )
+  }
 }
diff --git a/client/src/app/+admin/friends/shared/friend.service.ts b/client/src/app/+admin/friends/shared/friend.service.ts
index 9b3ff04b1..45607e28d 100644
--- a/client/src/app/+admin/friends/shared/friend.service.ts
+++ b/client/src/app/+admin/friends/shared/friend.service.ts
@@ -1,24 +1,24 @@
 import { Injectable } from '@angular/core'
-import { Observable } from 'rxjs/Observable'
+import { HttpClient } from '@angular/common/http'
 import 'rxjs/add/operator/catch'
 import 'rxjs/add/operator/map'
 
-import { ServerDataSource } from 'ng2-smart-table'
-
-import { AuthHttp, RestExtractor, RestDataSource, ResultList } from '../../../shared'
-import { Pod } from '../../../../../../shared'
+import { RestExtractor,  } from '../../../shared'
+import { Pod, ResultList } from '../../../../../../shared'
 
 @Injectable()
 export class FriendService {
   private static BASE_FRIEND_URL = API_URL + '/api/v1/pods/'
 
   constructor (
-    private authHttp: AuthHttp,
+    private authHttp: HttpClient,
     private restExtractor: RestExtractor
   ) {}
 
-  getDataSource () {
-    return new RestDataSource(this.authHttp, FriendService.BASE_FRIEND_URL)
+  getFriends () {
+    return this.authHttp.get<ResultList<Pod>>(FriendService.BASE_FRIEND_URL)
+                        .map(res => this.restExtractor.convertResultListDateToHuman(res))
+                        .catch(res => this.restExtractor.handleError(res))
   }
 
   makeFriends (notEmptyHosts: String[]) {
@@ -28,18 +28,18 @@ export class FriendService {
 
     return this.authHttp.post(FriendService.BASE_FRIEND_URL + 'make-friends', body)
                         .map(this.restExtractor.extractDataBool)
-                        .catch((res) => this.restExtractor.handleError(res))
+                        .catch(res => this.restExtractor.handleError(res))
   }
 
   quitFriends () {
     return this.authHttp.get(FriendService.BASE_FRIEND_URL + 'quit-friends')
-                        .map(res => res.status)
-                        .catch((res) => this.restExtractor.handleError(res))
+                        .map(this.restExtractor.extractDataBool)
+                        .catch(res => this.restExtractor.handleError(res))
   }
 
   removeFriend (friend: Pod) {
     return this.authHttp.delete(FriendService.BASE_FRIEND_URL + friend.id)
                         .map(this.restExtractor.extractDataBool)
-                        .catch((res) => this.restExtractor.handleError(res))
+                        .catch(res => this.restExtractor.handleError(res))
   }
 }
diff --git a/client/src/app/+admin/request-schedulers/shared/request-schedulers.service.ts b/client/src/app/+admin/request-schedulers/shared/request-schedulers.service.ts
index e9b166f78..44d9cbc3e 100644
--- a/client/src/app/+admin/request-schedulers/shared/request-schedulers.service.ts
+++ b/client/src/app/+admin/request-schedulers/shared/request-schedulers.service.ts
@@ -1,10 +1,11 @@
 import { Injectable } from '@angular/core'
+import { HttpClient } from '@angular/common/http'
 import { Observable } from 'rxjs/Observable'
 import 'rxjs/add/operator/catch'
 import 'rxjs/add/operator/map'
 
 import { RequestSchedulerStats } from '../../../../../../shared'
-import { AuthHttp, RestExtractor } from '../../../shared'
+import { RestExtractor } from '../../../shared'
 import { RequestSchedulerStatsAttributes } from './request-schedulers-stats-attributes.model'
 
 @Injectable()
@@ -12,19 +13,18 @@ export class RequestSchedulersService {
   private static BASE_REQUEST_URL = API_URL + '/api/v1/request-schedulers/'
 
   constructor (
-    private authHttp: AuthHttp,
+    private authHttp: HttpClient,
     private restExtractor: RestExtractor
   ) {}
 
-  getStats (): Observable<RequestSchedulerStats> {
-    return this.authHttp.get(RequestSchedulersService.BASE_REQUEST_URL + 'stats')
-                        .map(this.restExtractor.extractDataGet)
-                        .map(this.buildRequestObjects)
-                        .catch((res) => this.restExtractor.handleError(res))
+  getStats () {
+    return this.authHttp.get<RequestSchedulerStats>(RequestSchedulersService.BASE_REQUEST_URL + 'stats')
+                        .map(res => this.buildRequestObjects(res))
+                        .catch(res => this.restExtractor.handleError(res))
   }
 
   private buildRequestObjects (data: RequestSchedulerStats) {
-    const requestSchedulers = {}
+    const requestSchedulers: { [ id: string ]: RequestSchedulerStatsAttributes } = {}
 
     Object.keys(data).forEach(requestSchedulerName => {
       requestSchedulers[requestSchedulerName] = new RequestSchedulerStatsAttributes(data[requestSchedulerName])
diff --git a/client/src/app/+admin/users/shared/user.service.ts b/client/src/app/+admin/users/shared/user.service.ts
index 999013bcc..a4b89bf1d 100644
--- a/client/src/app/+admin/users/shared/user.service.ts
+++ b/client/src/app/+admin/users/shared/user.service.ts
@@ -1,11 +1,14 @@
 import { Injectable } from '@angular/core'
+import { HttpClient, HttpParams } from '@angular/common/http'
+import { Observable } from 'rxjs/Observable'
 import 'rxjs/add/operator/catch'
 import 'rxjs/add/operator/map'
 
+import { SortMeta } from 'primeng/primeng'
 import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe'
 
-import { AuthHttp, RestExtractor, RestDataSource, User } from '../../../shared'
-import { UserCreate, UserUpdate } from '../../../../../../shared'
+import { RestExtractor, User, RestPagination, RestService } from '../../../shared'
+import { UserCreate, UserUpdate, ResultList } from '../../../../../../shared'
 
 @Injectable()
 export class UserService {
@@ -13,53 +16,52 @@ export class UserService {
   private bytesPipe = new BytesPipe()
 
   constructor (
-    private authHttp: AuthHttp,
+    private authHttp: HttpClient,
+    private restService: RestService,
     private restExtractor: RestExtractor
   ) {}
 
   addUser (userCreate: UserCreate) {
     return this.authHttp.post(UserService.BASE_USERS_URL, userCreate)
                         .map(this.restExtractor.extractDataBool)
-                        .catch(this.restExtractor.handleError)
+                        .catch(err => this.restExtractor.handleError(err))
   }
 
   updateUser (userId: number, userUpdate: UserUpdate) {
     return this.authHttp.put(UserService.BASE_USERS_URL + userId, userUpdate)
-               .map(this.restExtractor.extractDataBool)
-               .catch(this.restExtractor.handleError)
+                        .map(this.restExtractor.extractDataBool)
+                        .catch(err => this.restExtractor.handleError(err))
   }
 
   getUser (userId: number) {
-    return this.authHttp.get(UserService.BASE_USERS_URL + userId)
-                        .map(this.restExtractor.extractDataGet)
-                        .catch(this.restExtractor.handleError)
+    return this.authHttp.get<User>(UserService.BASE_USERS_URL + userId)
+                        .catch(err => this.restExtractor.handleError(err))
   }
 
-  getDataSource () {
-    return new RestDataSource(this.authHttp, UserService.BASE_USERS_URL, this.formatDataSource.bind(this))
+  getUsers (pagination: RestPagination, sort: SortMeta): Observable<ResultList<User>> {
+    let params = new HttpParams()
+    params = this.restService.addRestGetParams(params, pagination, sort)
+
+    return this.authHttp.get<ResultList<User>>(UserService.BASE_USERS_URL, { params })
+                        .map(res => this.restExtractor.convertResultListDateToHuman(res))
+                        .map(res => this.restExtractor.applyToResultListData(res, this.formatUser.bind(this)))
+                        .catch(err => this.restExtractor.handleError(err))
   }
 
   removeUser (user: User) {
     return this.authHttp.delete(UserService.BASE_USERS_URL + user.id)
   }
 
-  private formatDataSource (users: User[]) {
-    const newUsers = []
+  private formatUser (user: User) {
+    let videoQuota
+    if (user.videoQuota === -1) {
+      videoQuota = 'Unlimited'
+    } else {
+      videoQuota = this.bytesPipe.transform(user.videoQuota)
+    }
 
-    users.forEach(user => {
-      let videoQuota
-      if (user.videoQuota === -1) {
-        videoQuota = 'Unlimited'
-      } else {
-        videoQuota = this.bytesPipe.transform(user.videoQuota)
-      }
-
-      const newUser = Object.assign(user, {
-        videoQuota
-      })
-      newUsers.push(newUser)
+    return Object.assign(user, {
+      videoQuota
     })
-
-    return newUsers
   }
 }
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 eb5bc9d4a..2944e3cbf 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
@@ -3,10 +3,29 @@
 
     <h3>Users list</h3>
 
-    <ng2-smart-table
-      [settings]="tableSettings" [source]="usersSource"
-      (delete)="removeUser($event)" (edit)="editUser($event)"
-    ></ng2-smart-table>
+    <p-dataTable
+        [value]="users" [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="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="role" header="Role"></p-column>
+      <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
+      <p-column header="Edit" styleClass="action-cell">
+        <ng-template pTemplate="body" let-user="rowData">
+          <a [routerLink]="getRouterUserEditLink(user)" title="Edit this user">
+            <span class="glyphicon glyphicon-pencil glyphicon-black"></span>
+          </a>
+        </ng-template>
+      </p-column>
+      <p-column header="Delete" styleClass="action-cell">
+        <ng-template pTemplate="body" let-user="rowData">
+          <span (click)="removeUser(user)" class="glyphicon glyphicon-remove glyphicon-black" title="Remove this user"></span>
+        </ng-template>
+      </p-column>
+    </p-dataTable>
 
     <a class="add-user btn btn-success pull-right" [routerLink]="['/admin/users/add']">
       <span class="glyphicon glyphicon-plus"></span>
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 7187a2008..c3fa55825 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
@@ -1,82 +1,37 @@
-import { Component } from '@angular/core'
+import { Component, OnInit } from '@angular/core'
+import { SortMeta } from 'primeng/primeng'
 
 import { NotificationsService } from 'angular2-notifications'
 
 import { ConfirmService } from '../../../core'
-import { RestDataSource, User, Utils } from '../../../shared'
+import { RestTable, RestPagination, User } from '../../../shared'
 import { UserService } from '../shared'
-import { Router } from '@angular/router'
 
 @Component({
   selector: 'my-user-list',
   templateUrl: './user-list.component.html',
   styleUrls: [ './user-list.component.scss' ]
 })
-export class UserListComponent {
-  usersSource: RestDataSource = null
-  tableSettings = {
-    mode: 'external',
-    attr: {
-      class: 'table-hover'
-    },
-    hideSubHeader: true,
-    actions: {
-      position: 'right',
-      add: false,
-      edit: true,
-      delete: true
-    },
-    delete: {
-      deleteButtonContent: Utils.getRowDeleteButton()
-    },
-    edit: {
-      editButtonContent: Utils.getRowEditButton()
-    },
-    pager: {
-      display: true,
-      perPage: 10
-    },
-    columns: {
-      id: {
-        title: 'ID',
-        sortDirection: 'asc'
-      },
-      username: {
-        title: 'Username'
-      },
-      email: {
-        title: 'Email'
-      },
-      videoQuota: {
-        title: 'Video quota'
-      },
-      role: {
-        title: 'Role',
-        sort: false
-      },
-      createdAt: {
-        title: 'Created Date',
-        valuePrepareFunction: Utils.dateToHuman
-      }
-    }
-  }
+export class UserListComponent extends RestTable implements OnInit {
+  users: User[] = []
+  totalRecords = 0
+  rowsPerPage = 10
+  sort: SortMeta = { field: 'id', order: 1 }
+  pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
 
   constructor (
-    private router: Router,
     private notificationsService: NotificationsService,
     private confirmService: ConfirmService,
     private userService: UserService
   ) {
-    this.usersSource = this.userService.getDataSource()
+    super()
   }
 
-  editUser ({ data }: { data: User }) {
-    this.router.navigate([ '/admin', 'users', data.id, 'update' ])
+  ngOnInit () {
+    this.loadData()
   }
 
-  removeUser ({ data }: { data: User }) {
-    const user = data
-
+  removeUser (user: User) {
     if (user.username === 'root') {
       this.notificationsService.error('Error', 'You cannot delete root.')
       return
@@ -89,12 +44,28 @@ export class UserListComponent {
         this.userService.removeUser(user).subscribe(
           () => {
             this.notificationsService.success('Success', `User ${user.username} deleted.`)
-            this.usersSource.refresh()
+            this.loadData()
           },
 
-          err => this.notificationsService.error('Error', err.text)
+          err => this.notificationsService.error('Error', err)
         )
       }
     )
   }
+
+  getRouterUserEditLink (user: User) {
+    return [ '/admin', 'users', user.id, 'update' ]
+  }
+
+  protected loadData () {
+    this.userService.getUsers(this.pagination, this.sort)
+                    .subscribe(
+                      resultList => {
+                        this.users = resultList.data
+                        this.totalRecords = resultList.total
+                      },
+
+                      err => this.notificationsService.error('Error', err)
+                    )
+  }
 }
diff --git a/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html
index c6723a734..e73f38112 100644
--- a/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html
+++ b/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html
@@ -3,9 +3,21 @@
 
   <h3>Video abuses list</h3>
 
-  <ng2-smart-table
-    [settings]="tableSettings" [source]="videoAbusesSource"
-  ></ng2-smart-table>
+  <p-dataTable
+      [value]="videoAbuses" [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="reason" header="Reason"></p-column>
+    <p-column field="reporterPodHost" header="Reporter pod host"></p-column>
+    <p-column field="reporterUsername" header="Reporter username"></p-column>
+    <p-column header="Video" styleClass="action-cell">
+      <ng-template pTemplate="body" let-videoAbuse="rowData">
+        <a [routerLink]="getRouterVideoLink(videoAbuse.videoId)" title="Go to the video">{{ videoAbuse.videoId }}</a>
+      </ng-template>
+    </p-column>
+    <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
+  </p-dataTable>
 
   </div>
 </div>
diff --git a/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.ts b/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.ts
index 7c838fbf0..cc9c1bdf4 100644
--- a/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.ts
+++ b/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.ts
@@ -1,72 +1,46 @@
-import { Component } from '@angular/core'
+import { Component, OnInit } from '@angular/core'
 
 import { NotificationsService } from 'angular2-notifications'
+import { SortMeta } from 'primeng/primeng'
 
-import { Utils, VideoAbuseService } from '../../../shared'
-import { VideoAbuse } from '../../../../../shared'
+import { RestTable, RestPagination, VideoAbuseService } from '../../../shared'
+import { VideoAbuse } from '../../../../../../shared'
 
 @Component({
   selector: 'my-video-abuse-list',
   templateUrl: './video-abuse-list.component.html'
 })
-export class VideoAbuseListComponent {
-  videoAbusesSource = null
-  tableSettings = {
-    mode: 'external',
-    attr: {
-      class: 'table-hover'
-    },
-    hideSubHeader: true,
-    actions: {
-      position: 'right',
-      add: false,
-      edit: false,
-      delete: false
-    },
-    pager: {
-      display: true,
-      perPage: 10
-    },
-    columns: {
-      id: {
-        title: 'ID',
-        sortDirection: 'asc'
-      },
-      reason: {
-        title: 'Reason',
-        sort: false
-      },
-      reporterPodHost: {
-        title: 'Reporter pod host',
-        sort: false
-      },
-      reporterUsername: {
-        title: 'Reporter username',
-        sort: false
-      },
-      videoId: {
-        title: 'Video',
-        type: 'html',
-        sort: false,
-        valuePrepareFunction: this.buildVideoLink
-      },
-      createdAt: {
-        title: 'Created Date',
-        valuePrepareFunction: Utils.dateToHuman
-      }
-    }
-  }
+export class VideoAbuseListComponent extends RestTable implements OnInit {
+  videoAbuses: VideoAbuse[] = []
+  totalRecords = 0
+  rowsPerPage = 1
+  sort: SortMeta = { field: 'id', order: 1 }
+  pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
 
   constructor (
     private notificationsService: NotificationsService,
     private videoAbuseService: VideoAbuseService
   ) {
-    this.videoAbusesSource = this.videoAbuseService.getDataSource()
+    super()
+  }
+
+  ngOnInit () {
+    this.loadData()
   }
 
-  buildVideoLink (videoId: string) {
-    // TODO: transform to routerLink
-    // https://github.com/akveo/ng2-smart-table/issues/57
-    return `<a href="/videos/${videoId}" title="Go to the video">${videoId}</a>`
+  getRouterVideoLink (videoId: number) {
+    return [ '/videos', videoId ]
+  }
+
+  protected loadData () {
+    return this.videoAbuseService.getVideoAbuses(this.pagination, this.sort)
+               .subscribe(
+                 resultList => {
+                   this.videoAbuses = resultList.data
+                   this.totalRecords = resultList.total
+                 },
+
+                 err => this.notificationsService.error('Error', err)
+               )
   }
 }
diff --git a/client/src/app/account/account-details/account-details.component.ts b/client/src/app/account/account-details/account-details.component.ts
index 8cbed5009..78e365a62 100644
--- a/client/src/app/account/account-details/account-details.component.ts
+++ b/client/src/app/account/account-details/account-details.component.ts
@@ -59,7 +59,7 @@ export class AccountDetailsComponent extends FormReactive implements OnInit {
       () => {
         this.notificationsService.success('Success', 'Information updated.')
 
-        this.authService.refreshUserInformations()
+        this.authService.refreshUserInformation()
       },
 
       err => this.error = err
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts
index a90654e26..57bf64f69 100644
--- a/client/src/app/app.component.ts
+++ b/client/src/app/app.component.ts
@@ -36,6 +36,8 @@ export class AppComponent implements OnInit {
   ) {}
 
   ngOnInit () {
+    this.authService.loadClientCredentials()
+
     if (this.authService.isLoggedIn()) {
       // The service will automatically redirect to the login page if the token is not valid anymore
       this.userService.checkTokenValidity()
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts
index 804a7a71e..6aa56b8a7 100644
--- a/client/src/app/app.module.ts
+++ b/client/src/app/app.module.ts
@@ -7,8 +7,6 @@ import {
 } from '@angularclass/hmr'
 
 import { MetaModule, MetaLoader, MetaStaticLoader, PageTitlePositioning } from '@ngx-meta/core'
-// TODO: remove, we need this to avoid error in ng2-smart-table
-import 'rxjs/add/operator/toPromise'
 import 'bootstrap-loader'
 
 import { ENV_PROVIDERS } from './environment'
diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts
index de9e14b2d..522efb23c 100644
--- a/client/src/app/core/auth/auth.service.ts
+++ b/client/src/app/core/auth/auth.service.ts
@@ -1,8 +1,8 @@
 import { Injectable } from '@angular/core'
-import { Headers, Http, Response, URLSearchParams } from '@angular/http'
 import { Router } from '@angular/router'
 import { Observable } from 'rxjs/Observable'
 import { Subject } from 'rxjs/Subject'
+import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'
 import 'rxjs/add/operator/map'
 import 'rxjs/add/operator/mergeMap'
 import 'rxjs/add/observable/throw'
@@ -11,15 +11,35 @@ import { NotificationsService } from 'angular2-notifications'
 
 import { AuthStatus } from './auth-status.model'
 import { AuthUser } from './auth-user.model'
-import { OAuthClientLocal, UserRole } from '../../../../../shared'
+import { OAuthClientLocal, UserRole, UserRefreshToken } from '../../../../../shared'
 // Do not use the barrel (dependency loop)
 import { RestExtractor } from '../../shared/rest'
+import { UserLogin } from '../../../../../shared/models/users/user-login.model'
+import { User } from '../../shared/users/user.model'
+
+interface UserLoginWithUsername extends UserLogin {
+  access_token: string
+  refresh_token: string
+  token_type: string
+  username: string
+}
+
+interface UserLoginWithUserInformation extends UserLogin {
+  access_token: string
+  refresh_token: string
+  token_type: string
+  username: string
+  id: number
+  role: UserRole
+  displayNSFW: boolean
+  email: string
+}
 
 @Injectable()
 export class AuthService {
   private static BASE_CLIENT_URL = API_URL + '/api/v1/oauth-clients/local'
   private static BASE_TOKEN_URL = API_URL + '/api/v1/users/token'
-  private static BASE_USER_INFORMATIONS_URL = API_URL + '/api/v1/users/me'
+  private static BASE_USER_INFORMATION_URL = API_URL + '/api/v1/users/me'
 
   loginChangedSource: Observable<AuthStatus>
 
@@ -29,7 +49,7 @@ export class AuthService {
   private user: AuthUser = null
 
   constructor (
-    private http: Http,
+    private http: HttpClient,
     private notificationsService: NotificationsService,
     private restExtractor: RestExtractor,
     private router: Router
@@ -37,32 +57,33 @@ export class AuthService {
     this.loginChanged = new Subject<AuthStatus>()
     this.loginChangedSource = this.loginChanged.asObservable()
 
-    // Fetch the client_id/client_secret
-    // FIXME: save in local storage?
-    this.http.get(AuthService.BASE_CLIENT_URL)
-      .map(this.restExtractor.extractDataGet)
-      .catch(res => this.restExtractor.handleError(res))
-      .subscribe(
-        (result: OAuthClientLocal) => {
-          this.clientId = result.client_id
-          this.clientSecret = result.client_secret
-          console.log('Client credentials loaded.')
-        },
-
-        error => {
-          let errorMessage = `Cannot retrieve OAuth Client credentials: ${error.text}. \n`
-          errorMessage += 'Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.'
-
-          // We put a bigger timeout
-          // This is an important message
-          this.notificationsService.error('Error', errorMessage, { timeOut: 7000 })
-        }
-      )
-
     // Return null if there is nothing to load
     this.user = AuthUser.load()
   }
 
+  loadClientCredentials () {
+    // Fetch the client_id/client_secret
+    // FIXME: save in local storage?
+    this.http.get<OAuthClientLocal>(AuthService.BASE_CLIENT_URL)
+             .catch(res => this.restExtractor.handleError(res))
+             .subscribe(
+               res => {
+                 this.clientId = res.client_id
+                 this.clientSecret = res.client_secret
+                 console.log('Client credentials loaded.')
+               },
+
+               error => {
+                 let errorMessage = `Cannot retrieve OAuth Client credentials: ${error.text}. \n`
+                 errorMessage += 'Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.'
+
+                 // We put a bigger timeout
+                 // This is an important message
+                 this.notificationsService.error('Error', errorMessage, { timeOut: 7000 })
+               }
+             )
+  }
+
   getRefreshToken () {
     if (this.user === null) return null
 
@@ -70,7 +91,11 @@ export class AuthService {
   }
 
   getRequestHeaderValue () {
-    return `${this.getTokenType()} ${this.getAccessToken()}`
+    const accessToken = this.getAccessToken()
+
+    if (accessToken === null) return null
+
+    return `${this.getTokenType()} ${accessToken}`
   }
 
   getAccessToken () {
@@ -96,39 +121,26 @@ export class AuthService {
   }
 
   isLoggedIn () {
-    if (this.getAccessToken()) {
-      return true
-    } else {
-      return false
-    }
+    return !!this.getAccessToken()
   }
 
   login (username: string, password: string) {
-    let body = new URLSearchParams()
-    body.set('client_id', this.clientId)
-    body.set('client_secret', this.clientSecret)
-    body.set('response_type', 'code')
-    body.set('grant_type', 'password')
-    body.set('scope', 'upload')
-    body.set('username', username)
-    body.set('password', password)
-
-    let headers = new Headers()
-    headers.append('Content-Type', 'application/x-www-form-urlencoded')
-
-    let options = {
-      headers: headers
-    }
-
-    return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options)
-                    .map(this.restExtractor.extractDataGet)
-                    .map(res => {
-                      res.username = username
-                      return res
-                    })
-                    .flatMap(res => this.mergeUserInformations(res))
+    // Form url encoded
+    const body = new HttpParams().set('client_id', this.clientId)
+                                 .set('client_secret', this.clientSecret)
+                                 .set('response_type', 'code')
+                                 .set('grant_type', 'password')
+                                 .set('scope', 'upload')
+                                 .set('username', username)
+                                 .set('password', password)
+
+    const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded')
+
+    return this.http.post<UserLogin>(AuthService.BASE_TOKEN_URL, body, { headers })
+                    .map(res => Object.assign(res, { username }))
+                    .flatMap(res => this.mergeUserInformation(res))
                     .map(res => this.handleLogin(res))
-                    .catch((res) => this.restExtractor.handleError(res))
+                    .catch(res => this.restExtractor.handleError(res))
   }
 
   logout () {
@@ -145,33 +157,26 @@ export class AuthService {
 
     const refreshToken = this.getRefreshToken()
 
-    let body = new URLSearchParams()
-    body.set('refresh_token', refreshToken)
-    body.set('client_id', this.clientId)
-    body.set('client_secret', this.clientSecret)
-    body.set('response_type', 'code')
-    body.set('grant_type', 'refresh_token')
-
-    let headers = new Headers()
-    headers.append('Content-Type', 'application/x-www-form-urlencoded')
+    // Form url encoded
+    const body = new HttpParams().set('refresh_token', refreshToken)
+                                 .set('client_id', this.clientId)
+                                 .set('client_secret', this.clientSecret)
+                                 .set('response_type', 'code')
+                                 .set('grant_type', 'refresh_token')
 
-    let options = {
-      headers: headers
-    }
+    const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded')
 
-    return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options)
-                    .map(this.restExtractor.extractDataGet)
+    return this.http.post<UserRefreshToken>(AuthService.BASE_TOKEN_URL, body, { headers })
                     .map(res => this.handleRefreshToken(res))
-                    .catch((res: Response) => {
+                    .catch(res => {
                       // The refresh token is invalid?
-                      if (res.status === 400 && res.json() && res.json().error === 'invalid_grant') {
+                      if (res.status === 400 && res.error === 'invalid_grant') {
                         console.error('Cannot refresh token -> logout...')
                         this.logout()
                         this.router.navigate(['/login'])
 
                         return Observable.throw({
-                          json: () => '',
-                          text: () => 'You need to reconnect.'
+                          error: 'You need to reconnect.'
                         })
                       }
 
@@ -179,7 +184,7 @@ export class AuthService {
                     })
   }
 
-  refreshUserInformations () {
+  refreshUserInformation () {
     const obj = {
       access_token: this.user.getAccessToken(),
       refresh_token: null,
@@ -187,7 +192,7 @@ export class AuthService {
       username: this.user.username
     }
 
-    this.mergeUserInformations (obj)
+    this.mergeUserInformation(obj)
         .subscribe(
           res => {
             this.user.displayNSFW = res.displayNSFW
@@ -198,42 +203,25 @@ export class AuthService {
         )
   }
 
-  private mergeUserInformations (obj: {
-    access_token: string,
-    refresh_token: string,
-    token_type: string,
-    username: string
-  }) {
-    // Do not call authHttp here to avoid circular dependencies headaches
-
-    const headers = new Headers()
-    headers.set('Authorization', `Bearer ${obj.access_token}`)
-
-    return this.http.get(AuthService.BASE_USER_INFORMATIONS_URL, { headers })
-             .map(res => res.json())
-             .map(res => {
-               const newProperties = {
-                 id: res.id as number,
-                 role: res.role as UserRole,
-                 displayNSFW: res.displayNSFW as boolean,
-                 email: res.email as string
-               }
+  private mergeUserInformation (obj: UserLoginWithUsername): Observable<UserLoginWithUserInformation> {
+    // User is not loaded yet, set manually auth header
+    const headers = new HttpHeaders().set('Authorization', `${obj.token_type} ${obj.access_token}`)
+
+    return this.http.get<User>(AuthService.BASE_USER_INFORMATION_URL, { headers })
+                    .map(res => {
+                      const newProperties = {
+                        id: res.id as number,
+                        role: res.role as UserRole,
+                        displayNSFW: res.displayNSFW as boolean,
+                        email: res.email as string
+                      }
 
-               return Object.assign(obj, newProperties)
-             }
+                      return Object.assign(obj, newProperties)
+                    }
     )
   }
 
-  private handleLogin (obj: {
-    access_token: string,
-    refresh_token: string,
-    token_type: string,
-    id: number,
-    username: string,
-    email: string,
-    role: UserRole,
-    displayNSFW: boolean
-  }) {
+  private handleLogin (obj: UserLoginWithUserInformation) {
     const id = obj.id
     const username = obj.username
     const role = obj.role
@@ -251,7 +239,7 @@ export class AuthService {
     this.setStatus(AuthStatus.LoggedIn)
   }
 
-  private handleRefreshToken (obj: { access_token: string, refresh_token: string }) {
+  private handleRefreshToken (obj: UserRefreshToken) {
     this.user.refreshTokens(obj.access_token, obj.refresh_token)
     this.user.save()
   }
@@ -259,5 +247,4 @@ export class AuthService {
   private setStatus (status: AuthStatus) {
     this.loginChanged.next(status)
   }
-
 }
diff --git a/client/src/app/core/config/config.service.ts b/client/src/app/core/config/config.service.ts
index acdc12cc6..3c479bcb8 100644
--- a/client/src/app/core/config/config.service.ts
+++ b/client/src/app/core/config/config.service.ts
@@ -1,7 +1,6 @@
 import { Injectable } from '@angular/core'
-import { Http } from '@angular/http'
+import { HttpClient } from '@angular/common/http'
 
-import { RestExtractor } from '../../shared/rest'
 import { ServerConfig } from '../../../../../shared'
 
 @Injectable()
@@ -14,17 +13,11 @@ export class ConfigService {
     }
   }
 
-  constructor (
-    private http: Http,
-    private restExtractor: RestExtractor
-  ) {}
+  constructor (private http: HttpClient) {}
 
   loadConfig () {
-    this.http.get(ConfigService.BASE_CONFIG_URL)
-             .map(this.restExtractor.extractDataGet)
-             .subscribe(data => {
-               this.config = data
-             })
+    this.http.get<ServerConfig>(ConfigService.BASE_CONFIG_URL)
+             .subscribe(data => this.config = data)
   }
 
   getConfig () {
diff --git a/client/src/app/shared/auth/auth-http.service.ts b/client/src/app/shared/auth/auth-http.service.ts
deleted file mode 100644
index 0fbaab0a8..000000000
--- a/client/src/app/shared/auth/auth-http.service.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-import { Injectable } from '@angular/core'
-import {
-  ConnectionBackend,
-  Headers,
-  Http,
-  Request,
-  RequestMethod,
-  RequestOptions,
-  RequestOptionsArgs,
-  Response,
-  XHRBackend
-} from '@angular/http'
-import { Observable } from 'rxjs/Observable'
-
-import { AuthService } from '../../core'
-
-@Injectable()
-export class AuthHttp extends Http {
-  constructor (backend: ConnectionBackend, defaultOptions: RequestOptions, private authService: AuthService) {
-    super(backend, defaultOptions)
-  }
-
-  request (url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
-    if (!options) options = {}
-
-    options.headers = new Headers()
-    this.setAuthorizationHeader(options.headers)
-
-    return super.request(url, options)
-                .catch((err) => {
-                  if (err.status === 401) {
-                    return this.handleTokenExpired(url, options)
-                  }
-
-                  return Observable.throw(err)
-                })
-  }
-
-  delete (url: string, options?: RequestOptionsArgs): Observable<Response> {
-    if (!options) options = {}
-    options.method = RequestMethod.Delete
-
-    return this.request(url, options)
-  }
-
-  get (url: string, options?: RequestOptionsArgs): Observable<Response> {
-    if (!options) options = {}
-    options.method = RequestMethod.Get
-
-    return this.request(url, options)
-  }
-
-  post (url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
-    if (!options) options = {}
-    options.method = RequestMethod.Post
-    options.body = body
-
-    return this.request(url, options)
-  }
-
-  put (url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
-    if (!options) options = {}
-    options.method = RequestMethod.Put
-    options.body = body
-
-    return this.request(url, options)
-  }
-
-  private handleTokenExpired (url: string | Request, options: RequestOptionsArgs) {
-    return this.authService.refreshAccessToken()
-                           .flatMap(() => {
-                             this.setAuthorizationHeader(options.headers)
-
-                             return super.request(url, options)
-                           })
-  }
-
-  private setAuthorizationHeader (headers: Headers) {
-    headers.set('Authorization', this.authService.getRequestHeaderValue())
-  }
-}
-
-export function useFactory (backend: XHRBackend, defaultOptions: RequestOptions, authService: AuthService) {
-  return new AuthHttp(backend, defaultOptions, authService)
-}
-
-export const AUTH_HTTP_PROVIDERS = [
-  {
-    provide: AuthHttp,
-    useFactory,
-    deps: [ XHRBackend, RequestOptions, AuthService ]
-  }
-]
diff --git a/client/src/app/shared/auth/auth-interceptor.service.ts b/client/src/app/shared/auth/auth-interceptor.service.ts
new file mode 100644
index 000000000..1e890d8f3
--- /dev/null
+++ b/client/src/app/shared/auth/auth-interceptor.service.ts
@@ -0,0 +1,62 @@
+import { Injectable, Injector } from '@angular/core'
+import {
+  HttpInterceptor,
+  HttpRequest,
+  HttpEvent,
+  HttpHandler, HTTP_INTERCEPTORS
+} from '@angular/common/http'
+import { Observable } from 'rxjs/Observable'
+
+import { AuthService } from '../../core'
+import 'rxjs/add/operator/switchMap'
+
+@Injectable()
+export class AuthInterceptor implements HttpInterceptor {
+  private authService: AuthService
+
+  // https://github.com/angular/angular/issues/18224#issuecomment-316957213
+  constructor (private injector: Injector) {}
+
+  intercept (req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
+    if (this.authService === undefined) {
+      this.authService = this.injector.get(AuthService)
+    }
+
+    const authReq = this.cloneRequestWithAuth(req)
+
+    // Pass on the cloned request instead of the original request
+    // Catch 401 errors (refresh token expired)
+    return next.handle(authReq)
+               .catch(err => {
+                 if (err.status === 401) {
+                   return this.handleTokenExpired(req, next)
+                 }
+
+                 return Observable.throw(err)
+               })
+  }
+
+  private handleTokenExpired (req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
+    return this.authService.refreshAccessToken()
+                           .switchMap(() => {
+                             const authReq = this.cloneRequestWithAuth(req)
+
+                             return next.handle(authReq)
+                           })
+  }
+
+  private cloneRequestWithAuth (req: HttpRequest<any>) {
+    const authHeaderValue = this.authService.getRequestHeaderValue()
+
+    if (authHeaderValue === null) return req
+
+    // Clone the request to add the new header
+    return req.clone({ headers: req.headers.set('Authorization', authHeaderValue) })
+  }
+}
+
+export const AUTH_INTERCEPTOR_PROVIDER = {
+  provide: HTTP_INTERCEPTORS,
+  useClass: AuthInterceptor,
+  multi: true
+}
diff --git a/client/src/app/shared/auth/index.ts b/client/src/app/shared/auth/index.ts
index 0f2bfb0d6..84a07196f 100644
--- a/client/src/app/shared/auth/index.ts
+++ b/client/src/app/shared/auth/index.ts
@@ -1 +1 @@
-export * from './auth-http.service'
+export * from './auth-interceptor.service'
diff --git a/client/src/app/shared/rest/index.ts b/client/src/app/shared/rest/index.ts
index e0be155cf..3f1996130 100644
--- a/client/src/app/shared/rest/index.ts
+++ b/client/src/app/shared/rest/index.ts
@@ -2,3 +2,4 @@ export * from './rest-data-source'
 export * from './rest-extractor.service'
 export * from './rest-pagination'
 export * from './rest.service'
+export * from './rest-table'
diff --git a/client/src/app/shared/rest/rest-data-source.ts b/client/src/app/shared/rest/rest-data-source.ts
index 5c205d280..57a2efb57 100644
--- a/client/src/app/shared/rest/rest-data-source.ts
+++ b/client/src/app/shared/rest/rest-data-source.ts
@@ -1,68 +1,32 @@
-import { Http, RequestOptionsArgs, URLSearchParams, Response } from '@angular/http'
-
-import { ServerDataSource } from 'ng2-smart-table'
-
-export class RestDataSource extends ServerDataSource {
-  private updateResponse: (input: any[]) => any[]
-
-  constructor (http: Http, endpoint: string, updateResponse?: (input: any[]) => any[]) {
-    const options = {
-      endPoint: endpoint,
-      sortFieldKey: 'sort',
-      dataKey: 'data'
-    }
-    super(http, options)
-
-    if (updateResponse) {
-      this.updateResponse = updateResponse
-    }
-  }
-
-  protected extractDataFromResponse (res: Response) {
-    const json = res.json()
-    if (!json) return []
-    let data = json.data
-
-    if (this.updateResponse !== undefined) {
-      data = this.updateResponse(data)
-    }
-
-    return data
-  }
-
-  protected extractTotalFromResponse (res: Response) {
-    const rawData = res.json()
-    return rawData ? parseInt(rawData.total, 10) : 0
-  }
-
-  protected addSortRequestOptions (requestOptions: RequestOptionsArgs) {
-    const searchParams = requestOptions.params as URLSearchParams
-
-    if (this.sortConf) {
-      this.sortConf.forEach((fieldConf) => {
-        const sortPrefix = fieldConf.direction === 'desc' ? '-' : ''
-
-        searchParams.set(this.conf.sortFieldKey, sortPrefix + fieldConf.field)
-      })
-    }
-
-    return requestOptions
-  }
-
-  protected addPagerRequestOptions (requestOptions: RequestOptionsArgs) {
-    const searchParams = requestOptions.params as URLSearchParams
-
-    if (this.pagingConf && this.pagingConf['page'] && this.pagingConf['perPage']) {
-      const perPage = this.pagingConf['perPage']
-      const page = this.pagingConf['page']
-
-      const start = (page - 1) * perPage
-      const count = perPage
-
-      searchParams.set('start', start.toString())
-      searchParams.set('count', count.toString())
-    }
-
-    return requestOptions
-  }
+export class RestDataSource  {
+  // protected addSortRequestOptions (requestOptions: RequestOptionsArgs) {
+  //   const searchParams = requestOptions.params as URLSearchParams
+  //
+  //   if (this.sortConf) {
+  //     this.sortConf.forEach((fieldConf) => {
+  //       const sortPrefix = fieldConf.direction === 'desc' ? '-' : ''
+  //
+  //       searchParams.set(this.conf.sortFieldKey, sortPrefix + fieldConf.field)
+  //     })
+  //   }
+  //
+  //   return requestOptions
+  // }
+  //
+  // protected addPagerRequestOptions (requestOptions: RequestOptionsArgs) {
+  //   const searchParams = requestOptions.params as URLSearchParams
+  //
+  //   if (this.pagingConf && this.pagingConf['page'] && this.pagingConf['perPage']) {
+  //     const perPage = this.pagingConf['perPage']
+  //     const page = this.pagingConf['page']
+  //
+  //     const start = (page - 1) * perPage
+  //     const count = perPage
+  //
+  //     searchParams.set('start', start.toString())
+  //     searchParams.set('count', count.toString())
+  //   }
+  //
+  //   return requestOptions
+  // }
 }
diff --git a/client/src/app/shared/rest/rest-extractor.service.ts b/client/src/app/shared/rest/rest-extractor.service.ts
index f6a818ec8..32dad5c73 100644
--- a/client/src/app/shared/rest/rest-extractor.service.ts
+++ b/client/src/app/shared/rest/rest-extractor.service.ts
@@ -1,52 +1,58 @@
 import { Injectable } from '@angular/core'
-import { Response } from '@angular/http'
 import { Observable } from 'rxjs/Observable'
+import { HttpErrorResponse } from '@angular/common/http'
 
-export interface ResultList {
-  data: any[]
-  total: number
-}
+import { Utils } from '../utils'
+import { ResultList } from '../../../../../shared'
 
 @Injectable()
 export class RestExtractor {
 
-  extractDataBool (res: Response) {
+  extractDataBool () {
     return true
   }
 
-  extractDataList (res: Response) {
-    const body = res.json()
+  applyToResultListData <T> (result: ResultList<T>, fun: Function, additionalArgs?: any[]): ResultList<T> {
+    const data: T[] = result.data
+    const newData: T[] = []
 
-    const ret: ResultList = {
-      data: body.data,
-      total: body.total
-    }
+    data.forEach(d => newData.push(fun.call(this, d, additionalArgs)))
 
-    return ret
+    return {
+      total: result.total,
+      data: newData
+    }
   }
 
-  extractDataGet (res: Response) {
-    return res.json()
+  convertResultListDateToHuman <T> (result: ResultList<T>, fieldsToConvert: string[] = [ 'createdAt' ]): ResultList<T> {
+    return this.applyToResultListData(result, this.convertDateToHuman, [ fieldsToConvert ])
   }
 
-  handleError (res: Response) {
-    let text = 'Server error: '
-    text += res.text()
-    let json = ''
+  convertDateToHuman (target: object, fieldsToConvert: string[]) {
+    const source = {}
+    fieldsToConvert.forEach(field => {
+      source[field] = Utils.dateToHuman(target[field])
+    })
 
-    try {
-      json = res.json()
-    } catch (err) {
-      console.error('Cannot get JSON from response.')
-    }
+    return Object.assign(target, source)
+  }
 
-    const error = {
-      json,
-      text
+  handleError (err: HttpErrorResponse) {
+    let errorMessage
+
+    if (err.error instanceof Error) {
+      // A client-side or network error occurred. Handle it accordingly.
+      errorMessage = err.error.message
+      console.error('An error occurred:', errorMessage)
+    } else if (err.status !== undefined) {
+      // The backend returned an unsuccessful response code.
+      // The response body may contain clues as to what went wrong,
+      errorMessage = err.error
+      console.error(`Backend returned code ${err.status}, body was: ${errorMessage}`)
+    } else {
+      errorMessage = err
     }
 
-    console.error(error)
-
-    return Observable.throw(error)
+    return Observable.throw(errorMessage)
   }
 }
diff --git a/client/src/app/shared/rest/rest-pagination.ts b/client/src/app/shared/rest/rest-pagination.ts
index 766e7a9e5..0faa59303 100644
--- a/client/src/app/shared/rest/rest-pagination.ts
+++ b/client/src/app/shared/rest/rest-pagination.ts
@@ -1,5 +1,4 @@
 export interface RestPagination {
-  currentPage: number
-  itemsPerPage: number
-  totalItems: number
+  start: number
+  count: number
 }
diff --git a/client/src/app/shared/rest/rest-table.ts b/client/src/app/shared/rest/rest-table.ts
new file mode 100644
index 000000000..db2cb5e14
--- /dev/null
+++ b/client/src/app/shared/rest/rest-table.ts
@@ -0,0 +1,27 @@
+import { LazyLoadEvent, SortMeta } from 'primeng/primeng'
+
+import { RestPagination } from './rest-pagination'
+
+export abstract class RestTable {
+  abstract totalRecords: number
+  abstract rowsPerPage: number
+  abstract sort: SortMeta
+  abstract pagination: RestPagination
+
+  protected abstract loadData (): void
+
+  loadLazy (event: LazyLoadEvent) {
+    this.sort = {
+      order: event.sortOrder,
+      field: event.sortField
+    }
+
+    this.pagination = {
+      start: event.first,
+      count: this.rowsPerPage
+    }
+
+    this.loadData()
+  }
+
+}
diff --git a/client/src/app/shared/rest/rest.service.ts b/client/src/app/shared/rest/rest.service.ts
index 43dc20b34..f7838ba06 100644
--- a/client/src/app/shared/rest/rest.service.ts
+++ b/client/src/app/shared/rest/rest.service.ts
@@ -1,27 +1,34 @@
 import { Injectable } from '@angular/core'
-import { URLSearchParams } from '@angular/http'
+import { HttpParams } from '@angular/common/http'
+import { SortMeta } from 'primeng/primeng'
 
 import { RestPagination } from './rest-pagination'
 
 @Injectable()
 export class RestService {
 
-  buildRestGetParams (pagination?: RestPagination, sort?: string) {
-    const params = new URLSearchParams()
+  addRestGetParams (params: HttpParams, pagination?: RestPagination, sort?: SortMeta | string) {
+    let newParams = params
 
-    if (pagination) {
-      const start: number = (pagination.currentPage - 1) * pagination.itemsPerPage
-      const count: number = pagination.itemsPerPage
-
-      params.set('start', start.toString())
-      params.set('count', count.toString())
+    if (pagination !== undefined) {
+      newParams = newParams.set('start', pagination.start.toString())
+                           .set('count', pagination.count.toString())
     }
 
-    if (sort) {
-      params.set('sort', sort)
+    if (sort !== undefined) {
+      let sortString = ''
+
+      if (typeof sort === 'string') {
+        sortString = sort
+      } else {
+        const sortPrefix = sort.order === 1 ? '' : '-'
+        sortString = sortPrefix + sort.field
+      }
+
+      newParams = newParams.set('sort', sortString)
     }
 
-    return params
+    return newParams
   }
 
 }
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index 99b51aa4e..118ce822d 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -1,6 +1,6 @@
 import { NgModule } from '@angular/core'
+import { HttpClientModule } from '@angular/common/http'
 import { CommonModule } from '@angular/common'
-import { HttpModule } from '@angular/http'
 import { FormsModule, ReactiveFormsModule } from '@angular/forms'
 import { RouterModule } from '@angular/router'
 
@@ -11,9 +11,9 @@ import { ProgressbarModule } from 'ngx-bootstrap/progressbar'
 import { PaginationModule } from 'ngx-bootstrap/pagination'
 import { ModalModule } from 'ngx-bootstrap/modal'
 import { FileUploadModule } from 'ng2-file-upload/ng2-file-upload'
-import { Ng2SmartTableModule } from 'ng2-smart-table'
+import { DataTableModule, SharedModule as PrimeSharedModule } from 'primeng/primeng'
 
-import { AUTH_HTTP_PROVIDERS } from './auth'
+import { AUTH_INTERCEPTOR_PROVIDER } from './auth'
 import { RestExtractor, RestService } from './rest'
 import { SearchComponent, SearchService } from './search'
 import { UserService } from './users'
@@ -24,8 +24,8 @@ import { VideoAbuseService } from './video-abuse'
     CommonModule,
     FormsModule,
     ReactiveFormsModule,
-    HttpModule,
     RouterModule,
+    HttpClientModule,
 
     BsDropdownModule.forRoot(),
     ModalModule.forRoot(),
@@ -33,7 +33,9 @@ import { VideoAbuseService } from './video-abuse'
     ProgressbarModule.forRoot(),
 
     FileUploadModule,
-    Ng2SmartTableModule
+
+    DataTableModule,
+    PrimeSharedModule
   ],
 
   declarations: [
@@ -46,15 +48,16 @@ import { VideoAbuseService } from './video-abuse'
     CommonModule,
     FormsModule,
     ReactiveFormsModule,
-    HttpModule,
     RouterModule,
+    HttpClientModule,
 
     BsDropdownModule,
     FileUploadModule,
     ModalModule,
     PaginationModule,
     ProgressbarModule,
-    Ng2SmartTableModule,
+    DataTableModule,
+    PrimeSharedModule,
     BytesPipe,
     KeysPipe,
 
@@ -62,7 +65,7 @@ import { VideoAbuseService } from './video-abuse'
   ],
 
   providers: [
-    AUTH_HTTP_PROVIDERS,
+    AUTH_INTERCEPTOR_PROVIDER,
     RestExtractor,
     RestService,
     SearchService,
diff --git a/client/src/app/shared/users/user.service.ts b/client/src/app/shared/users/user.service.ts
index 35180be4d..5c089d221 100644
--- a/client/src/app/shared/users/user.service.ts
+++ b/client/src/app/shared/users/user.service.ts
@@ -1,10 +1,8 @@
 import { Injectable } from '@angular/core'
-import { Http } from '@angular/http'
+import { HttpClient } from '@angular/common/http'
 import 'rxjs/add/operator/catch'
 import 'rxjs/add/operator/map'
 
-import { AuthService } from '../../core'
-import { AuthHttp } from '../auth'
 import { RestExtractor } from '../rest'
 import { UserCreate, UserUpdateMe } from '../../../../../shared'
 
@@ -13,9 +11,7 @@ export class UserService {
   static BASE_USERS_URL = API_URL + '/api/v1/users/'
 
   constructor (
-    private http: Http,
-    private authHttp: AuthHttp,
-    private authService: AuthService,
+    private authHttp: HttpClient,
     private restExtractor: RestExtractor
   ) {}
 
@@ -34,7 +30,7 @@ export class UserService {
 
     return this.authHttp.put(url, body)
                         .map(this.restExtractor.extractDataBool)
-                        .catch((res) => this.restExtractor.handleError(res))
+                        .catch(res => this.restExtractor.handleError(res))
   }
 
   updateMyDetails (details: UserUpdateMe) {
@@ -42,12 +38,12 @@ export class UserService {
 
     return this.authHttp.put(url, details)
                         .map(this.restExtractor.extractDataBool)
-                        .catch((res) => this.restExtractor.handleError(res))
+                        .catch(res => this.restExtractor.handleError(res))
   }
 
   signup (userCreate: UserCreate) {
-    return this.http.post(UserService.BASE_USERS_URL + 'register', userCreate)
+    return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate)
                         .map(this.restExtractor.extractDataBool)
-                        .catch(this.restExtractor.handleError)
+                        .catch(res => this.restExtractor.handleError(res))
   }
 }
diff --git a/client/src/app/shared/utils.ts b/client/src/app/shared/utils.ts
index c3189a570..7c8ae2e3e 100644
--- a/client/src/app/shared/utils.ts
+++ b/client/src/app/shared/utils.ts
@@ -2,15 +2,7 @@ import { DatePipe } from '@angular/common'
 
 export class Utils {
 
-  static dateToHuman (date: String) {
+  static dateToHuman (date: Date) {
     return new DatePipe('en').transform(date, 'medium')
   }
-
-  static getRowDeleteButton () {
-    return '<span class="glyphicon glyphicon-remove glyphicon-black"></span>'
-  }
-
-  static getRowEditButton () {
-    return '<span class="glyphicon glyphicon-pencil glyphicon-black"></span>'
-  }
 }
diff --git a/client/src/app/shared/video-abuse/video-abuse.service.ts b/client/src/app/shared/video-abuse/video-abuse.service.ts
index 636a02084..984581114 100644
--- a/client/src/app/shared/video-abuse/video-abuse.service.ts
+++ b/client/src/app/shared/video-abuse/video-abuse.service.ts
@@ -1,42 +1,53 @@
 import { Injectable } from '@angular/core'
-import { Http } from '@angular/http'
-import { Observable } from 'rxjs/Observable'
+import { HttpClient, HttpParams } from '@angular/common/http'
 import 'rxjs/add/operator/catch'
 import 'rxjs/add/operator/map'
+import { Observable } from 'rxjs/Observable'
+
+import { SortMeta } from 'primeng/primeng'
 
 import { AuthService } from '../core'
-import { AuthHttp } from '../auth'
-import { RestDataSource, RestExtractor, ResultList } from '../rest'
-import { VideoAbuse } from '../../../../../shared'
+import { RestExtractor, RestPagination, RestService } from '../rest'
+import { Utils } from '../utils'
+import { ResultList, VideoAbuse } from '../../../../../shared'
 
 @Injectable()
 export class VideoAbuseService {
   private static BASE_VIDEO_ABUSE_URL = API_URL + '/api/v1/videos/'
 
   constructor (
-    private authHttp: AuthHttp,
+    private authHttp: HttpClient,
+    private restService: RestService,
     private restExtractor: RestExtractor
   ) {}
 
-  getDataSource () {
-    return new RestDataSource(this.authHttp, VideoAbuseService.BASE_VIDEO_ABUSE_URL + 'abuse')
+  getVideoAbuses (pagination: RestPagination, sort: SortMeta): Observable<ResultList<VideoAbuse>> {
+    const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + 'abuse'
+
+    let params = new HttpParams()
+    params = this.restService.addRestGetParams(params, pagination, sort)
+
+    return this.authHttp.get<ResultList<VideoAbuse>>(url, { params })
+                        .map(res => this.restExtractor.convertResultListDateToHuman(res))
+                        .map(res => this.restExtractor.applyToResultListData(res, this.formatVideoAbuse.bind(this)))
+                        .catch(res => this.restExtractor.handleError(res))
   }
 
   reportVideo (id: number, reason: string) {
+    const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + id + '/abuse'
     const body = {
       reason
     }
-    const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + id + '/abuse'
 
     return this.authHttp.post(url, body)
                         .map(this.restExtractor.extractDataBool)
-                        .catch((res) => this.restExtractor.handleError(res))
+                        .catch(res => this.restExtractor.handleError(res))
   }
 
-  private extractVideoAbuses (result: ResultList) {
-    const videoAbuses: VideoAbuse[] = result.data
-    const totalVideoAbuses = result.total
-
-    return { videoAbuses, totalVideoAbuses }
+  private formatVideoAbuse (videoAbuse: VideoAbuse) {
+    return Object.assign(videoAbuse, {
+      createdAt: Utils.dateToHuman(videoAbuse.createdAt)
+    })
   }
+
 }
diff --git a/client/src/app/videos/shared/index.ts b/client/src/app/videos/shared/index.ts
index 97d795321..8168e3bfd 100644
--- a/client/src/app/videos/shared/index.ts
+++ b/client/src/app/videos/shared/index.ts
@@ -1,3 +1,4 @@
 export * from './sort-field.type'
 export * from './video.model'
 export * from './video.service'
+export * from './video-pagination.model'
diff --git a/client/src/app/videos/shared/video-pagination.model.ts b/client/src/app/videos/shared/video-pagination.model.ts
new file mode 100644
index 000000000..9e71769cb
--- /dev/null
+++ b/client/src/app/videos/shared/video-pagination.model.ts
@@ -0,0 +1,5 @@
+export interface VideoPagination {
+  currentPage: number
+  itemsPerPage: number
+  totalItems: number
+}
diff --git a/client/src/app/videos/shared/video.model.ts b/client/src/app/videos/shared/video.model.ts
index 1a413db9d..17f41059d 100644
--- a/client/src/app/videos/shared/video.model.ts
+++ b/client/src/app/videos/shared/video.model.ts
@@ -46,7 +46,7 @@ export class Video implements VideoServerModel {
 
   constructor (hash: {
     author: string,
-    createdAt: string,
+    createdAt: Date | string,
     categoryLabel: string,
     category: number,
     licenceLabel: string,
@@ -70,7 +70,7 @@ export class Video implements VideoServerModel {
     files: VideoFile[]
   }) {
     this.author = hash.author
-    this.createdAt = new Date(hash.createdAt)
+    this.createdAt = new Date(hash.createdAt.toString())
     this.categoryLabel = hash.categoryLabel
     this.category = hash.category
     this.licenceLabel = hash.licenceLabel
diff --git a/client/src/app/videos/shared/video.service.ts b/client/src/app/videos/shared/video.service.ts
index 67091a8d8..b6d2a0666 100644
--- a/client/src/app/videos/shared/video.service.ts
+++ b/client/src/app/videos/shared/video.service.ts
@@ -1,27 +1,26 @@
 import { Injectable } from '@angular/core'
-import { Http, Headers, RequestOptions } from '@angular/http'
 import { Observable } from 'rxjs/Observable'
 import 'rxjs/add/operator/catch'
 import 'rxjs/add/operator/map'
+import { HttpClient, HttpParams } from '@angular/common/http'
 
 import { Search } from '../../shared'
 import { SortField } from './sort-field.type'
-import { AuthService } from '../../core'
 import {
-  AuthHttp,
   RestExtractor,
-  RestPagination,
   RestService,
-  ResultList,
   UserService
 } from '../../shared'
 import { Video } from './video.model'
+import { VideoPagination } from './video-pagination.model'
 import {
-  UserVideoRate,
-  VideoRateType,
-  VideoUpdate,
-  VideoAbuseCreate,
-  UserVideoRateUpdate
+UserVideoRate,
+VideoRateType,
+VideoUpdate,
+VideoAbuseCreate,
+UserVideoRateUpdate,
+Video as VideoServerModel,
+ResultList
 } from '../../../../../shared'
 
 @Injectable()
@@ -33,9 +32,7 @@ export class VideoService {
   videoLanguages: Array<{ id: number, label: string }> = []
 
   constructor (
-    private authService: AuthService,
-    private authHttp: AuthHttp,
-    private http: Http,
+    private authHttp: HttpClient,
     private restExtractor: RestExtractor,
     private restService: RestService
   ) {}
@@ -52,11 +49,10 @@ export class VideoService {
     return this.loadVideoAttributeEnum('languages', this.videoLanguages)
   }
 
-  getVideo (uuid: string): Observable<Video> {
-    return this.http.get(VideoService.BASE_VIDEO_URL + uuid)
-                    .map(this.restExtractor.extractDataGet)
-                    .map(videoHash => new Video(videoHash))
-                    .catch((res) => this.restExtractor.handleError(res))
+  getVideo (uuid: string) {
+    return this.authHttp.get<VideoServerModel>(VideoService.BASE_VIDEO_URL + uuid)
+                        .map(videoHash => new Video(videoHash))
+                        .catch((res) => this.restExtractor.handleError(res))
   }
 
   updateVideo (video: Video) {
@@ -72,38 +68,41 @@ export class VideoService {
       nsfw: video.nsfw
     }
 
-    const headers = new Headers({ 'Content-Type': 'application/json' })
-    const options = new RequestOptions({ headers: headers })
-
-    return this.authHttp.put(`${VideoService.BASE_VIDEO_URL}/${video.id}`, body, options)
+    return this.authHttp.put(`${VideoService.BASE_VIDEO_URL}/${video.id}`, body)
                         .map(this.restExtractor.extractDataBool)
                         .catch(this.restExtractor.handleError)
   }
 
-  getVideos (pagination: RestPagination, sort: SortField) {
-    const params = this.restService.buildRestGetParams(pagination, sort)
+  getVideos (videoPagination: VideoPagination, sort: SortField) {
+    const pagination = this.videoPaginationToRestPagination(videoPagination)
 
-    return this.http.get(VideoService.BASE_VIDEO_URL, { search: params })
-                    .map(res => res.json())
-                    .map(this.extractVideos)
-                    .catch((res) => this.restExtractor.handleError(res))
-  }
+    let params = new HttpParams()
+    params = this.restService.addRestGetParams(params, pagination, sort)
 
-  removeVideo (id: number) {
-    return this.authHttp.delete(VideoService.BASE_VIDEO_URL + id)
-                        .map(this.restExtractor.extractDataBool)
+    return this.authHttp.get(VideoService.BASE_VIDEO_URL, { params })
+                        .map(this.extractVideos)
                         .catch((res) => this.restExtractor.handleError(res))
   }
 
-  searchVideos (search: Search, pagination: RestPagination, sort: SortField) {
-    const params = this.restService.buildRestGetParams(pagination, sort)
+  searchVideos (search: Search, videoPagination: VideoPagination, sort: SortField) {
+    const url = VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value)
+
+    const pagination = this.videoPaginationToRestPagination(videoPagination)
+
+    let params = new HttpParams()
+    params = this.restService.addRestGetParams(params, pagination, sort)
 
     if (search.field) params.set('field', search.field)
 
-    return this.http.get(VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value), { search: params })
-                    .map(this.restExtractor.extractDataList)
-                    .map(this.extractVideos)
-                    .catch((res) => this.restExtractor.handleError(res))
+    return this.authHttp.get<ResultList<VideoServerModel>>(url, { params })
+                        .map(this.extractVideos)
+                        .catch((res) => this.restExtractor.handleError(res))
+  }
+
+  removeVideo (id: number) {
+    return this.authHttp.delete(VideoService.BASE_VIDEO_URL + id)
+               .map(this.restExtractor.extractDataBool)
+               .catch((res) => this.restExtractor.handleError(res))
   }
 
   reportVideo (id: number, reason: string) {
@@ -114,7 +113,7 @@ export class VideoService {
 
     return this.authHttp.post(url, body)
                         .map(this.restExtractor.extractDataBool)
-                        .catch((res) => this.restExtractor.handleError(res))
+                        .catch(res => this.restExtractor.handleError(res))
   }
 
   setVideoLike (id: number) {
@@ -129,14 +128,20 @@ export class VideoService {
     const url = UserService.BASE_USERS_URL + '/me/videos/' + id + '/rating'
 
     return this.authHttp.get(url)
-                        .map(this.restExtractor.extractDataGet)
-                        .catch((res) => this.restExtractor.handleError(res))
+                        .catch(res => this.restExtractor.handleError(res))
   }
 
   blacklistVideo (id: number) {
     return this.authHttp.post(VideoService.BASE_VIDEO_URL + id + '/blacklist', {})
                         .map(this.restExtractor.extractDataBool)
-                        .catch((res) => this.restExtractor.handleError(res))
+                        .catch(res => this.restExtractor.handleError(res))
+  }
+
+  private videoPaginationToRestPagination (videoPagination: VideoPagination) {
+    const start: number = (videoPagination.currentPage - 1) * videoPagination.itemsPerPage
+    const count: number = videoPagination.itemsPerPage
+
+    return { start, count }
   }
 
   private setVideoRate (id: number, rateType: VideoRateType) {
@@ -147,13 +152,14 @@ export class VideoService {
 
     return this.authHttp.put(url, body)
                         .map(this.restExtractor.extractDataBool)
-                        .catch((res) => this.restExtractor.handleError(res))
+                        .catch(res => this.restExtractor.handleError(res))
   }
 
-  private extractVideos (result: ResultList) {
+  private extractVideos (result: ResultList<VideoServerModel>) {
     const videosJson = result.data
     const totalVideos = result.total
     const videos = []
+
     for (const videoJson of videosJson) {
       videos.push(new Video(videoJson))
     }
@@ -162,15 +168,14 @@ export class VideoService {
   }
 
   private loadVideoAttributeEnum (attributeName: 'categories' | 'licences' | 'languages', hashToPopulate: { id: number, label: string }[]) {
-    return this.http.get(VideoService.BASE_VIDEO_URL + attributeName)
-                    .map(this.restExtractor.extractDataGet)
-                    .subscribe(data => {
-                      Object.keys(data).forEach(dataKey => {
-                        hashToPopulate.push({
-                          id: parseInt(dataKey, 10),
-                          label: data[dataKey]
+    return this.authHttp.get(VideoService.BASE_VIDEO_URL + attributeName)
+                        .subscribe(data => {
+                          Object.keys(data).forEach(dataKey => {
+                            hashToPopulate.push({
+                              id: parseInt(dataKey, 10),
+                              label: data[dataKey]
+                            })
+                          })
                         })
-                      })
-                    })
   }
 }
diff --git a/client/src/app/videos/video-list/video-list.component.ts b/client/src/app/videos/video-list/video-list.component.ts
index 4ac539960..590632063 100644
--- a/client/src/app/videos/video-list/video-list.component.ts
+++ b/client/src/app/videos/video-list/video-list.component.ts
@@ -8,11 +8,12 @@ import { NotificationsService } from 'angular2-notifications'
 import {
   SortField,
   Video,
-  VideoService
+  VideoService,
+  VideoPagination
 } from '../shared'
 import { AuthService, AuthUser } from '../../core'
-import { RestPagination, Search, SearchField } from '../../shared'
-import { SearchService } from '../../shared'
+import { Search, SearchField, SearchService } from '../../shared'
+import {  } from '../../shared'
 
 @Component({
   selector: 'my-videos-list',
@@ -21,7 +22,7 @@ import { SearchService } from '../../shared'
 })
 export class VideoListComponent implements OnInit, OnDestroy {
   loading: BehaviorSubject<boolean> = new BehaviorSubject(false)
-  pagination: RestPagination = {
+  pagination: VideoPagination = {
     currentPage: 1,
     itemsPerPage: 25,
     totalItems: null
@@ -152,6 +153,6 @@ export class VideoListComponent implements OnInit, OnDestroy {
 
   private navigateToNewParams () {
     const routeParams = this.buildRouteParams()
-    this.router.navigate(['/videos/list', routeParams])
+    this.router.navigate([ '/videos/list', routeParams ])
   }
 }
diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss
index 285339d42..cd573841d 100644
--- a/client/src/sass/application.scss
+++ b/client/src/sass/application.scss
@@ -1,4 +1,6 @@
-@import '../../node_modules/video.js/dist/video-js.css';
+@import '~primeng/resources/themes/bootstrap/theme.css';
+@import '~primeng/resources/primeng.css';
+@import '~video.js/dist/video-js.css';
 @import './video-js-custom.scss';
 
 [hidden] {
@@ -45,23 +47,13 @@ input.readonly {
   }
 }
 
-/* some fixes for ng2-smart-table */
-ng2-smart-table {
-  thead tr {
-    border-top: 1px solid rgb(233, 235, 236)
-  }
-
-  td, th {
-    padding: 8px !important;
-    color: #333333 !important;
-    font-size: 14px !important;
-  }
+/* ngprime data table customizations */
+p-datatable {
+  .action-cell {
+    text-align: center;
 
-  .ng2-smart-pagination-nav .page-link {
-    font-size: 11px !important;
-  }
-
-  .glyphicon {
-    font-family: 'Glyphicons Halflings' !important;
+    .glyphicon {
+      cursor: pointer;
+    }
   }
 }
diff --git a/client/yarn.lock b/client/yarn.lock
index 9478e23b2..3552dbf99 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -4436,10 +4436,6 @@ ng-router-loader@^2.0.0:
     loader-utils "^0.2.16"
     recast "^0.11.20"
 
-ng2-completer@^1.2.2:
-  version "1.6.1"
-  resolved "https://registry.yarnpkg.com/ng2-completer/-/ng2-completer-1.6.1.tgz#62bad1a0a1d99c62b15f6723911ee0a3a00c91bb"
-
 ng2-file-upload@^1.1.4-2:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/ng2-file-upload/-/ng2-file-upload-1.2.1.tgz#5563c5dfd6f43fbfbe815c206e343464a0a6a197"
@@ -4448,13 +4444,6 @@ ng2-material-dropdown@0.7.10:
   version "0.7.10"
   resolved "https://registry.yarnpkg.com/ng2-material-dropdown/-/ng2-material-dropdown-0.7.10.tgz#093471f2a9cadd726cbcb120b0ad7818a54fa5ed"
 
-ng2-smart-table@1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/ng2-smart-table/-/ng2-smart-table-1.2.1.tgz#b25102c1a8b0588c508cf913c539ddf0f0b3341d"
-  dependencies:
-    lodash "^4.17.4"
-    ng2-completer "^1.2.2"
-
 ngc-webpack@3.2.2:
   version "3.2.2"
   resolved "https://registry.yarnpkg.com/ngc-webpack/-/ngc-webpack-3.2.2.tgz#1905c40e3c7d30c86fe029c7a7fda71cb4dc59df"
@@ -5340,6 +5329,10 @@ pretty-error@^2.0.2:
     renderkid "^2.0.1"
     utila "~0.4"
 
+primeng@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/primeng/-/primeng-4.2.0.tgz#49c8c99de26d254f41d3fbb8759227fe1d269772"
+
 private@^0.1.6, private@^0.1.7, private@~0.1.5:
   version "0.1.7"
   resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1"
diff --git a/server/lib/friends.ts b/server/lib/friends.ts
index ea9ddbe8d..c0dd24c53 100644
--- a/server/lib/friends.ts
+++ b/server/lib/friends.ts
@@ -190,6 +190,7 @@ function quitFriends () {
       .catch(err => {
         logger.error('Some errors while quitting friends.', err)
         // Don't stop the process
+        return pods
       })
     })
     .then(pods => {
diff --git a/shared/models/users/index.ts b/shared/models/users/index.ts
index 45dbc7b8f..efb58c320 100644
--- a/shared/models/users/index.ts
+++ b/shared/models/users/index.ts
@@ -1,5 +1,7 @@
 export * from './user.model'
 export * from './user-create.model'
+export * from './user-login.model'
+export * from './user-refresh-token.model'
 export * from './user-update.model'
 export * from './user-update-me.model'
 export * from './user-role.type'
diff --git a/shared/models/users/user-login.model.ts b/shared/models/users/user-login.model.ts
new file mode 100644
index 000000000..b0383c695
--- /dev/null
+++ b/shared/models/users/user-login.model.ts
@@ -0,0 +1,5 @@
+export interface UserLogin {
+  access_token: string
+  refresh_token: string
+  token_type: string
+}
\ No newline at end of file
diff --git a/shared/models/users/user-refresh-token.model.ts b/shared/models/users/user-refresh-token.model.ts
new file mode 100644
index 000000000..f528dd961
--- /dev/null
+++ b/shared/models/users/user-refresh-token.model.ts
@@ -0,0 +1,4 @@
+export interface UserRefreshToken {
+  access_token: string
+  refresh_token: string
+}
diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts
index 6277dbe59..75070bfd6 100644
--- a/shared/models/videos/video.model.ts
+++ b/shared/models/videos/video.model.ts
@@ -9,8 +9,8 @@ export interface Video {
   id: number
   uuid: string
   author: string
-  createdAt: Date
-  updatedAt: Date
+  createdAt: Date | string
+  updatedAt: Date | string
   categoryLabel: string
   category: number
   licenceLabel: string