Client: replace simple tables by ng2 smart table component

This commit is contained in:
Chocobozzz 2017-01-30 22:41:14 +01:00
parent 13fc89f4a4
commit 28798b5d94
17 changed files with 263 additions and 162 deletions

View File

@ -58,6 +58,7 @@
"ng2-bootstrap": "1.1.16-10", "ng2-bootstrap": "1.1.16-10",
"ng2-file-upload": "^1.1.4-2", "ng2-file-upload": "^1.1.4-2",
"ng2-meta": "https://github.com/chocobozzz/ng2-meta#build", "ng2-meta": "https://github.com/chocobozzz/ng2-meta#build",
"ng2-smart-table": "^0.5.1-0",
"ngc-webpack": "1.1.0", "ngc-webpack": "1.1.0",
"node-sass": "^4.1.1", "node-sass": "^4.1.1",
"normalize.css": "^5.0.0", "normalize.css": "^5.0.0",

View File

@ -1,29 +1,11 @@
<h3>Friends list</h3> <h3>Friends list</h3>
<table class="table table-hover"> <ng2-smart-table [settings]="tableSettings" [source]="friendsSource"></ng2-smart-table>
<thead>
<tr>
<th class="table-column-id">ID</th>
<th>Host</th>
<th>Score</th>
<th>Created Date</th>
</tr>
</thead>
<tbody> <a *ngIf="hasFriends()" class="add-user btn btn-danger pull-left" (click)="quitFriends()">
<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()">
Quit friends Quit friends
</a> </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 Make friends
</a> </a>

View File

@ -1,8 +1,10 @@
import { Component, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { NotificationsService } from 'angular2-notifications'; import { NotificationsService } from 'angular2-notifications';
import { ServerDataSource } from 'ng2-smart-table';
import { ConfirmService } from '../../../core'; import { ConfirmService } from '../../../core';
import { Utils } from '../../../shared';
import { Friend, FriendService } from '../shared'; import { Friend, FriendService } from '../shared';
@Component({ @Component({
@ -10,17 +12,51 @@ import { Friend, FriendService } from '../shared';
templateUrl: './friend-list.component.html', templateUrl: './friend-list.component.html',
styleUrls: [ './friend-list.component.scss' ] styleUrls: [ './friend-list.component.scss' ]
}) })
export class FriendListComponent implements OnInit { export class FriendListComponent {
friends: Friend[]; 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( constructor(
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private confirmService: ConfirmService, private confirmService: ConfirmService,
private friendService: FriendService private friendService: FriendService
) { } ) {
this.friendsSource = this.friendService.getDataSource();
}
ngOnInit() { hasFriends() {
this.getFriends(); return this.friendsSource.count() != 0;
} }
quitFriends() { quitFriends() {
@ -33,7 +69,7 @@ export class FriendListComponent implements OnInit {
status => { status => {
this.notificationsService.success('Sucess', 'Friends left!'); this.notificationsService.success('Sucess', 'Friends left!');
this.getFriends(); this.friendsSource.refresh();
}, },
err => this.notificationsService.error('Error', err.text) 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)
);
}
} }

View File

@ -3,8 +3,10 @@ import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map'; import 'rxjs/add/operator/map';
import { ServerDataSource } from 'ng2-smart-table';
import { Friend } from './friend.model'; import { Friend } from './friend.model';
import { AuthHttp, RestExtractor, ResultList } from '../../../shared'; import { AuthHttp, RestExtractor, RestDataSource, ResultList } from '../../../shared';
@Injectable() @Injectable()
export class FriendService { export class FriendService {
@ -15,11 +17,8 @@ export class FriendService {
private restExtractor: RestExtractor private restExtractor: RestExtractor
) {} ) {}
getFriends() { getDataSource() {
return this.authHttp.get(FriendService.BASE_FRIEND_URL) return new RestDataSource(this.authHttp, FriendService.BASE_FRIEND_URL);
.map(this.restExtractor.extractDataList)
.map(this.extractFriends)
.catch((res) => this.restExtractor.handleError(res));
} }
makeFriends(notEmptyHosts) { makeFriends(notEmptyHosts) {
@ -37,11 +36,4 @@ export class FriendService {
.map(res => res.status) .map(res => res.status)
.catch((res) => this.restExtractor.handleError(res)); .catch((res) => this.restExtractor.handleError(res));
} }
private extractFriends(result: ResultList) {
const friends: Friend[] = result.data;
const totalFriends = result.total;
return { friends, totalFriends };
}
} }

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map'; import 'rxjs/add/operator/map';
import { AuthHttp, RestExtractor, ResultList, User } from '../../../shared'; import { AuthHttp, RestExtractor, RestDataSource, User } from '../../../shared';
@Injectable() @Injectable()
export class UserService { export class UserService {
@ -25,25 +25,11 @@ export class UserService {
.catch(this.restExtractor.handleError); .catch(this.restExtractor.handleError);
} }
getUsers() { getDataSource() {
return this.authHttp.get(UserService.BASE_USERS_URL) return new RestDataSource(this.authHttp, UserService.BASE_USERS_URL);
.map(this.restExtractor.extractDataList)
.map(this.extractUsers)
.catch((res) => this.restExtractor.handleError(res));
} }
removeUser(user: User) { removeUser(user: User) {
return this.authHttp.delete(UserService.BASE_USERS_URL + user.id); 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 };
}
} }

