From c3a1e3d7addcedfdfbb69f5f418909aee60ffd41 Mon Sep 17 00:00:00 2001
From: Hans-Peter Herzog <hherzog@sipwise.com>
Date: Wed, 25 Apr 2018 15:18:23 +0200
Subject: [PATCH] TT#36018 PPBXConfig: As a Customer, I want to navigate
 through the list of pbx groups by using a pagination mechanism

- PBXConfig: As a Customer, I want to navigate through the list of devices by using a pagination mechanism
- PBXConfig: As a Customer, I want to navigate through the list of seats by using a pagination mechanism

Change-Id: I15393d01a3f50eeafcc300ef27ad2769c6f1dc1a
---
 karma.js                                      |   3 +
 src/api/call-forward.js                       |  17 +-
 src/api/common.js                             |  53 +++++
 src/api/pbx-config.js                         | 214 ++++++++++++++----
 src/api/pbx-devices.js                        |  69 ------
 src/api/subscriber.js                         |  16 ++
 src/api/user.js                               |   7 +-
 .../pages/PbxConfiguration/CscPbxDevices.vue  |  23 +-
 .../pages/PbxConfiguration/CscPbxGroups.vue   |  23 +-
 .../pages/PbxConfiguration/CscPbxSeats.vue    |  21 +-
 src/store/pbx-config/actions.js               |  66 ++++--
 src/store/pbx-config/getters.js               |   6 +
 src/store/pbx-config/mutations.js             |  31 ++-
 src/store/pbx-config/state.js                 |   2 +
 t/Dockerfile                                  |   2 +-
 t/store/pbx-config.js                         |  52 +++--
 16 files changed, 403 insertions(+), 202 deletions(-)
 create mode 100644 src/api/common.js
 delete mode 100644 src/api/pbx-devices.js

diff --git a/karma.js b/karma.js
index b3f472c2..37cb8132 100644
--- a/karma.js
+++ b/karma.js
@@ -1,7 +1,10 @@
 'use strict';
 
+var _ = require('lodash');
 var webpackCsc = require('./build/webpack.base.conf');
 
