1

Hello everyone ! I hope you are well ?

I'm currently developing a simple application to learn more about spring-boot, spring-security, jpa and server side pagination.

I built my API successfully, and I have a controller that uses pagination. To consume this API, I develop a react application and everything works as I want (2FA, account creation, login, forgot password, change password, ...).

But now, I want to develop an administration interface in which I want to be able to manage user accounts.

So, I created a controller which requires the admin role and to be connected. To return the user list I use a DAO that extends PagingAndSortingRepository and it works fine !

Now I want to implement this pagination in my react application and that's where I'm having a problem.

I have tried many paging libraries for react. But they all need to recover all the data in a single list, which I do not want. So, I started to develop my own pagination component. I manage easily to make my front side pagination but only for accessing the first page, the last, the preceding one and the following one but, I can not add the page selection buttons like this : 13[...][n-2][n-1][n].

Currently my paging component looks like this:My pagination component And i want something like this: enter image description here

And here is my code:

User.java

@Entity
@Table(
    name = "USER",
    uniqueConstraints = {
            @UniqueConstraint(columnNames = "username"),
            @UniqueConstraint(columnNames = "email")
    }
)
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    @Size(min = 3, max = 16)
    private String username;

    @NaturalId
    @NotBlank
    @Email
    private String email;

    @NotBlank
    @Size(max = 100)
    private String password;

    private boolean isUsingTwoFA;

    private String twoFASecret;

    @Column(columnDefinition = "DATE NOT NULL")
    private LocalDate accountCreationDate;

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
        name = "USER_ROLE",
        joinColumns = @JoinColumn(name = "user_id"),
        inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private Set<Role> roles = new HashSet<>();
}

UserDto.java

public class UserDto {

    private String username;
    private String email;
    private String accountCreationDate;
    private boolean isUsingTwoFA;
    List<String> roles;
}

PagedUserRepository.java

public interface PagedUserRepository extends PagingAndSortingRepository<User, Long> {

    Page<User> findAll(Pageable pageable);
}

DashboardUserService.java

@Service
public class DashboardUsersService {

    private UserRepository userRepository;
    private PagedUserRepository pagedUserRepository;

    @Autowired
    public DashboardUsersService(UserRepository userRepository, PagedUserRepository pagedUserRepository) {
        this.userRepository = userRepository;
        this.pagedUserRepository = pagedUserRepository;
    }

    public ResponseEntity<?> getUsers(int page, int size) {

        Pageable pageable = PageRequest.of(page, size);
        Page<User> usersPage = pagedUserRepository.findAll(pageable);

        if (usersPage.getContent().isEmpty()) {
            return new ResponseEntity<>(new ApiResponseDto(false, "Unable to retrieve any user"), HttpStatus.INTERNAL_SERVER_ERROR);
        }

        final List<UserDto> users = usersPage.getContent()
                .stream()
                .map(UserDto::new)
                .collect(Collectors.toList());

        return new ResponseEntity<>(new PagedResponseDto(users, usersPage), HttpStatus.OK);
    }
}

DashboardUserController.java

@CrossOrigin(maxAge = 36000)
@RestController
@RequestMapping(path = "/api/secure/admin/dashboard/users")
public class DashboardUsersController {

    @Autowired
    private DashboardUsersService dashboardUsersService;

    @Secured("ROLE_ADMIN")
    @GetMapping
    public ResponseEntity<?> getUsers(
            @RequestParam(value = "page", defaultValue = "0") int page,
            @RequestParam(value = "size", defaultValue = "10") int size
    ) {
        return dashboardUsersService.getUsers(page, size);
    }
}

Users.js

import React, {Component} from 'react';
import {withRouter} from 'react-router-dom';
import {getPageUsers} from "../../../../api/AdminApi";
import UserTableLine from "./component/UserTableLine";
import UserPagination from "./component/UserPagination";

class Users extends Component{

    state = {
        pagedResponse: {},
        users: [],
        showLoading: false
    };

    constructor(props){
        super(props);

        this.getFirstPageUsers = this.getFirstPageUsers.bind(this);
        this.handleChangePage = this.handleChangePage.bind(this);
    }

    componentDidMount(){
        document.title = "Users management";
        this.getFirstPageUsers();
    }

    getFirstPageUsers(){
        const defaultPageable = {
            pageNumber: 0
        };
        this.setState({showLoading: true});
        getPageUsers(defaultPageable).then(res => {
            this.setState({
                pagedResponse: res,
                users: res.content,
                showLoading: false
            });
        }).catch(error => {
            if(error.message && error.success === false){
                this.props.showAlert(error.message, "error");
            } else {
                this.props.showAlert("Sorry! Something went wrong. Please try again!", "error");
            }
            this.setState({showLoading: false});
            console.log(error);
        });
    }

