Client: replace simple tables by ng2 smart table component
authorChocobozzz <florian.bigard@gmail.com>
Mon, 30 Jan 2017 21:41:14 +0000 (22:41 +0100)
committerChocobozzz <florian.bigard@gmail.com>
Mon, 30 Jan 2017 21:41:14 +0000 (22:41 +0100)
17 files changed:
client/package.json
client/src/app/+admin/friends/friend-list/friend-list.component.html
client/src/app/+admin/friends/friend-list/friend-list.component.ts
client/src/app/+admin/friends/shared/friend.service.ts
client/src/app/+admin/users/shared/user.service.ts
client/src/app/+admin/users/user-list/user-list.component.html
client/src/app/+admin/users/user-list/user-list.component.ts
client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html
client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.ts
client/src/app/shared/index.ts
client/src/app/shared/rest/index.ts
client/src/app/shared/rest/rest-data-source.ts [new file with mode: 0644]
client/src/app/shared/shared.module.ts
client/src/app/shared/utils.ts [new file with mode: 0644]
client/src/app/shared/video-abuse/video-abuse.service.ts
client/src/sass/application.scss
server/initializers/constants.js

index bd6cb03e73eac7dd0aa5ece679ecfe3d44cd0a91..c762a1fd52552b4004d6897f1da576c97321f56f 100644 (file)
@@ -58,6 +58,7 @@
     "ng2-bootstrap": "1.1.16-10",
     "ng2-file-upload": "^1.1.4-2",
     "ng2-meta": "https://github.com/chocobozzz/ng2-meta#build",
+    "ng2-smart-table": "^0.5.1-0",
     "ngc-webpack": "1.1.0",
     "node-sass": "^4.1.1",
     "normalize.css": "^5.0.0",
index 06258f8c8a20f20e7f51a0161be27e892fcd21c6..254d0c65e34cead3498dda00d518ccfb757e8983 100644 (file)
@@ -1,29 +1,11 @@
 <h3>Friends list</h3>
 
-<table class="table table-hover">
-  <thead>
-    <tr>
-      <th class="table-column-id">ID</th>
-      <th>Host</th>
-      <th>Score</th>
-      <th>Created Date</th>
-    </tr>
-  </thead>
+<ng2-smart-table [settings]="tableSettings" [source]="friendsSource"></ng2-smart-table>
 
-  <tbody>
-    <tr *ngFor="let friend of friends">
-      <td>{{ friend.id }}</td>
-      <td>{{ friend.host }}</td>
-      <td>{{ friend.score }}</td>
-      <td>{{ friend.createdAt | date: 'medium' }}</td>
-    </tr>
-  </tbody>
-</table>
-
-<a *ngIf="friends && friends.length !== 0" class="add-user btn btn-danger pull-left" (click)="quitFriends()">
+<a *ngIf="hasFriends()" class="add-user btn btn-danger pull-left" (click)="quitFriends()">
   Quit friends
 </a>
 
-<a *ngIf="friends?.length === 0" class="add-user btn btn-success pull-right" [routerLink]="['/admin/friends/add']">
+<a *ngIf="!hasFriends()" class="add-user btn btn-success pull-right" [routerLink]="['/admin/friends/add']">
   Make friends
 </a>
index 175ad9cba8938cf489d9d9188462958a8f358c4a..f294276402a755027f7e5d8a5a3f32392a521784 100644 (file)
@@ -1,8 +1,10 @@
-import { Component, OnInit } from '@angular/core';
+import { Component } from '@angular/core';
 
 import { NotificationsService } from 'angular2-notifications';
+import { ServerDataSource } from 'ng2-smart-table';
 
 import { ConfirmService } from '../../../core';
+import { Utils } from '../../../shared';
 import { Friend, FriendService } from '../shared';
 
 @Component({
@@ -10,17 +12,51 @@ import { Friend, FriendService } from '../shared';
   templateUrl: './friend-list.component.html',
   styleUrls: [ './friend-list.component.scss' ]
 })