View File

@ -1,26 +1,9 @@
<h3>Users list</h3> <h3>Users list</h3>
<table class="table table-hover"> <ng2-smart-table
<thead> [settings]="tableSettings" [source]="usersSource"
<tr> (delete)="removeUser($event)"
<th class="table-column-id">ID</th> ></ng2-smart-table>
<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>
<a class="add-user btn btn-success pull-right" [routerLink]="['/admin/users/add']"> <a class="add-user btn btn-success pull-right" [routerLink]="['/admin/users/add']">
<span class="glyphicon glyphicon-plus"></span> <span class="glyphicon glyphicon-plus"></span>

View File

@ -1,9 +1,9 @@
import { Component, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { NotificationsService } from 'angular2-notifications'; import { NotificationsService } from 'angular2-notifications';
import { ConfirmService } from '../../../core'; import { ConfirmService } from '../../../core';
import { User } from '../../../shared'; import { User, Utils } from '../../../shared';
import { UserService } from '../shared'; import { UserService } from '../shared';
@Component({ @Component({
@ -11,33 +11,62 @@ import { UserService } from '../shared';
templateUrl: './user-list.component.html', templateUrl: './user-list.component.html',
styleUrls: [ './user-list.component.scss' ] styleUrls: [ './user-list.component.scss' ]
}) })
export class UserListComponent implements OnInit { export class UserListComponent {
totalUsers: number; usersSource = null;
users: User[]; 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( constructor(
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private confirmService: ConfirmService, private confirmService: ConfirmService,
private userService: UserService private userService: UserService
) {} ) {
this.usersSource = this.userService.getDataSource();
ngOnInit() {
this.getUsers();
} }
getUsers() { removeUser({ data }) {
this.userService.getUsers().subscribe( const user: User = data;
({ users, totalUsers }) => {
this.users = users;
this.totalUsers = totalUsers;
},
err => this.notificationsService.error('Error', err.text) 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( this.confirmService.confirm('Do you really want to delete this user?', 'Delete').subscribe(
res => { res => {
if (res === false) return; if (res === false) return;
@ -45,7 +74,7 @@ export class UserListComponent implements OnInit {
this.userService.removeUser(user).subscribe( this.userService.removeUser(user).subscribe(
() => { () => {
this.notificationsService.success('Success', `User ${user.username} deleted.`); this.notificationsService.success('Success', `User ${user.username} deleted.`);
this.getUsers(); this.usersSource.refresh();
}, },
err => this.notificationsService.error('Error', err.text) err => this.notificationsService.error('Error', err.text)

View File

@ -1,27 +1,5 @@
<h3>Video abuses list</h3> <h3>Video abuses list</h3>
<table class="table table-hover"> <ng2-smart-table
<thead> [settings]="tableSettings" [source]="videoAbusesSource"
<tr> ></ng2-smart-table>
<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>

View File

@ -1,35 +1,72 @@
import { Component, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { NotificationsService } from 'angular2-notifications'; import { NotificationsService } from 'angular2-notifications';
import { VideoAbuseService, VideoAbuse} from '../../../shared'; import { Utils, VideoAbuseService, VideoAbuse} from '../../../shared';
@Component({ @Component({
selector: 'my-video-abuse-list', selector: 'my-video-abuse-list',
templateUrl: './video-abuse-list.component.html', templateUrl: './video-abuse-list.component.html',
styleUrls: [ './video-abuse-list.component.scss' ] styleUrls: [ './video-abuse-list.component.scss' ]
}) })
export class VideoAbuseListComponent implements OnInit { export class VideoAbuseListComponent {
videoAbuses: VideoAbuse[]; 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( constructor(
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private videoAbuseService: VideoAbuseService private videoAbuseService: VideoAbuseService
) { } ) {
this.videoAbusesSource = this.videoAbuseService.getDataSource();
}
ngOnInit() { buildVideoLink(videoId: string) {
this.getVideoAbuses(); // 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>`;
buildVideoLink(videoAbuse: VideoAbuse) {
return `/videos/${videoAbuse.videoId}`;
}
private getVideoAbuses() {
this.videoAbuseService.getVideoAbuses().subscribe(
res => this.videoAbuses = res.videoAbuses,
err => this.notificationsService.error('Error', err.text)
);
} }
} }

View File

@ -5,3 +5,4 @@ export * from './search';
export * from './users'; export * from './users';
export * from './video-abuse'; export * from './video-abuse';
export * from './shared.module'; export * from './shared.module';
export * from './utils';

View File

@ -1,3 +1,4 @@
export * from './rest-data-source';
export * from './rest-extractor.service'; export * from './rest-extractor.service';
export * from './rest-pagination'; export * from './rest-pagination';
export * from './rest.service'; export * from './rest.service';

View File

@ -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;
}
}

View File

@ -10,6 +10,7 @@ import { ProgressbarModule } from 'ng2-bootstrap/progressbar';
import { PaginationModule } from 'ng2-bootstrap/pagination'; import { PaginationModule } from 'ng2-bootstrap/pagination';
import { ModalModule } from 'ng2-bootstrap/modal'; import { ModalModule } from 'ng2-bootstrap/modal';
import { FileUploadModule } from 'ng2-file-upload/ng2-file-upload'; import { FileUploadModule } from 'ng2-file-upload/ng2-file-upload';
import { Ng2SmartTableModule } from 'ng2-smart-table';
import { AUTH_HTTP_PROVIDERS } from './auth'; import { AUTH_HTTP_PROVIDERS } from './auth';
import { RestExtractor, RestService } from './rest'; import { RestExtractor, RestService } from './rest';
@ -29,7 +30,8 @@ import { VideoAbuseService } from './video-abuse';
PaginationModule.forRoot(), PaginationModule.forRoot(),
ProgressbarModule.forRoot(), ProgressbarModule.forRoot(),
FileUploadModule FileUploadModule,
Ng2SmartTableModule
], ],
declarations: [ declarations: [
@ -49,6 +51,7 @@ import { VideoAbuseService } from './video-abuse';
ModalModule, ModalModule,
PaginationModule, PaginationModule,
ProgressbarModule, ProgressbarModule,
Ng2SmartTableModule,
BytesPipe, BytesPipe,
SearchComponent SearchComponent

View File

@ -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>';
}
}

View File

@ -6,7 +6,7 @@ import 'rxjs/add/operator/map';
import { AuthService } from '../core'; import { AuthService } from '../core';
import { AuthHttp } from '../auth'; import { AuthHttp } from '../auth';
import { RestExtractor, ResultList } from '../rest'; import { RestDataSource, RestExtractor, ResultList } from '../rest';
import { VideoAbuse } from './video-abuse.model'; import { VideoAbuse } from './video-abuse.model';
@Injectable() @Injectable()
@ -18,10 +18,8 @@ export class VideoAbuseService {
private restExtractor: RestExtractor private restExtractor: RestExtractor
) {} ) {}
getVideoAbuses() { getDataSource() {
return this.authHttp.get(VideoAbuseService.BASE_VIDEO_ABUSE_URL + 'abuse') return new RestDataSource(this.authHttp, VideoAbuseService.BASE_VIDEO_ABUSE_URL + 'abuse');
.map(this.restExtractor.extractDataList)
.map(this.extractVideoAbuses)
} }
reportVideo(id: string, reason: string) { reportVideo(id: string, reason: string) {

View File

@ -42,8 +42,23 @@ menu {
} }
} }
.table-column-id { .ng2-smart-table-container {
width: 200px; .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] { [hidden] {
@ -55,6 +70,10 @@ input.readonly {
background-color: #fff !important; background-color: #fff !important;
} }
.glyphicon-black {
color: black;
}
footer { footer {
border-top: 1px solid rgba(0, 0, 0, 0.2); border-top: 1px solid rgba(0, 0, 0, 0.2);
padding-top: 10px; padding-top: 10px;

View File

@ -18,8 +18,8 @@ const SEARCHABLE_COLUMNS = {
// Sortable columns per schema // Sortable columns per schema
const SORTABLE_COLUMNS = { const SORTABLE_COLUMNS = {
USERS: [ 'username', '-username', 'createdAt', '-createdAt' ], USERS: [ 'id', '-id', 'username', '-username', 'createdAt', '-createdAt' ],
VIDEO_ABUSES: [ 'createdAt', '-createdAt' ], VIDEO_ABUSES: [ 'id', '-id', 'createdAt', '-createdAt' ],
VIDEOS: [ 'name', '-name', 'duration', '-duration', 'createdAt', '-createdAt' ] VIDEOS: [ 'name', '-name', 'duration', '-duration', 'createdAt', '-createdAt' ]
} }