    handleChangePage(pageable){
        this.setState({showLoading: true});
        getPageUsers(pageable).then(res => {
            this.setState({
                pagedResponse: res,
                users: res.content,
                showLoading: false
            });
        }).catch(error => {
            if(error.message && error.success === false){
                this.props.showAlert(error.message, "error");
            } else {
                this.props.showAlert("Sorry! Something went wrong. Please try again!", "error");
            }
            this.setState({showLoading: false});
            console.log(error);
        });
    }

    render(){

        let tableLines = [];

        if(this.state.pagedResponse && this.state.users.length > 0){
            tableLines = Object.keys(this.state.users)
                .map(key => <UserTableLine key={key} user={this.state.users[key]}/>);
        }

        return(
            <div>
                <h1>Users <span className="text-muted" style={{fontSize: 11}}>management</span></h1>
                <hr/>
                {
                    this.state.showLoading
                    ?
                        <div className="align-content-center text-center">
                            <h4 className="text-muted">Loading. Please Wait...</h4>
                            <i className="material-icons w3-xxxlarge w3-spin align-content-center">refresh</i>
                        </div>
                    :
                        <div>
                            <table className="table table-hover">
                                <thead>
                                    <tr>
                                        <th scope="col">Avatar</th>
                                        <th scope="col">Username</th>
                                        <th scope="col">email</th>
                                        <th scope="col">Action</th>
                                    </tr>
                                </thead>
                                <tbody>
                                {tableLines}
                                </tbody>
                            </table>
                            <UserPagination
                                showAlert={this.props.showAlert}
                                page={this.state.pagedResponse}
                                handleChangePage={this.handleChangePage}
                            />
                        </div>
                }
            </div>
        );
    }
}

export default withRouter(Users);

UserTableLine.js

import React, {Component} from 'react';
import {withRouter} from 'react-router-dom';

import {Modal, ModalBody, ModalHeader} from 'reactstrap';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
import {faSearch} from '@fortawesome/free-solid-svg-icons';

class UserTableLine extends Component {

    state = {
        showModalUserInfo: false,
        user: {}
    };

    constructor(props) {
        super(props);

        this.toggle = this.toggle.bind(this);
    }

    componentDidMount() {
        this.setState({
            user: this.props.user
        });
    }

    toggle() {
        this.setState({
            showModalUserInfo: !this.state.showModalUserInfo
        });
    }

    render() {

        let roles;

        if (this.state.user && this.state.user.roles) {
            roles = Object.keys(this.state.user.roles).map(
                key => " " + this.state.user.roles[key]
            );
        }

        return (
            <tr>
                <th scope="row">
                    <img src={"http://cravatar.eu/helmavatar/" + this.state.user.username + "/32.png"}
                     alt={this.state.user.username} className="img-fluid"/>
                </th>
                <th>
                    {this.state.user.username}
                </th>
                <th>
                    {this.state.user.email}
                </th>
                <th>
                    <button className="btn btn-dark" onClick={this.toggle}><FontAwesomeIcon icon={faSearch}/></button>
                </th>

                <Modal isOpen={this.state.showModalUserInfo} toggle={this.toggle} className={this.props.className}>
                    <ModalHeader toggle={this.toggle}>
                        <div className="align-content-center align-items-center align-self-center text-center">
                            <img src={"http://cravatar.eu/helmavatar/" + this.state.user.username + "/50.png"}
                             alt={this.state.user.username} className="img-fluid rounded align-self-center"/>
                        {" " + this.state.user.username + ' { ' + roles + ' }'}
                        </div>
                    </ModalHeader>
                    <ModalBody>
                        <p>
                            <b>Email adresse:</b> {this.state.user.email}
                        </p>
                        <p>
                            <b>Account creation date:</b> {this.state.user.accountCreationDate}
                        </p>
                        <p>
                            <b>2FA status:</b>
                            {
                                this.state.user.usingTwoFA
                                ?
                                    <span className="badge badge-success">enabled</span>
                                :
                                    <span className="badge badge-danger">disabled</span>
                            }
                        </p>
                    </ModalBody>
                </Modal>
            </tr>
        );
    }
}

export default withRouter(UserTableLine);

And finally, UserPagination.js

import React, {Component} from 'react';
import {withRouter} from 'react-router-dom';