-export class FriendListComponent implements OnInit {
-  friends: Friend[];
+export class FriendListComponent {
+  friendsSource = null;
+  tableSettings = {
+    attr: {
+      class: 'table-hover'
+    },
+    hideSubHeader: true,
+    actions: {
+      position: 'right',
+      add: false,
+      edit: false,
+      delete: false
+    },
+    columns: {
+      id: {
+        title: 'ID',
+        sort: false,
+        sortDirection: 'asc'
+      },
+      host: {
+        title: 'Host',
+        sort: false
+      },
+      score: {
+        title: 'Score',
+        sort: false
+      },
+      createdAt: {
+        title: 'Created Date',
+        sort: false,
+        valuePrepareFunction: Utils.dateToHuman
+      }
+    }
+  }
 
   constructor(
     private notificationsService: NotificationsService,
     private confirmService: ConfirmService,
     private friendService: FriendService
-  ) {  }
+  ) {
+    this.friendsSource = this.friendService.getDataSource();
+  }
 
-  ngOnInit() {
-    this.getFriends();
+  hasFriends() {
+    return this.friendsSource.count() != 0;
   }
 
   quitFriends() {
@@ -33,7 +69,7 @@ export class FriendListComponent implements OnInit {
           status => {
             this.notificationsService.success('Sucess', 'Friends left!');
 
-            this.getFriends();
+            this.friendsSource.refresh();
           },
 
           err => this.notificationsService.error('Error', err.text)
@@ -41,12 +77,4 @@ export class FriendListComponent implements OnInit {
       }
     );
   }
-
-  private getFriends() {
-    this.friendService.getFriends().subscribe(
-      res => this.friends = res.friends,
-
-      err => this.notificationsService.error('Error', err.text)
-    );
-  }
 }
index e97459385dd4aa9e842cf2beb67fbcdfb28a1449..6cb84f5cde09ca41aaeadde480979de3e08bd642 100644 (file)
@@ -3,8 +3,10 @@ import { Observable } from 'rxjs/Observable';
 import 'rxjs/add/operator/catch';
 import 'rxjs/add/operator/map';
 
+import { ServerDataSource } from 'ng2-smart-table';
+
 import { Friend } from './friend.model';
-import { AuthHttp, RestExtractor, ResultList } from '../../../shared';
+import { AuthHttp, RestExtractor, RestDataSource, ResultList } from '../../../shared';
 
 @Injectable()
 export class FriendService {
@@ -15,11 +17,8 @@ export class FriendService {
     private restExtractor: RestExtractor
   ) {}
 
-  getFriends() {
-    return this.authHttp.get(FriendService.BASE_FRIEND_URL)
-                        .map(this.restExtractor.extractDataList)
-                        .map(this.extractFriends)
-                        .catch((res) => this.restExtractor.handleError(res));
+  getDataSource() {
+    return new RestDataSource(this.authHttp, FriendService.BASE_FRIEND_URL);
   }
 
   makeFriends(notEmptyHosts) {
@@ -37,11 +36,4 @@ export class FriendService {
                         .map(res => res.status)
                         .catch((res) => this.restExtractor.handleError(res));
   }
-
-  private extractFriends(result: ResultList) {
-    const friends: Friend[] = result.data;
-    const totalFriends = result.total;
-
-    return { friends, totalFriends };
-  }
 }
index d9005b213e510a75ce1aa8e886eef0006ae58baa..f6d360e096b933b866b1913514a94ba785fbddc2 100644 (file)
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
 import 'rxjs/add/operator/catch';
 import 'rxjs/add/operator/map';
 
-import { AuthHttp, RestExtractor, ResultList, User } from '../../../shared';
+import { AuthHttp, RestExtractor, RestDataSource, User } from '../../../shared';
 
 @Injectable()
 export class UserService {
@@ -25,25 +25,11 @@ export class UserService {
                         .catch(this.restExtractor.handleError);
   }
 
-  getUsers() {
-    return this.authHttp.get(UserService.BASE_USERS_URL)
-                 .map(this.restExtractor.extractDataList)
-                 .map(this.extractUsers)
-                 .catch((res) => this.restExtractor.handleError(res));
+  getDataSource() {
+    return new RestDataSource(this.authHttp, UserService.BASE_USERS_URL);
   }
 
   removeUser(user: User) {
     return this.authHttp.delete(UserService.BASE_USERS_URL + user.id);
   }
-
-  private extractUsers(result: ResultList) {
-    const usersJson = result.data;
-    const totalUsers = result.total;
-    const users = [];
-    for (const userJson of usersJson) {
-      users.push(new User(userJson));
-    }
-
-    return { users, totalUsers };
-  }
 }
index 36193d119c162b2847629f0c2a0477f642bc5d24..3d3d7e054a1b211a8da0c959f26c6845bac7084b 100644 (file)
@@ -1,26 +1,9 @@
 <h3>Users list</h3>
 
-<table class="table table-hover">
-  <thead>
-    <tr>
-      <th class="table-column-id">ID</th>
-      <th>Username</th>
-      <th>Created Date</th>
-      <th class="text-right">Remove</th>
-    </tr>
-  </thead>
-
-  <tbody>
-    <tr *ngFor="let user of users">
-      <td>{{ user.id }}</td>
-      <td>{{ user.username }}</td>
-      <td>{{ user.createdAt | date: 'medium' }}</td>
-      <td class="text-right">
-        <span class="glyphicon glyphicon-remove" *ngIf="!user.isAdmin()" (click)="removeUser(user)"></span>
-      </td>
-    </tr>
-  </tbody>
-</table>
+<ng2-smart-table
+  [settings]="tableSettings" [source]="usersSource"
+  (delete)="removeUser($event)"
+></ng2-smart-table>
 
 <a class="add-user btn btn-success pull-right" [routerLink]="['/admin/users/add']">
   <span class="glyphicon glyphicon-plus"></span>
index baefb7064ce60fa30d99cbce4e06325ed7551006..db025d3a82a71d6a5680cbc4eddb41747f5bbc55 100644 (file)
@@ -1,9 +1,9 @@
-import { Component, OnInit } from '@angular/core';
+import { Component } from '@angular/core';
 
 import { NotificationsService } from 'angular2-notifications';
 
 import { ConfirmService } from '../../../core';
-import { User } from '../../../shared';
+import { User, Utils } from '../../../shared';
 import { UserService } from '../shared';
 
 @Component({
@@ -11,33 +11,62 @@ import { UserService } from '../shared';
   templateUrl: './user-list.component.html',
   styleUrls: [ './user-list.component.scss' ]
 })
-export class UserListComponent implements OnInit {
-  totalUsers: number;
-  users: User[];
+export class UserListComponent {
+  usersSource = null;
+  tableSettings = {
+    mode: 'external',
+    attr: {
+      class: 'table-hover'
+    },
+    hideSubHeader: true,
+    actions: {
+      position: 'right',
+      add: false,
+      edit: false,
+      delete: true
+    },
+    delete: {
+      deleteButtonContent: Utils.getRowDeleteButton()
+    },
+    pager: {
+      display: true,
+      perPage: 10
+    },
+    columns: {
+      id: {
+        title: 'ID',
+        sortDirection: 'asc'
+      },
+      username: {
+        title: 'Username'
+      },
+      role: {
+        title: 'Role',
+        sort: false
+      },
+      createdAt: {
+        title: 'Created Date',
+        valuePrepareFunction: Utils.dateToHuman
+      }
+    }
+  }
 
   constructor(
     private notificationsService: NotificationsService,
     private confirmService: ConfirmService,
     private userService: UserService
-  ) {}
-
-  ngOnInit() {
-    this.getUsers();
+  ) {
+    this.usersSource = this.userService.getDataSource();
   }
 
-  getUsers() {
-    this.userService.getUsers().subscribe(
-      ({ users, totalUsers }) => {
-        this.users = users;
-        this.totalUsers = totalUsers;
-      },
-
-      err => this.notificationsService.error('Error', err.text)
-    );
-  }
+  removeUser({ data }) {
+    const user: User = data;
 
+    if (user.username === 'root') {
+      this.notificationsService.error('Error', 'You cannot delete root.');
+      return;
+    }
 
-  removeUser(user: User) {
     this.confirmService.confirm('Do you really want to delete this user?', 'Delete').subscribe(
       res => {
         if (res === false) return;
@@ -45,7 +74,7 @@ export class UserListComponent implements OnInit {
         this.userService.removeUser(user).subscribe(
           () => {
             this.notificationsService.success('Success', `User ${user.username} deleted.`);
-            this.getUsers();
+            this.usersSource.refresh();
           },
 
           err => this.notificationsService.error('Error', err.text)
index 46043577c96b9bc1e38ba2da19585dec4aa083d6..b2fd17bf0c82abc3ed5779e7bc6fbb38fdb15492 100644 (file)
@@ -1,27 +1,5 @@
 <h3>Video abuses list</h3>
 
-<table class="table table-hover">
-  <thead>
-    <tr>
-      <th class="cell-id">ID</th>
-      <th class="cell-reason">Reason</th>
-      <th>Reporter pod host</th>
-      <th>Reporter username</th>
-      <th>Video</th>
-      <th>Created at</th>
-    </tr>
-  </thead>
-
-  <tbody>
-    <tr *ngFor="let videoAbuse of videoAbuses">
-      <td>{{ videoAbuse.id }}</td>
-      <td>{{ videoAbuse.reason }}</td>
-      <td>{{ videoAbuse.reporterPodHost }}</td>
-      <td>{{ videoAbuse.reporterUsername }}</td>
-      <td>
-        <a [routerLink]="buildVideoLink(videoAbuse)" title="Go to video">{{ videoAbuse.videoId }}</a>
-      </td>
-      <td>{{ videoAbuse.createdAt | date: 'medium' }}</td>
-    </tr>
-  </tbody>
-</table>
+<ng2-smart-table
+  [settings]="tableSettings" [source]="videoAbusesSource"
+></ng2-smart-table>
index cfd9151b08050811ba4b731b5a4f4fbaa3784c90..2f22a4ab07998c2df85ea7d4ae929a5f3c6c48e1 100644 (file)
@@ -1,35 +1,72 @@
-import { Component, OnInit } from '@angular/core';
+import { Component } from '@angular/core';
 
 import { NotificationsService } from 'angular2-notifications';
 
-import { VideoAbuseService, VideoAbuse} from '../../../shared';
+import { Utils, VideoAbuseService, VideoAbuse} from '../../../shared';
 
 @Component({
        selector: 'my-video-abuse-list',
        templateUrl: './video-abuse-list.component.html',
   styleUrls: [ './video-abuse-list.component.scss' ]
 })
-export class VideoAbuseListComponent implements OnInit {
-  videoAbuses: VideoAbuse[];
+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
+      }
+    }
+  }
 
   constructor(
     private notificationsService: NotificationsService,
     private videoAbuseService: VideoAbuseService
-  ) {  }
-
-  ngOnInit() {
-    this.getVideoAbuses();
-  }
-
-  buildVideoLink(videoAbuse: VideoAbuse) {
-    return `/videos/${videoAbuse.videoId}`;
-  }
-
-  private getVideoAbuses() {
-    this.videoAbuseService.getVideoAbuses().subscribe(
-      res => this.videoAbuses = res.videoAbuses,
+  ) {
+    this.videoAbusesSource = this.videoAbuseService.getDataSource();
+   }
 
-      err => this.notificationsService.error('Error', err.text)
-    );
+  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>`;
   }
 }
index 7876e6f140da267f60b7cb3cbc44f205147d0db5..61e8ed5232c73e3d0b744346e44b6f797e45b0bc 100644 (file)
@@ -5,3 +5,4 @@ export * from './search';
 export * from './users';
 export * from './video-abuse';
 export * from './shared.module';
+export * from './utils';
index 3c9509dc7be6a59f95e2f989f69052caa5724d1a..3cb123c3b8a1c59f4a30a18337833da08e7adbc0 100644 (file)
@@ -1,3 +1,4 @@
+export * from './rest-data-source';
 export * from './rest-extractor.service';
 export * from './rest-pagination';
 export * from './rest.service';
diff --git a/client/src/app/shared/rest/rest-data-source.ts b/client/src/app/shared/rest/rest-data-source.ts
new file mode 100644 (file)
index 0000000..847dd7c
--- /dev/null
@@ -0,0 +1,51 @@
+import { Http, RequestOptionsArgs, URLSearchParams,  } from '@angular/http';
+
+import { ServerDataSource } from 'ng2-smart-table';
+
+export class RestDataSource extends ServerDataSource {
+ constructor(http: Http, endpoint: string) {
+   const options = {
+     endPoint: endpoint,
+     sortFieldKey: 'sort',
+     dataKey: 'data'
+   }
+
+   super(http, options);
+ }
+
+ protected extractTotalFromResponse(res) {
+    const rawData = res.json();
+    return rawData ? parseInt(rawData.total): 0;
+  }
+
+ protected addSortRequestOptions(requestOptions: RequestOptionsArgs) {
+    let searchParams: URLSearchParams = <URLSearchParams> requestOptions.search;
+
+    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) {
+    let searchParams: URLSearchParams = <URLSearchParams> requestOptions.search;
+
+    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;
+  }
+}
index 7b2386d6cf584ec9f3c52487c4e834b1983bd897..99893c8b1335fae6e2c6480eb16c05eea27d70f6 100644 (file)
@@ -10,6 +10,7 @@ import { ProgressbarModule } from 'ng2-bootstrap/progressbar';
 import { PaginationModule } from 'ng2-bootstrap/pagination';
 import { ModalModule } from 'ng2-bootstrap/modal';
 import { FileUploadModule } from 'ng2-file-upload/ng2-file-upload';
+import { Ng2SmartTableModule } from 'ng2-smart-table';
 
 import { AUTH_HTTP_PROVIDERS } from './auth';
 import { RestExtractor, RestService } from './rest';
@@ -29,7 +30,8 @@ import { VideoAbuseService } from './video-abuse';
     PaginationModule.forRoot(),
     ProgressbarModule.forRoot(),
 
-    FileUploadModule
+    FileUploadModule,
+    Ng2SmartTableModule
   ],
 
   declarations: [
@@ -49,6 +51,7 @@ import { VideoAbuseService } from './video-abuse';
     ModalModule,
     PaginationModule,
     ProgressbarModule,
+    Ng2SmartTableModule,
     BytesPipe,
 
     SearchComponent
diff --git a/client/src/app/shared/utils.ts b/client/src/app/shared/utils.ts
new file mode 100644 (file)
index 0000000..1dd6f96
--- /dev/null
@@ -0,0 +1,12 @@
+import { DatePipe } from '@angular/common';
+
+export class Utils {
+
+  static dateToHuman(date: String) {
+    return new DatePipe('en').transform(date, 'medium')
+  }
+
+  static getRowDeleteButton() {
+    return '<span class="glyphicon glyphicon-remove glyphicon-black"></span>';
+  }
+}
index 2750a41c7b21ae05d0890ceb45b549c5753fd35c..f23c36f05a748596ff11688feed9045df4218a4a 100644 (file)
@@ -6,7 +6,7 @@ import 'rxjs/add/operator/map';
 
 import { AuthService } from '../core';
 import { AuthHttp } from '../auth';
-import { RestExtractor, ResultList } from '../rest';
+import { RestDataSource, RestExtractor, ResultList } from '../rest';
 import { VideoAbuse } from './video-abuse.model';
 
 @Injectable()
@@ -18,10 +18,8 @@ export class VideoAbuseService {
     private restExtractor: RestExtractor
   ) {}
 
-  getVideoAbuses() {
-    return this.authHttp.get(VideoAbuseService.BASE_VIDEO_ABUSE_URL + 'abuse')
-                        .map(this.restExtractor.extractDataList)
-                        .map(this.extractVideoAbuses)
+  getDataSource() {
+    return new RestDataSource(this.authHttp, VideoAbuseService.BASE_VIDEO_ABUSE_URL + 'abuse');
   }
 
   reportVideo(id: string, reason: string) {
index 30588067f1de617d1710659e39bc11c5dbcba9e7..994b1e2b9a341b62fabaea04e9b47f1e4e0fbac3 100644 (file)
@@ -42,8 +42,23 @@ menu {
   }
 }
 
-.table-column-id {
-  width: 200px;
+.ng2-smart-table-container {
+  .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;
+    }
+  }
+
+  .ng2-smart-pagination-nav .page-link {
+    font-size: 11px !important;
+  }
 }
 
 [hidden] {
@@ -55,6 +70,10 @@ input.readonly {
   background-color: #fff !important;
 }
 
+.glyphicon-black {
+  color: black;
+}
+
 footer {
   border-top: 1px solid rgba(0, 0, 0, 0.2);
   padding-top: 10px;
index 90adbf406269db8e219e935ff1993d299e723bd1..ad7cf4f4da9e3efb7d198af0e594899fdd3ae104 100644 (file)
@@ -18,8 +18,8 @@ const SEARCHABLE_COLUMNS = {
 
 // Sortable columns per schema
 const SORTABLE_COLUMNS = {
-  USERS: [ 'username', '-username', 'createdAt', '-createdAt' ],
-  VIDEO_ABUSES: [ 'createdAt', '-createdAt' ],
+  USERS: [ 'id', '-id', 'username', '-username', 'createdAt', '-createdAt' ],
+  VIDEO_ABUSES: [ 'id', '-id', 'createdAt', '-createdAt' ],
   VIDEOS: [ 'name', '-name', 'duration', '-duration', 'createdAt', '-createdAt' ]
 }