+webpackCsc.module.rules.shift();
+
 module.exports = function(config) {
     config.set({
         basePath: '',
diff --git a/src/api/call-forward.js b/src/api/call-forward.js
index 0529f6b1..d8114129 100644
--- a/src/api/call-forward.js
+++ b/src/api/call-forward.js
@@ -3,9 +3,8 @@ import _ from 'lodash';
 import Vue from 'vue';
 import { i18n } from '../i18n';
 import { getJsonBody } from './utils';
-import { normalizeDestination } from '../filters/number-format'
-
-let rowCountAssumption = 1000;
+import { normalizeDestination } from '../filters/number-format';
+import { LIST_ALL_ROWS } from './common';
 
 export function getMappings(id) {
     return new Promise((resolve, reject) => {
@@ -24,10 +23,10 @@ export function getSourcesets(id) {
     return new Promise((resolve, reject) => {
         Promise.resolve().then(() => {
             return Vue.http.get('/api/cfsourcesets/',
-                { params: { subscriber_id: id, page: 1, rows: rowCountAssumption } })
+                { params: { subscriber_id: id, page: 1, rows: LIST_ALL_ROWS } })
         }).then(result => {
             let totalCount = getJsonBody(result.body).total_count;
-            if (totalCount > rowCountAssumption) {
+            if (totalCount > LIST_ALL_ROWS) {
                 return Vue.http.get('/api/cfsourcesets/',
                     { params: { subscriber_id: id, page: 1,
                         rows: totalCount } })
@@ -47,10 +46,10 @@ export function getTimesets(id) {
     return new Promise((resolve, reject) => {
         Promise.resolve().then(() => {
             return Vue.http.get('/api/cftimesets/',
-                { params: { subscriber_id: id, page: 1, rows: rowCountAssumption } })
+                { params: { subscriber_id: id, page: 1, rows: LIST_ALL_ROWS } })
         }).then(result => {
             let totalCount = getJsonBody(result.body).total_count;
-            if (totalCount > rowCountAssumption) {
+            if (totalCount > LIST_ALL_ROWS) {
                 return Vue.http.get('/api/cftimesets/',
                     { params: { subscriber_id: id, page: 1,
                         rows: totalCount } })
@@ -72,10 +71,10 @@ export function getDestinationsets(id) {
     return new Promise((resolve, reject) => {
         Promise.resolve().then(() => {
             return Vue.http.get('/api/cfdestinationsets/',
-                { params: { subscriber_id: id, page: 1, rows: rowCountAssumption } })
+                { params: { subscriber_id: id, page: 1, rows: LIST_ALL_ROWS } })
         }).then(result => {
             let totalCount = getJsonBody(result.body).total_count;
-            if (totalCount > rowCountAssumption) {
+            if (totalCount > LIST_ALL_ROWS) {
                 return Vue.http.get('/api/cfdestinationsets/',
                     { params: { subscriber_id: id, page: 1,
                         rows: totalCount } })
diff --git a/src/api/common.js b/src/api/common.js
new file mode 100644
index 00000000..67cd9dac
--- /dev/null
+++ b/src/api/common.js
@@ -0,0 +1,53 @@
+
+import _ from 'lodash';
+import Vue from 'vue';
+import { getJsonBody } from './utils';
+
+export const LIST_DEFAULT_PAGE = 1;
+export const LIST_DEFAULT_ROWS = 25;
+export const LIST_ALL_ROWS = 1000;
+
+export function getList(options) {
+    return new Promise((resolve, reject)=>{
+        options = options || {};
+        options = _.merge({
+            all: false,
+            params: {
+                page: LIST_DEFAULT_PAGE,
+                rows: LIST_DEFAULT_ROWS
+            }
+        }, options);
+        Promise.resolve().then(()=>{
+            if(options.all === true) {
+                options.params.rows = LIST_ALL_ROWS;
+            }
+            return Vue.http.get(options.path, {
+                params: options.params
+            });
+        }).then((res)=>{
+            let body = getJsonBody(res.body);
+            if(options.all === true && body.total_count > LIST_ALL_ROWS) {
+                return Vue.http.get(options.path, {
+                    params: _.merge(options.params, {
+                        rows: body.total_count
+                    })
+                });
+            }
+            else {
+                return Promise.resolve(res);
+            }
+        }).then((res)=>{
+            let body = getJsonBody(res.body);
+            let lastPage = Math.ceil( body.total_count / options.params.rows );
+            if(options.all === true) {
+                lastPage = 1;
+            }
+            resolve({
+                items: _.get(body, options.root, []),
+                lastPage: lastPage
+            });
+        }).catch((err)=>{
+            reject(err);
+        });
+    });
+}
diff --git a/src/api/pbx-config.js b/src/api/pbx-config.js
index bb521ffb..e0bb079f 100644
--- a/src/api/pbx-config.js
+++ b/src/api/pbx-config.js
@@ -1,59 +1,139 @@
 
 import _ from 'lodash';
-import Vue from 'vue';
-import { getJsonBody } from './utils';
 import { getNumbers, assignNumbers } from './user';
 import { createSubscriber, deleteSubscriber, setDisplayName,
     setPbxExtension, setPbxHuntPolicy, setPbxHuntTimeout,
-    setPbxGroupMemberIds, setPbxGroupIds } from './subscriber';
+    setPbxGroupMemberIds, setPbxGroupIds, getSubscribers } from './subscriber';
 import uuid from 'uuid';
+import { getList } from './common'
 
 var createId = uuid.v4;
-var assumedRows = 1000;
 
-export function getAllPbxSubscribers() {
+export const PBX_CONFIG_ORDER_BY = 'created_timestamp';
+export const PBX_CONFIG_ORDER_DIRECTION = 'desc';
+
+export function getGroups(options) {
     return new Promise((resolve, reject)=>{
-        var params = {};
-        Promise.resolve().then(()=>{
-            return Vue.http.get('/api/subscribers', {
-                params: _.assign(params, {
-                    page: 1,
-                    rows: assumedRows
-                })
-            });
-        }).then((res)=>{
-            let body = getJsonBody(res.body);
-            if(body.total_count > assumedRows) {
-                return Vue.http.get('/api/subscribers', {
-                    params: _.assign(params, {
-                        page: 1,
-                        rows: body.total_count,
-                    })
-                });
+        options = options || {};
+        options = _.merge(options, {
+            params: {
+                is_pbx_group: 1
+            }
+        });
+        getSubscribers(options).then((res)=>{
+            resolve(res);
+        }).catch((err)=>{
+            reject(err);
+        });
+    });
+}
+
+export function getSeats(options) {
+    return new Promise((resolve, reject)=>{
+        options = options || {};
+        options = _.merge(options, {
+            params: {
+                is_pbx_group: 0,
+                is_pbx_pilot: 0
+            }
+        });
+        getSubscribers(options).then((res)=>{
+            resolve(res);
+        }).catch((err)=>{
+            reject(err);
+        });
+    });
+}
+
+export function getPilot(options) {
+    return new Promise((resolve, reject)=>{
+        options = options || {};
+        options = _.merge(options, {
+            params: {
+                is_pbx_group: 0,
+                is_pbx_pilot: 1
+            }
+        });
+        getSubscribers(options).then((subscribers)=>{
+            if(subscribers.items.length === 1) {
+                resolve(subscribers.items[0]);
             }
             else {
-                return Promise.resolve(body);
+                resolve(null);
             }
-        }).then(($subscribers)=>{
-            let subscribers = _.get($subscribers, '_embedded.ngcp:subscribers', []);
-            let pilot = null;
-            let seats = [];
-            let groups = [];
-            subscribers.forEach((subscriber)=>{
-                if(_.has(subscriber, 'is_pbx_pilot') && subscriber.is_pbx_pilot === true) {
-                    pilot = subscriber;
-                }
-                else if(_.has(subscriber, 'is_pbx_group') && subscriber.is_pbx_group === true) {
-                    groups.push(subscriber);
-                }
-                else if (_.has(subscriber, 'pbx_extension') && subscriber.pbx_extension !== null) {
-                    seats.push(subscriber);
+        }).catch((err)=>{
+            reject(err);
+        });
+    });
+}
+
+export function getDevices(options) {
+    return new Promise((resolve, reject)=>{
+        options = options || {};
+        options = _.merge(options, {
+            path: '/api/pbxdevices/',
+            root: '_embedded.ngcp:pbxdevices'
+        });
+        getList(options).then((list)=>{
+            resolve(list);
+        }).catch((err)=>{
+            reject(err);
+        });
+    });
+}
+
+export function getProfiles(options) {
+    return new Promise((resolve, reject)=>{
+        options = options || {};
+        options = _.merge(options, {
+            path: '/api/pbxdeviceprofiles/',
+            root: '_embedded.ngcp:pbxdeviceprofiles'
+        });
+        getList(options).then((list)=>{
+            resolve(list);
+        }).catch((err)=>{
+            reject(err);
+        });
+    });
+}
+
+export function getModels(options) {
+    return new Promise((resolve, reject)=>{
+        options = options || {};
+        options = _.merge(options, {
+            path: '/api/pbxdevicemodels/',
+            root: '_embedded.ngcp:pbxdevicemodels'
+        });
+        getList(options).then((list)=>{
+            resolve(list);
+        }).catch((err)=>{
+            reject(err);
+        });
+    });
+}
+
+export function getGroupList(page) {
+    return new Promise((resolve, reject)=>{
+        Promise.all([
+            getGroups({
+                params: {
+                    page: page,
+                    order_by: PBX_CONFIG_ORDER_BY,
+                    order_by_direction: PBX_CONFIG_ORDER_DIRECTION
                 }
-            });
+            }),
+            getSeats({
+                all: true
+            }),
+            getPilot(),
+            getNumbers()
+        ]).then((result)=>{
             resolve({
-                pilot: pilot,
-                groups: groups,
-                seats: seats
+                groups: result[0],
+                seats: result[1],
+                pilot: result[2],
+                numbers: result[3],
+                lastPage: result[0].lastPage
             });
         }).catch((err)=>{
             reject(err);
@@ -61,17 +141,57 @@ export function getAllPbxSubscribers() {
     });
 }
 
-export function getPbxConfiguration() {
+export function getSeatList(page) {
     return new Promise((resolve, reject)=>{
         Promise.all([
-            getAllPbxSubscribers(),
+            getSeats({
+                params: {
+                    page: page,
+                    order_by: PBX_CONFIG_ORDER_BY,
+                    order_by_direction: PBX_CONFIG_ORDER_DIRECTION
+                }
+            }),
+            getGroups({
+                all: true
+            }),
+            getPilot(),
             getNumbers()
         ]).then((result)=>{
             resolve({
-                pilot: result[0].pilot,
-                seats: result[0].seats,
-                groups: result[0].groups,
-                numbers: result[1]
+                seats: result[0],
+                groups: result[1],
+                pilot: result[2],
+                numbers: result[3],
+                lastPage: result[0].lastPage
+            });
+        }).catch((err)=>{
+            reject(err);
+        });
+    });
+}
+
+export function getDeviceList(page) {
+    return new Promise((resolve, reject)=>{
+        Promise.all([
+            getDevices({
+                params: {
+                    page: page,
+                    order_by: PBX_CONFIG_ORDER_BY,
+                    order_by_direction: PBX_CONFIG_ORDER_DIRECTION
+                }
+            }),
+            getProfiles({
+                all: true
+            }),
+            getModels({
+                all: true
+            })
+        ]).then((result)=>{
+            resolve({
+                devices: result[0],
+                profiles: result[1],
+                models: result[2],
+                lastPage: result[0].lastPage
             });
         }).catch((err)=>{
             reject(err);
diff --git a/src/api/pbx-devices.js b/src/api/pbx-devices.js
deleted file mode 100644
index c47f4c80..00000000
--- a/src/api/pbx-devices.js
+++ /dev/null
@@ -1,69 +0,0 @@
-'use strict';
-
-import _ from 'lodash';
-import Vue from 'vue';
-import { getJsonBody } from './utils'
-
-export function getAllDevices(options) {
-    return new Promise((resolve, reject)=>{
-        let rows = _.get(options, 'rows', 25);
-        let page = _.get(options, 'page', 1);
-        Vue.http.get('/api/pbxdevices/', null, {
-            params: {
-                rows: rows,
-                page: page
-            }
-        }).then((result)=>{
-            let body = getJsonBody(result.body);
-            let totalCount = body.totalCount;
-            let lastPage = Math.ceil(totalCount / rows);
-            let items = _.get(body, '_embedded.ngcp:pbxdevices');
-            resolve({
-                lastPage: lastPage,
-                items: items
-            });
-        }).catch((err)=>{
-            reject(err);
-        });
-    });
-}
-
-export function getAllProfiles() {
-    return new Promise((resolve, reject)=>{
-        Vue.http.get('/api/pbxdeviceprofiles/').then((result)=>{
-            let body = getJsonBody(result.body);
-            resolve(_.get(body, '_embedded.ngcp:pbxdeviceprofiles'));
-        }).catch((err)=>{
-            reject(err);
-        });
-    });
-}
-
-export function getAllModels() {
-    return new Promise((resolve, reject)=>{
-        Vue.http.get('/api/pbxdevicemodels/').then((result)=>{
-            let body = getJsonBody(result.body);
-            resolve(_.get(body, '_embedded.ngcp:pbxdevicemodels'));
-        }).catch((err)=>{
-            reject(err);
-        });
-    });
-}
-
-export function getDeviceList() {
-    return new Promise((resolve, reject)=>{
-        Promise.all([
-            getAllDevices(),
-            getAllProfiles(),
-            getAllModels()
-        ]).then((results)=>{
-            resolve({
-                devices: results[0],
-                profiles: results[1],
-                models: results[2]
-            });
-        }).catch((err)=>{
-            reject(err);
-        });
-    });
-}
diff --git a/src/api/subscriber.js b/src/api/subscriber.js
index 185c1dbc..3025af57 100644
--- a/src/api/subscriber.js
+++ b/src/api/subscriber.js
@@ -2,6 +2,7 @@
 import _ from 'lodash';
 import Vue from 'vue';
 import { getJsonBody } from './utils'
+import { getList } from './common'
 
 export function getPreferences(id) {
     return new Promise((resolve, reject)=>{
@@ -263,3 +264,18 @@ export function setPbxGroupMemberIds(id, ids) {
 export function setPbxGroupIds(id, ids) {
     return setField(id, 'pbx_group_ids', ids);
 }
+
+export function getSubscribers(options) {
+    return new Promise((resolve, reject)=>{
+        options = options || {};
+        options = _.merge(options, {
+            path: '/api/subscribers/',
+            root: '_embedded.ngcp:subscribers'
+        });
+        getList(options).then((list)=>{
+            resolve(list);
+        }).catch((err)=>{
+            reject(err);
+        });
+    });
+}
diff --git a/src/api/user.js b/src/api/user.js
index a6292868..a4e0ac01 100644
--- a/src/api/user.js
+++ b/src/api/user.js
@@ -1,8 +1,7 @@
 
 import _ from 'lodash';
 import Vue from 'vue';
-
-var assumedNumbers = 1000;
+import { LIST_ALL_ROWS } from './common'
 
 export function login(username, password) {
     return new Promise((resolve, reject)=>{
@@ -120,12 +119,12 @@ export function getNumbers() {
             return Vue.http.get(path, {
                 params: _.assign(params, {
                     page: 1,
-                    rows: assumedNumbers,
+                    rows: LIST_ALL_ROWS,
                 })
             });
         }).then((res)=>{
             let body = JSON.parse(res.body);
-            if(body.total_count > assumedNumbers) {
+            if(body.total_count > LIST_ALL_ROWS) {
                 return Vue.http.get(path, {
                     params: _.assign(params, {
                         page: 1,
diff --git a/src/components/pages/PbxConfiguration/CscPbxDevices.vue b/src/components/pages/PbxConfiguration/CscPbxDevices.vue
index aad41f3f..cbbd00a6 100644
--- a/src/components/pages/PbxConfiguration/CscPbxDevices.vue
+++ b/src/components/pages/PbxConfiguration/CscPbxDevices.vue
@@ -3,6 +3,9 @@
         <div v-if="isListLoadingVisible" class="row justify-center">
             <q-spinner-dots color="primary" :size="40" />
         </div>
+        <div v-if="devices.length > 0 && !isListRequesting && listLastPage > 1" class="row justify-center">
+            <q-pagination :value="listCurrentPage" :max="listLastPage" @change="changePage" />
+        </div>
         <csc-pbx-device v-for="device in devices" :key="device.id"
                         :device="device" :modelOptions="modelOptions" />
         <div v-if="devices.length === 0 && !isListRequesting" class="row justify-center csc-no-entities">
@@ -15,27 +18,39 @@
     import { mapGetters } from 'vuex'
     import CscPage  from '../../CscPage'
     import CscPbxDevice from './CscPbxDevice'
-    import { QSpinnerDots } from 'quasar-framework'
+    import { QSpinnerDots, QPagination } from 'quasar-framework'
     export default {
         data () {
             return {
             }
         },
         mounted() {
-            this.$store.dispatch('pbxConfig/listDevices');
+            this.$store.dispatch('pbxConfig/listDevices', {
+                page: 1
+            });
         },
         components: {
             CscPage,
             CscPbxDevice,
-            QSpinnerDots
+            QSpinnerDots,
+            QPagination
         },
         computed: {
             ...mapGetters('pbxConfig', [
                 'devices',
                 'modelOptions',
                 'isListRequesting',
-                'isListLoadingVisible'
+                'isListLoadingVisible',
+                'listCurrentPage',
+                'listLastPage'
             ])
+        },
+        methods: {
+            changePage(page) {
+                this.$store.dispatch('pbxConfig/listDevices', {
+                    page: page
+                });
+            }
         }
     }
 </script>
diff --git a/src/components/pages/PbxConfiguration/CscPbxGroups.vue b/src/components/pages/PbxConfiguration/CscPbxGroups.vue
index 5a2c770c..139874e3 100644
--- a/src/components/pages/PbxConfiguration/CscPbxGroups.vue
+++ b/src/components/pages/PbxConfiguration/CscPbxGroups.vue
@@ -9,6 +9,9 @@
         <div v-if="isListLoadingVisible" class="row justify-center">
             <q-spinner-dots color="primary" :size="40" />
         </div>
+        <div v-if="groups.length > 0 && !isListRequesting && listLastPage > 1" class="row justify-center">
+            <q-pagination :value="listCurrentPage" :max="listLastPage" @change="changePage" />
+        </div>
         <csc-pbx-group v-for="group in groups" :key="group.id" :group="group" :alias-number-options="aliasNumberOptions"
                        :seat-options="seatOptions" :hunt-policy-options="huntPolicyOptions" @remove="removeGroup"
                        :loading="isItemLoading(group.id)" @save-name="setGroupName" @save-extension="setGroupExtension"
@@ -28,7 +31,7 @@
     import { QChip, QCard, QCardSeparator, QCardTitle, QCardMain,
         QCardActions, QIcon, QPopover, QList, QItem, QItemMain,
         QField, QInput, QBtn, QSelect, QInnerLoading, QSpinnerDots,
-        QSpinnerMat, Dialog
+        QSpinnerMat, Dialog, QPagination
     } from 'quasar-framework'
     import aliasNumberOptions from '../../../mixins/alias-number-options'
     import itemError from '../../../mixins/item-error'
@@ -40,14 +43,17 @@
             QChip, QCard, QCardSeparator, QCardTitle, QCardMain,
             QCardActions, QIcon, QPopover, QList, QItem, QItemMain,
             QField, QInput, QBtn, QSelect, QInnerLoading, QSpinnerDots,
-            QSpinnerMat, Dialog
+            QSpinnerMat, Dialog, QPagination
         },
         mounted() {
-            this.$store.dispatch('pbxConfig/listGroups');
+            this.$store.dispatch('pbxConfig/listGroups', {
+                page: 1
+            });
         },
         data () {
             return {
-                addFormEnabled: false
+                addFormEnabled: false,
+                page: 1
             }
         },
         computed: {
@@ -95,7 +101,9 @@
                 'listState',
                 'listError',
                 'isListRequesting',
-                'isListLoadingVisible'
+                'isListLoadingVisible',
+                'listCurrentPage',
+                'listLastPage'
             ])
         },
         watch: {
@@ -159,6 +167,11 @@
             },
             updateSeats(data) {
                 this.$store.dispatch('pbxConfig/updateSeats', data);
+            },
+            changePage(page) {
+                this.$store.dispatch('pbxConfig/listGroups', {
+                    page: page
+                });
             }
         }
     }
diff --git a/src/components/pages/PbxConfiguration/CscPbxSeats.vue b/src/components/pages/PbxConfiguration/CscPbxSeats.vue
index 729979bd..3ae4305a 100644
--- a/src/components/pages/PbxConfiguration/CscPbxSeats.vue
+++ b/src/components/pages/PbxConfiguration/CscPbxSeats.vue
@@ -9,6 +9,9 @@
         <div v-if="isListLoadingVisible" class="row justify-center">
             <q-spinner-dots color="primary" :size="40" />
         </div>
+        <div v-if="seats.length > 0 && !isListRequesting && listLastPage > 1" class="row justify-center">
+            <q-pagination :value="listCurrentPage" :max="listLastPage" @change="changePage" />
+        </div>
         <csc-pbx-seat v-for="seat in seats" :key="seat.id" :seat="seat" :alias-number-options="aliasNumberOptions"
                       :group-options="groupOptions" @remove="removeSeat" :loading="isItemLoading(seat.id)"
                       @save-name="setSeatName" @save-extension="setSeatExtension"
@@ -26,16 +29,17 @@
     import { QChip, QCard, QCardSeparator, QCardTitle, QCardMain,
         QCardActions, QIcon, QPopover, QList, QItem, QItemMain,
         QField, QInput, QBtn, QSelect, QInnerLoading, QSpinnerDots,
-        QSpinnerMat, Dialog } from 'quasar-framework'
+        QSpinnerMat, Dialog, QPagination } from 'quasar-framework'
     import aliasNumberOptions from '../../../mixins/alias-number-options'
     import itemError from '../../../mixins/item-error'
-//    import { showGlobalError } from '../../../helpers/ui'
     import { mapGetters } from 'vuex'
 
     export default {
         mixins: [aliasNumberOptions, itemError],
         mounted() {
-            this.$store.dispatch('pbxConfig/listSeats');
+            this.$store.dispatch('pbxConfig/listSeats', {
+                page: 1
+            });
         },
         data () {
             return {
@@ -47,7 +51,7 @@
             QChip, QCard, QCardSeparator, QCardTitle, QCardMain,
             QCardActions, QIcon, QPopover, QList, QItem, QItemMain,
             QField, QInput, QBtn, QSelect, QInnerLoading, QSpinnerDots,
-            QSpinnerMat, Dialog
+            QSpinnerMat, Dialog, QPagination
         },
         computed: {
             ...mapGetters('pbxConfig', [
@@ -62,7 +66,9 @@
                 'listState',
                 'listError',
                 'isListRequesting',
-                'isListLoadingVisible'
+                'isListLoadingVisible',
+                'listCurrentPage',
+                'listLastPage'
             ]),
             groupOptions() {
                 let groups = [];
@@ -131,6 +137,11 @@
             },
             updateGroups(data) {
                 this.$store.dispatch('pbxConfig/updateGroups', data);
+            },
+            changePage(page) {
+                this.$store.dispatch('pbxConfig/listSeats', {
+                    page: page
+                });
             }
         }
     }
diff --git a/src/store/pbx-config/actions.js b/src/store/pbx-config/actions.js
index 82e080cd..5aa645ae 100644
--- a/src/store/pbx-config/actions.js
+++ b/src/store/pbx-config/actions.js
@@ -1,23 +1,24 @@
 'use strict';
 
+import _ from 'lodash';
 import { assignNumbers } from '../../api/user';
-import { getPbxConfiguration, addGroup,
-    removeGroup, addSeat, removeSeat, setGroupName,
+import { addGroup, removeGroup, addSeat, removeSeat, setGroupName,
     setGroupExtension, setGroupHuntPolicy, setGroupHuntTimeout,
-    updateGroupSeats, setSeatName, setSeatExtension, updateSeatGroups } from '../../api/pbx-config'
-import { getDeviceList } from '../../api/pbx-devices'
+    updateGroupSeats, setSeatName, setSeatExtension,
+    updateSeatGroups, getGroupList, getSeatList, getDeviceList } from '../../api/pbx-config'
 
 export default {
-    listGroups(context, silent) {
-        return new Promise((resolve, reject)=>{
-            context.commit('listRequesting', silent);
-            getPbxConfiguration().then((config)=>{
-                context.commit('listSucceeded', config);
-                resolve();
-            }).catch((err)=>{
-                context.commit('listFailed', err.message);
-                reject(err);
-            });
+    listGroups(context, options) {
+        let silent = _.get(options, 'silent', false);
+        let page = _.get(options, 'page', 1);
+        context.commit('listRequesting', {
+            silent: silent,
+            page: page
+        });
+        getGroupList(page).then((groups)=>{
+            context.commit('listSucceeded', groups);
+        }).catch((err)=>{
+            context.commit('listFailed', err.message);
         });
     },
     addGroup(context, group) {
@@ -106,15 +107,25 @@ export default {
             context.commit('removeItemFailed', err.message);
         });
     },
-    listSeats(context, silent) {
-        return context.dispatch('listGroups', silent);
+    listSeats(context, options) {
+        let silent = _.get(options, 'silent', false);
+        let page = _.get(options, 'page', 1);
+        context.commit('listRequesting', {
+            silent: silent,
+            page: page
+        });
+        getSeatList(page).then((seats)=>{
+            context.commit('listSucceeded', seats);
+        }).catch((err)=>{
+            context.commit('listFailed', err.message);
+        });
     },
     addSeat(context, seat) {
         seat.customerId = context.state.pilot.customer_id;
         seat.domainId = context.state.pilot.domain_id;
         context.commit('addItemRequesting', seat);
         addSeat(seat).then(()=>{
-            return context.dispatch('listGroups', true);
+            return context.dispatch('listSeats', true);
         }).then(()=>{
             context.commit('addItemSucceeded');
         }).catch((err)=>{
@@ -124,7 +135,7 @@ export default {
     setSeatName(context, seat) {
         context.commit('updateItemRequesting', seat);
         setSeatName(seat.id, seat.name).then(() => {
-            return context.dispatch('listGroups', true);
+            return context.dispatch('listSeats', true);
         }).then(()=>{
             context.commit('updateItemSucceeded');
         }).catch((err) => {
@@ -134,7 +145,7 @@ export default {
     setSeatExtension(context, seat) {
         context.commit('updateItemRequesting', seat);
         setSeatExtension(seat.id, seat.extension).then(()=>{
-            return context.dispatch('listGroups', true);
+            return context.dispatch('listSeats', true);
         }).then(() => {
             context.commit('updateItemSucceeded');
         }).catch((err) => {
@@ -144,7 +155,7 @@ export default {
     updateGroups(context, seat) {
         context.commit('updateItemRequesting', seat);
         updateSeatGroups(seat.id, seat.groups).then(()=>{
-            return context.dispatch('listGroups', true);
+            return context.dispatch('listSeats', true);
         }).then(() => {
             context.commit('updateItemSucceeded');
         }).catch((err) => {
@@ -154,18 +165,23 @@ export default {
     removeSeat(context, seat) {
         context.commit('removeItemRequesting', seat);
         removeSeat(seat.id).then(()=>{
-            return context.dispatch('listGroups', true);
+            return context.dispatch('listSeats', true);
         }).then(()=>{
             context.commit('removeItemSucceeded');
         }).catch((err)=>{
             context.commit('removeItemFailed', err.message);
         });
     },
-    listDevices(context) {
+    listDevices(context, options) {
         return new Promise((resolve, reject)=>{
-            context.commit('deviceListRequesting');
-            getDeviceList().then((result)=>{
-                context.commit('deviceListSucceeded', result);
+            let silent = _.get(options, 'silent', false);
+            let page = _.get(options, 'page', 1);
+            context.commit('deviceListRequesting', {
+                silent: silent,
+                page: page
+            });
+            getDeviceList(page).then((devices)=>{
+                context.commit('deviceListSucceeded', devices);
                 resolve();
             }).catch((err)=>{
                 context.commit('deviceListFailed', err.message);
diff --git a/src/store/pbx-config/getters.js b/src/store/pbx-config/getters.js
index 78ecdd55..a57b665c 100644
--- a/src/store/pbx-config/getters.js
+++ b/src/store/pbx-config/getters.js
@@ -118,5 +118,11 @@ export default {
             });
         });
         return modelOptions;
+    },
+    listCurrentPage(state) {
+        return state.listCurrentPage;
+    },
+    listLastPage(state) {
+        return state.listLastPage;
     }
 }
diff --git a/src/store/pbx-config/mutations.js b/src/store/pbx-config/mutations.js
index 67d28764..b8e07f39 100644
--- a/src/store/pbx-config/mutations.js
+++ b/src/store/pbx-config/mutations.js
@@ -4,25 +4,34 @@ import _ from 'lodash'
 import { RequestState } from '../common'
 
 export default {
-    listRequesting(state, silent) {
+    listRequesting(state, options) {
+        options = options || {};
+        state.listCurrentPage = _.get(options, 'page', 1);
+        state.listLastPage = null;
+        state.listLoadingSilently = _.get(options, 'silent', false);
         state.listState = RequestState.requesting;
         state.listError = null;
-        state.listLoadingSilently = silent;
+        state.groups = {};
+        state.groupsOrdered = [];
+        state.seats = {};
+        state.seatsOrdered = [];
+        state.numbersMap = {};
     },
     listSucceeded(state, all) {
         state.listState = RequestState.succeeded;
         state.listError = null;
+        state.listLastPage = all.lastPage;
         state.pilot = all.pilot;
         state.groups = {};
         state.groupsOrdered = [];
         state.seats = {};
         state.seatsOrdered = [];
         state.numbersMap = {};
-        all.groups.forEach((group)=>{
+        all.groups.items.forEach((group)=>{
             state.groups[group.id] = group;
             state.groupsOrdered.push(group);
         });
-        all.seats.forEach((seat)=>{
+        all.seats.items.forEach((seat)=>{
             seat.pbx_group_ids.forEach((groupId)=>{
                 let group = state.groups[groupId];
                 let seats = _.get(group, 'seats', []);
@@ -53,8 +62,6 @@ export default {
             });
             state.numbers = all.numbers;
         }
-        _.reverse(state.groupsOrdered);
-        _.reverse(state.seatsOrdered);
     },
     listFailed(state, error) {
         state.listState = RequestState.failed;
@@ -107,17 +114,21 @@ export default {
             }
         });
     },
-    deviceListRequesting(state, silent) {
+    deviceListRequesting(state, options) {
+        options = options || {};
+        state.listCurrentPage = _.get(options, 'page', 1);
+        state.listLastPage = null;
+        state.listLoadingSilently = _.get(options, 'silent', false);
         state.listState = RequestState.requesting;
         state.listError = null;
-        state.listLoadingSilently = silent;
     },
     deviceListSucceeded(state, data) {
         state.listState = RequestState.succeeded;
         state.listError = null;
+        state.listLastPage = data.lastPage;
         state.devicesOrdered = data.devices.items;
-        state.profilesOrdered = data.profiles;
-        state.modelsOrdered = data.models;
+        state.profilesOrdered = data.profiles.items;
+        state.modelsOrdered = data.models.items;
         state.devicesOrdered.forEach((device)=>{
             state.profilesOrdered.forEach((profile)=>{
                 if(device.profile_id === profile.id) {
diff --git a/src/store/pbx-config/state.js b/src/store/pbx-config/state.js
index 15f5e9fe..9fcfa25c 100644
--- a/src/store/pbx-config/state.js
+++ b/src/store/pbx-config/state.js
@@ -19,6 +19,8 @@ export default {
     listState: RequestState.initiated,
     listError: null,
     listLoadingSilently: false,
+    listCurrentPage: 1,
+    listLastPage: null,
     addState: RequestState.initiated,
     addError: null,
     addItem: null,
diff --git a/t/Dockerfile b/t/Dockerfile
index cdecd083..717888e3 100644
--- a/t/Dockerfile
+++ b/t/Dockerfile
@@ -5,7 +5,7 @@ FROM docker.mgm.sipwise.com/sipwise-stretch:latest
 # is updated with the current date. It will force refresh of all
 # of the base images and things like `apt-get update` won't be using
 # old cached versions when the Dockerfile is built.
-ENV REFRESHED_AT 2017-12-15
+ENV REFRESHED_AT 2018-04-26
 ENV DEBIAN_FRONTEND noninteractive
 ENV DISPLAY=:0
 
diff --git a/t/store/pbx-config.js b/t/store/pbx-config.js
index 69f179bb..5771fa90 100644
--- a/t/store/pbx-config.js
+++ b/t/store/pbx-config.js
@@ -9,24 +9,30 @@ describe('PBX Configuration Store', () => {
         let state = {};
         let data = {
             pilot: {},
-            seats: [
-                {
-                    id: 2,
-                    pbx_group_ids: []
-                },
-                {
-                    id: 3,
-                    pbx_group_ids: []
-                }
-            ],
-            groups: [
-                {
-                    id: 4
-                },
-                {
-                    id: 5
-                }
-            ],
+            seats: {
+                items: [
+                    {
+                        id: 2,
+                        pbx_group_ids: []
+                    },
+                    {
+                        id: 3,
+                        pbx_group_ids: []
+                    }
+                ],
+                lastPage: 1
+            },
+            groups: {
+                items: [
+                    {
+                        id: 4
+                    },
+                    {
+                        id: 5
+                    }
+                ],
+                lastPage: 1
+            },
             numbers: [
                 {
                     id: 6
@@ -36,11 +42,11 @@ describe('PBX Configuration Store', () => {
                 }
             ]
         };
-        PbxConfig.mutations.listAllSucceeded(state, data);
-        assert.equal(state.seats[2], data.seats[0]);
-        assert.equal(state.seats[3], data.seats[1]);
-        assert.equal(state.groups[4], data.groups[0]);
-        assert.equal(state.groups[5], data.groups[1]);
+        PbxConfig.mutations.listSucceeded(state, data);
+        assert.equal(state.seats[2], data.seats.items[0]);
+        assert.equal(state.seats[3], data.seats.items[1]);
+        assert.equal(state.groups[4], data.groups.items[0]);
+        assert.equal(state.groups[5], data.groups.items[1]);
         assert.deepEqual(state.numbers, data.numbers);
     });
 });