class UserPagination extends Component {

    state = {
        pagination: {}
    };

    constructor(props) {
        super(props);

        this.onPageChange = this.onPageChange.bind(this);
        this.goToFirstPage = this.goToFirstPage.bind(this);
        this.goToLastPage = this.goToLastPage.bind(this);
        this.goToPreviousPage = this.goToPreviousPage.bind(this);
        this.goToNextPage = this.goToNextPage.bind(this);
        this.setStatePromise = this.setStatePromise.bind(this);
    }

    componentDidMount() {
        const pagination = {
            firstPage: this.props.page.firstPage,
            lastPage: this.props.page.lastPage,
            currentPageable: {
                sort: {
                    sorted: false,
                    unsorted: true
                },
                offset: this.props.page.offset,
                pageSize: this.props.page.pageSize,
                pageNumber: this.props.page.number
            },
            previousPageable: this.props.page.previousPageable,
            nextPageable: this.props.page.nextPageable,
            totalPages: this.props.page.totalPages,
            totalElement: this.props.page.totalElement
        };
        this.setState({pagination});
    }

    setStatePromise(newState) {
        return new Promise((resolve) => {
            this.setState(newState, () => {
                resolve();
            });
        });
    }

    onPageChange = (pageable) => {
        this.props.handleChangePage(pageable);
    };

    goToFirstPage() {
        const firstPage = {
            sort: {
                sorted: false,
                unsorted: true
            },
            offset: 0,
            pageSize: 10,
            pageNumber: 0
        };
        this.onPageChange(firstPage);
    }

    goToLastPage() {
        const lastPage = {
            sort: {
                sorted: false,
                unsorted: true
            },
            pageSize: 10,
            pageNumber: this.state.pagination.totalPages - 1
        };
        this.onPageChange(lastPage);
    }

    goToPreviousPage() {
        const previousPage = this.state.pagination.previousPageable;
        if (previousPage !== "INSTANCE") {
            this.onPageChange(previousPage);
        }
    }

    goToNextPage() {
        const nextPage = this.state.pagination.nextPageable;
        if (nextPage !== "INSTANCE") {
            this.onPageChange(nextPage);
        }
    }

    getPagesNumberButtons(){
        let pages = [];
        if (this.state.pagination) {
            pages.push(
                <li key={1} className="page-item active">
                    <p className="page-link">{this.state.pagination.currentPageable.pageNumber}</p>
                </li>
            );
        }
        return pages;
    }

    render() {

        return (
            <div>
                <ul className="pagination">
                    <li className="page-item" onClick={this.goToFirstPage}>
                        <p className="page-link">&laquo;</p>
                    </li>
                    <li className="page-item" onClick={this.goToPreviousPage}>
                        <p className="page-link">Prev</p>
                    </li>
                    {this.getPagesNumberButtons}
                    <li id="nextPage" className="page-item" onClick={this.goToNextPage}>
                        <p className="page-link">Next</p>
                    </li>
                    <li id="lastPage" className="page-item" onClick={this.goToLastPage}>
                        <p className="page-link">&raquo;</p>
                    </li>
                </ul>
            </div>
        );
    }

}

export default withRouter(UserPagination);

But If you prefer to have the complete code: Github

If you have an idea to solve my problem or if you know a library that handles with server-side paging, I'm interested :)

Thank you in advance for taking the time to read all this and an even bigger thank you for your help.

Alexis

Edit 1 : Here is the response I get from the controller:

{
    "content": [
        {
            "username": "test1",
            "email": "[email protected]",
            "accountCreationDate": "2018-08-22",
            "roles": [
                "USER"
            ],
            "usingTwoFA": false
        },
        {
            "username": "test2",
            "email": "[email protected]",
            "accountCreationDate": "2018-08-22",
            "roles": [
                "USER"
            ],
            "usingTwoFA": false
        },
        {
            "username": "test3",
            "email": "[email protected]",
            "accountCreationDate": "2018-08-22",
            "roles": [
                "USER"
            ],
            "usingTwoFA": false
        },
        {
            "username": "test4",
            "email": "[email protected]",
            "accountCreationDate": "2018-08-22",
            "roles": [
                "USER"
            ],
            "usingTwoFA": false
        },
        {
            "username": "test5",
            "email": "[email protected]",
            "accountCreationDate": "2018-08-22",
            "roles": [
                "USER"
            ],
            "usingTwoFA": false
        },
        {
            "username": "test6",
            "email": "[email protected]",
            "accountCreationDate": "2018-08-22",
            "roles": [
                "USER"
            ],
            "usingTwoFA": false
        },
        {
            "username": "test7",
            "email": "[email protected]",
            "accountCreationDate": "2018-08-22",
            "roles": [
                "USER"
            ],
            "usingTwoFA": false
        },
        {
            "username": "test8",
            "email": "[email protected]",
            "accountCreationDate": "2018-08-22",
            "roles": [
                "USER"
            ],
            "usingTwoFA": false
        },
        {
            "username": "test9",
            "email": "[email protected]",
            "accountCreationDate": "2018-08-22",
            "roles": [
                "USER"
            ],
            "usingTwoFA": false
        },
        {
            "username": "test10",
            "email": "[email protected]",
            "accountCreationDate": "2018-08-22",
            "roles": [
                "USER"
            ],
            "usingTwoFA": false
        }
    ],
    "offset": 0,
    "pageNumber": 0,
    "pageSize": 10,
    "lastPage": false,
    "totalElement": 24,
    "totalPages": 3,
    "size": 10,
    "number": 0,
    "numberOfElements": 10,
    "firstPage": true,
    "previousPageable": "INSTANCE",
    "nextPageable": {
        "sort": {
            "sorted": false,
            "unsorted": true
        },
        "offset": 10,
        "pageSize": 10,
        "pageNumber": 1,
        "paged": true,
        "unpaged": false
    }
}
7
  • So you want to create a number pagination to scroll across and navigate? Without the 'prev' / 'next'? Commented Aug 24, 2018 at 8:40
  • That's exactly it but keeping the 'prev' / 'next', to get something like this imgur.com/05ilghx Commented Aug 24, 2018 at 9:13
  • You say "But they all need to recover all the data in a single list", but what are you trying to achieve? Commented Aug 24, 2018 at 10:01
  • 1
    Hi @SGhaleb ! Thank you for these two links. I think the first is more what I'm looking for. I will try to implement this solution tonight. I will come back to this question when I tested this solution to provide feedback on this topic. Commented Aug 24, 2018 at 11:27
  • 1
    Hi @SGhaleb after several attempts, the examples you provided me did not correspond finally. So I developed a generic component for this type of pagination and it works ! Thank you again for your help because the links you gave me, helped me to develop this component. This is not an optimal solution (1k lines of code) but that will be enough for a first version, I will optimize it later. Commented Aug 25, 2018 at 15:03

1 Answer 1

1

After interacting with @SGhaleb, I finally developed my own component for this kind of pagination and it works. This is not an optimal solution (+ 1k lines of code) but it will be enough for a first version, I will optimize it later.

Here is the code of the said component:

import React, {Component} from 'react';

class UserPagination extends Component {

    constructor(props) {
        super(props);

        this.state = {
            page: props.page,
            pageSize: props.pageSize,
            currentPage: props.currentPage,
            totalNumberOfElements: props.totalNumberOfElements
        };

        this.onPageChange = this.onPageChange.bind(this);
        this.goToFirstPage = this.goToFirstPage.bind(this);
        this.goToLastPage = this.goToLastPage.bind(this);
        this.goToPreviousPage = this.goToPreviousPage.bind(this);
        this.goToNextPage = this.goToNextPage.bind(this);
        this.buildPagination = this.buildPagination.bind(this);
    }

    onPageChange = (pageNumber) => {
        this.props.handleChangePage(pageNumber);
    };

    static getDerivedStateFromProps(props, state) {
        state = props;
        return state;
    }

    goToFirstPage() {
        this.onPageChange(0);
    }

    goToLastPage() {
        this.onPageChange(this.state.page.totalNumberOfPages - 1);
    }

    goToPreviousPage() {
        const previousPage = this.state.page.previousPageable;
        if (previousPage !== "INSTANCE") {
            this.onPageChange(previousPage.pageNumber);
        }
    }

    goToNextPage() {
        const {currentPage, page} = this.state;
        const nextPage = page.nextPageable;
        if (nextPage !== "INSTANCE") {
            this.onPageChange(currentPage + 1);
        }
    }

    buildPagination(page, currentPage) {
        //PAGINATION LOGIC
        //SEE LINK TO PASTEBIN.COM
    }

    render() {

        const {page, currentPage} = this.state;

        let pagination = this.buildPagination(page, currentPage);


        return (
            <ul className="pagination">
                {pagination}
            </ul>
        );
    }
}

export default UserPagination;

https://pastebin.com/x4Fx9pLm

Now the pagination looks like what I wanted :

pagination component

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.