diff --git a/app/addons/replication/__tests__/api.tests.js b/app/addons/replication/__tests__/api.tests.js index ff2db57e6..097592f3e 100644 --- a/app/addons/replication/__tests__/api.tests.js +++ b/app/addons/replication/__tests__/api.tests.js @@ -478,71 +478,40 @@ describe('Replication API', () => { describe("fetchReplicationDocs", () => { const _repDocs = { - "total_rows":2, - "offset":0, - "rows":[ + "docs":[ { - "id":"_design/_replicator", - "key":"_design/_replicator", - "value":{ - "rev":"1-1390740c4877979dbe8998382876556c" + "_id": "c94d4839d1897105cb75e1251e0003ea", + "_rev": "3-4559cb522de85ce03bd0e1991025e89a", + "user_ctx": { + "name": "tester", + "roles": ["_admin", "_reader", "_writer"] }, - "doc":{"_id":"_design/_replicator", - "_rev":"1-1390740c4877979dbe8998382876556c", - "language":"javascript", - "validate_doc_update":"\n function(newDoc, oldDoc, userCtx) {\n function reportError(error_msg) {\n log('Error writing document `' + newDoc._id +\n '\\' to the replicator database: ' + error_msg);\n throw({forbidden: error_msg});\n }\n\n function validateEndpoint(endpoint, fieldName) {\n if ((typeof endpoint !== 'string') &&\n ((typeof endpoint !== 'object') || (endpoint === null))) {\n\n reportError('The `' + fieldName + '\\' property must exist' +\n ' and be either a string or an object.');\n }\n\n if (typeof endpoint === 'object') {\n if ((typeof endpoint.url !== 'string') || !endpoint.url) {\n reportError('The url property must exist in the `' +\n fieldName + '\\' field and must be a non-empty string.');\n }\n\n if ((typeof endpoint.auth !== 'undefined') &&\n ((typeof endpoint.auth !== 'object') ||\n endpoint.auth === null)) {\n\n reportError('`' + fieldName +\n '.auth\\' must be a non-null object.');\n }\n\n if ((typeof endpoint.headers !== 'undefined') &&\n ((typeof endpoint.headers !== 'object') ||\n endpoint.headers === null)) {\n\n reportError('`' + fieldName +\n '.headers\\' must be a non-null object.');\n }\n }\n }\n\n var isReplicator = (userCtx.roles.indexOf('_replicator') >= 0);\n var isAdmin = (userCtx.roles.indexOf('_admin') >= 0);\n\n if (oldDoc && !newDoc._deleted && !isReplicator &&\n (oldDoc._replication_state === 'triggered')) {\n reportError('Only the replicator can edit replication documents ' +\n 'that are in the triggered state.');\n }\n\n if (!newDoc._deleted) {\n validateEndpoint(newDoc.source, 'source');\n validateEndpoint(newDoc.target, 'target');\n\n if ((typeof newDoc.create_target !== 'undefined') &&\n (typeof newDoc.create_target !== 'boolean')) {\n\n reportError('The `create_target\\' field must be a boolean.');\n }\n\n if ((typeof newDoc.continuous !== 'undefined') &&\n (typeof newDoc.continuous !== 'boolean')) {\n\n reportError('The `continuous\\' field must be a boolean.');\n }\n\n if ((typeof newDoc.doc_ids !== 'undefined') &&\n !isArray(newDoc.doc_ids)) {\n\n reportError('The `doc_ids\\' field must be an array of strings.');\n }\n\n if ((typeof newDoc.selector !== 'undefined') &&\n (typeof newDoc.selector !== 'object')) {\n\n reportError('The `selector\\' field must be an object.');\n }\n\n if ((typeof newDoc.filter !== 'undefined') &&\n ((typeof newDoc.filter !== 'string') || !newDoc.filter)) {\n\n reportError('The `filter\\' field must be a non-empty string.');\n }\n\n if ((typeof newDoc.doc_ids !== 'undefined') &&\n (typeof newDoc.selector !== 'undefined')) {\n\n reportError('`doc_ids\\' field is incompatible with `selector\\'.');\n }\n\n if ( ((typeof newDoc.doc_ids !== 'undefined') ||\n (typeof newDoc.selector !== 'undefined')) &&\n (typeof newDoc.filter !== 'undefined') ) {\n\n reportError('`filter\\' field is incompatible with `selector\\' and `doc_ids\\'.');\n }\n\n if ((typeof newDoc.query_params !== 'undefined') &&\n ((typeof newDoc.query_params !== 'object') ||\n newDoc.query_params === null)) {\n\n reportError('The `query_params\\' field must be an object.');\n }\n\n if (newDoc.user_ctx) {\n var user_ctx = newDoc.user_ctx;\n\n if ((typeof user_ctx !== 'object') || (user_ctx === null)) {\n reportError('The `user_ctx\\' property must be a ' +\n 'non-null object.');\n }\n\n if (!(user_ctx.name === null ||\n (typeof user_ctx.name === 'undefined') ||\n ((typeof user_ctx.name === 'string') &&\n user_ctx.name.length > 0))) {\n\n reportError('The `user_ctx.name\\' property must be a ' +\n 'non-empty string or null.');\n }\n\n if (!isAdmin && (user_ctx.name !== userCtx.name)) {\n reportError('The given `user_ctx.name\\' is not valid');\n }\n\n if (user_ctx.roles && !isArray(user_ctx.roles)) {\n reportError('The `user_ctx.roles\\' property must be ' +\n 'an array of strings.');\n }\n\n if (!isAdmin && user_ctx.roles) {\n for (var i = 0; i < user_ctx.roles.length; i++) {\n var role = user_ctx.roles[i];\n\n if (typeof role !== 'string' || role.length === 0) {\n reportError('Roles must be non-empty strings.');\n }\n if (userCtx.roles.indexOf(role) === -1) {\n reportError('Invalid role (`' + role +\n '\\') in the `user_ctx\\'');\n }\n }\n }\n } else {\n if (!isAdmin) {\n reportError('The `user_ctx\\' property is missing (it is ' +\n 'optional for admins only).');\n }\n }\n } else {\n if (!isAdmin) {\n if (!oldDoc.user_ctx || (oldDoc.user_ctx.name !== userCtx.name)) {\n reportError('Replication documents can only be deleted by ' +\n 'admins or by the users who created them.');\n }\n }\n }\n }\n" - } - }, - { - "id":"_design/filters", - "key":"_design/filters", - "value":{ - "rev":"1-1390740c4877979dbe8998382876556c" - }, - "doc":{ - "_id":"_design/filters", - "_rev":"1-1390740c4877979dbe8998382876556c", - "filters": { - "afilter": "\n function (doc, req) { if (doc.type === 'a-doc') { return true; } \n return false }" - } - } - }, - { - "id":"c94d4839d1897105cb75e1251e0003ea", - "key":"c94d4839d1897105cb75e1251e0003ea", - "value":{ - "rev":"3-4559cb522de85ce03bd0e1991025e89a" + "source": { + "headers": { + "Authorization": "Basic dGVzdGVyOnRlc3RlcnBhc3M=" + }, + "url": "http://dev:5984/animaldb" }, - "doc":{"_id":"c94d4839d1897105cb75e1251e0003ea", - "_rev":"3-4559cb522de85ce03bd0e1991025e89a", - "user_ctx":{ - "name":"tester", - "roles":["_admin", "_reader", "_writer"]}, - "source":{ - "headers":{ - "Authorization":"Basic dGVzdGVyOnRlc3RlcnBhc3M=" - }, - "url":"http://dev:5984/animaldb"}, - "target":{ - "headers":{ - "Authorization":"Basic dGVzdGVyOnRlc3RlcnBhc3M="}, - "url":"http://dev:5984/animaldb-clone" + "target": { + "headers": { + "Authorization": "Basic dGVzdGVyOnRlc3RlcnBhc3M=" }, - "create_target":true, - "continuous":false, - "owner":"tester", - "_replication_state":"completed", - "_replication_state_time":"2017-02-28T12:16:28+00:00", - "_replication_id":"0ce2939af29317b5dbe11c15570ddfda", - "_replication_stats":{ - "revisions_checked":14, - "missing_revisions_found":14, - "docs_read":14, - "docs_written":14, - "changes_pending":null, - "doc_write_failures":0, - "checkpointed_source_seq":"15-g1AAAAJDeJyV0N0NgjAQAOAKRnlzBJ3AcKWl9Uk20ZbSEII4gm6im-gmugke1AQJ8aFpck3u50vuakJIVIaGrJqzKSADKrYxPqixECii123bVmWoFidMLGVsqEjYtP0voTcY9f6rzHqFKcglsz5K1imHkcJTnoJVPsqxUy4jxepEioJ7KM0cI7nih9BtkDSlkAif2zjp7qRHJwW9lLNdDkZ6S08nvQZJMsNT4b_d20k_d4oVE1aK6VT1AXTajes" - } + "url": "http://dev:5984/animaldb-clone" + }, + "create_target": true, + "continuous": false, + "owner": "tester", + "_replication_state": "completed", + "_replication_state_time": "2017-02-28T12:16:28+00:00", + "_replication_id": "0ce2939af29317b5dbe11c15570ddfda", + "_replication_stats": { + "revisions_checked": 14, + "missing_revisions_found": 14, + "docs_read": 14, + "docs_written": 14, + "changes_pending": null, + "doc_write_failures": 0, + "checkpointed_source_seq": "15-g1AAAAJDeJyV0N0NgjAQAOAKRnlzBJ3AcKWl9Uk20ZbSEII4gm6im-gmugke1AQJ8aFpck3u50vuakJIVIaGrJqzKSADKrYxPqixECii123bVmWoFidMLGVsqEjYtP0voTcY9f6rzHqFKcglsz5K1imHkcJTnoJVPsqxUy4jxepEioJ7KM0cI7nih9BtkDSlkAif2zjp7qRHJwW9lLNdDkZ6S08nvQZJMsNT4b_d20k_d4oVE1aK6VT1AXTajes" } } ]}; @@ -577,15 +546,71 @@ describe('Replication API', () => { }); it("returns parsedReplicationDocs and ignores all design docs", () => { - fetchMock.getOnce('./_scheduler/jobs', 404); - fetchMock.get('./_replicator/_all_docs?include_docs=true&limit=100', _repDocs); + fetchMock.getOnce('/_scheduler/jobs', 404); + fetchMock.post((url, options) => { + const body = JSON.parse(options.body); + return url === "/_replicator/_find" + && body.limit === 6 + && body.skip === 0; + }, _repDocs); return supportNewApi(true) - .then(fetchReplicationDocs) - .then(docs => { - expect(docs.length).toBe(1); + .then(() => fetchReplicationDocs({docsPerPage: 5, page: 1})) + .then(({docs}) => { + expect(docs).toHaveLength(1); expect(docs[0]._id).toBe("c94d4839d1897105cb75e1251e0003ea"); }); }); + + it("paginates to page 2 correctly", () => { + fetchMock.getOnce('/_scheduler/jobs', 200); + fetchMock.post((url, options) => { + const body = JSON.parse(options.body); + return url === "/_replicator/_find" + && body.limit === 11 + && body.skip === 10; + }, _repDocs); + fetchMock.get('/_scheduler/docs?limit=11&skip=10', _schedDocs); + return supportNewApi(true) + .then(() => fetchReplicationDocs({docsPerPage: 10, page: 2})) + .then(({canShowNext}) => { + expect(canShowNext).toBeFalsy; + }); + }); + + it("sets canShowNext true and trims docs correctly", () => { + const clonedDoc = _repDocs.docs[2]; + const repDocs = { + docs: [{ + ...clonedDoc, + _id: '1', + }, { + ...clonedDoc, + _id: '2', + }, { + + ...clonedDoc, + _id: '3', + }, { + ...clonedDoc, + _id: '4', + }] + }; + + fetchMock.getOnce('/_scheduler/jobs', 200); + fetchMock.post((url, options) => { + const body = JSON.parse(options.body); + return url === "/_replicator/_find" + && body.limit === 4 + && body.skip === 6; + }, repDocs); + fetchMock.get('/_scheduler/docs?limit=4&skip=6', _schedDocs); + return supportNewApi(true) + .then(() => fetchReplicationDocs({docsPerPage: 3, page: 3})) + .then(({docs, canShowNext}) => { + expect(canShowNext).toBeTruthy(); + expect(docs).toHaveLength(3); + }); + }); }); describe('new api', () => { @@ -594,15 +619,71 @@ describe('Replication API', () => { }); it("returns parsedReplicationDocs", () => { - fetchMock.getOnce('./_scheduler/jobs', 200); - fetchMock.get('./_replicator/_all_docs?include_docs=true&limit=100', _repDocs); - fetchMock.get('./_scheduler/docs?include_docs=true', _schedDocs); + fetchMock.getOnce('/_scheduler/jobs', 200); + fetchMock.post((url, options) => { + const body = JSON.parse(options.body); + return url === "/_replicator/_find" + && body.limit === 6 + && body.skip === 0; + }, _repDocs); + fetchMock.get('/_scheduler/docs?limit=6&skip=0', _schedDocs); return supportNewApi(true) - .then(fetchReplicationDocs) - .then(docs => { - expect(docs.length).toBe(1); + .then(() => fetchReplicationDocs({docsPerPage: 5, page: 1})) + .then(({docs}) => { + expect(docs).toHaveLength(1); expect(docs[0]._id).toBe("c94d4839d1897105cb75e1251e0003ea"); - expect(docs[0].stateTime.toDateString()).toBe((new Date('2017-03-07T14:46:17')).toDateString()); + expect(docs[0].stateTime.toDateString()).toBe(new Date('2017-03-07T14:46:17').toDateString()); + }); + }); + + it("paginates to page 2 correctly", () => { + fetchMock.getOnce('/_scheduler/jobs', 200); + fetchMock.post((url, options) => { + const body = JSON.parse(options.body); + return url === "/_replicator/_find" + && body.limit === 11 + && body.skip === 10; + }, _repDocs); + fetchMock.get('/_scheduler/docs?limit=11&skip=10', _schedDocs); + return supportNewApi(true) + .then(() => fetchReplicationDocs({docsPerPage: 10, page: 2})) + .then(({canShowNext}) => { + expect(canShowNext).toBeFalsy(); + }); + }); + + it("sets canShowNext true and trims docs correctly", () => { + const clonedDoc = _repDocs.docs[2]; + const repDocs = { + docs: [{ + ...clonedDoc, + _id: '1', + }, { + ...clonedDoc, + _id: '2', + }, { + + ...clonedDoc, + _id: '3', + }, { + ...clonedDoc, + _id: '4', + }] + }; + + fetchMock.getOnce('/_scheduler/jobs', 200); + fetchMock.post((url, options) => { + const body = JSON.parse(options.body); + return url === "/_replicator/_find" + && body.limit === 4 + && body.skip === 6; + }, repDocs); + fetchMock.get('/_scheduler/docs?limit=4&skip=6', _schedDocs); + return supportNewApi(true) + .then(() => fetchReplicationDocs({docsPerPage: 3, page: 3})) + .then(({docs, canShowNext}) => { + expect(canShowNext).toBeTruthy(); + expect(docs).toHaveLength(3); }); }); }); diff --git a/app/addons/replication/actions.js b/app/addons/replication/actions.js index 745a36120..25affec98 100644 --- a/app/addons/replication/actions.js +++ b/app/addons/replication/actions.js @@ -26,7 +26,6 @@ import { createReplicatorDB } from './api'; - export const initReplicator = (routeLocalSource, localSource) => dispatch => { if (routeLocalSource && routeLocalSource !== localSource) { dispatch({ @@ -85,9 +84,9 @@ export const replicate = (params) => dispatch => { clear: true }); - dispatch(getReplicationActivity()); - FauxtonAPI.navigate('#/replication'); - }).catch(json => { + dispatch(getReplicationActivity(params.pagination)); + }) + .catch(json => { if (json.error && json.error === "not_found") { return createReplicatorDB().then(() => { return replicate(params)(dispatch); @@ -111,19 +110,16 @@ export const clearReplicationForm = () => { return { type: ActionTypes.REPLICATION_CLEAR_FORM }; }; -export const getReplicationActivity = () => dispatch => { +export const getReplicationActivity = (params) => dispatch => { dispatch({ type: ActionTypes.REPLICATION_FETCHING_STATUS, }); - supportNewApi() - .then(supportNewApi => { - return fetchReplicationDocs(supportNewApi); - }) - .then(docs => { + fetchReplicationDocs(params) + .then(docsInfo => { dispatch({ type: ActionTypes.REPLICATION_STATUS, - options: docs + options: docsInfo }); }); }; @@ -201,7 +197,7 @@ export const clearSelectedReplicates = () => { }; }; -export const deleteDocs = (docs) => dispatch => { +export const deleteDocs = (docs, pagination) => dispatch => { const bulkDocs = docs.map(({raw: doc}) => { doc._deleted = true; return doc; @@ -237,7 +233,7 @@ export const deleteDocs = (docs) => dispatch => { }); dispatch(clearSelectedDocs()); - dispatch(getReplicationActivity()); + dispatch(getReplicationActivity(pagination)); }) .catch(resp => { resp.json() @@ -416,3 +412,43 @@ export const checkForNewApi = () => dispatch => { }); }); }; + +export const updatePerPageResults = (amount) => { + const newPaginate = { + page: 1, + docsPerPage: amount + }; + return (dispatch) => { + dispatch({ + type: ActionTypes.REPLICATION_UPDATE_PER_PAGE_RESULTS, + options: amount + }); + + dispatch(getReplicationActivity(newPaginate)); + }; +}; + +export const paginateNext = (paginate) => { + return dispatch => { + dispatch({ + type: ActionTypes.REPLICATION_NEXT_PAGE + }); + + paginate.page += 1; + dispatch(getReplicationActivity(paginate)); + }; +}; + +export const paginatePrevious = (paginate) => { + return dispatch => { + dispatch({ + type: ActionTypes.REPLICATION_PREVIOUS_PAGE + }); + + if (paginate.page > 1) { + paginate.page -= 1; + } + + dispatch(getReplicationActivity(paginate)); + }; +}; diff --git a/app/addons/replication/actiontypes.js b/app/addons/replication/actiontypes.js index 714386863..e5d56e94f 100644 --- a/app/addons/replication/actiontypes.js +++ b/app/addons/replication/actiontypes.js @@ -38,5 +38,8 @@ export default { REPLICATION_CLEAR_SELECTED_REPLICATES: 'REPLICATION_CLEAR_SELECTED_REPLICATES', REPLICATION_FETCHING_FORM_STATE: 'REPLICATION_FETCHING_FORM_STATE', REPLICATION_HIDE_PASSWORD_MODAL: 'REPLICATION_HIDE_PASSWORD_MODAL', - REPLICATION_SHOW_PASSWORD_MODAL: 'REPLICATION_SHOW_PASSWORD_MODAL' + REPLICATION_SHOW_PASSWORD_MODAL: 'REPLICATION_SHOW_PASSWORD_MODAL', + REPLICATION_UPDATE_PER_PAGE_RESULTS: 'REPLICATION_UPDATE_PER_PAGE_RESULTS', + REPLICATION_NEXT_PAGE: 'REPLICATION_NEXT_PAGE', + REPLICATION_PREVIOUS_PAGE: 'REPLICATION_PREVIOUS_PAGE' }; diff --git a/app/addons/replication/api.js b/app/addons/replication/api.js index 75dee5f0e..dc66c977d 100644 --- a/app/addons/replication/api.js +++ b/app/addons/replication/api.js @@ -13,10 +13,12 @@ import '@webcomponents/url'; import Constants from './constants'; import FauxtonAPI from '../../core/api'; +import app from '../../app'; import Helpers from '../../helpers'; import {get, post, put} from '../../core/ajax'; import base64 from 'base-64'; import _ from 'lodash'; +import { removeOverflowDocsAndCalculateHasNext } from '../documents/index-results/actions/fetch'; let newApiPromise = null; export const supportNewApi = (forceCheck) => { @@ -260,24 +262,22 @@ export const getDocUrl = (doc) => { return removeSensitiveUrlInfo(url); }; -export const parseReplicationDocs = (rows) => { - return rows.map(row => row.doc).map(doc => { - return { - _id: doc._id, - _rev: doc._rev, - selected: false, //use this field for bulk delete in the ui - source: getDocUrl(doc.source), - target: getDocUrl(doc.target), - createTarget: doc.create_target, - continuous: doc.continuous === true ? true : false, - status: doc._replication_state, - errorMsg: doc._replication_state_reason ? doc._replication_state_reason : '', - statusTime: new Date(doc._replication_state_time), - startTime: new Date(doc._replication_start_time), - url: `#/database/_replicator/${encodeURIComponent(doc._id)}`, - raw: doc - }; - }); +export const parseReplicationDocs = docs => { + return docs.map(doc => ({ + _id: doc._id, + _rev: doc._rev, + selected: false, //use this field for bulk delete in the ui + source: getDocUrl(doc.source), + target: getDocUrl(doc.target), + createTarget: doc.create_target, + continuous: doc.continuous === true, + status: doc._replication_state, + errorMsg: doc._replication_state_reason ? doc._replication_state_reason : '', + statusTime: new Date(doc._replication_state_time), + startTime: new Date(doc._replication_start_time), + url: `#/database/_replicator/${encodeURIComponent(doc._id)}`, + raw: doc + })); }; export const convertState = (state) => { @@ -308,34 +308,69 @@ export const combineDocsAndScheduler = (docs, schedulerDocs) => { }); }; -export const fetchReplicationDocs = () => { +export const fetchReplicationDocs = ({docsPerPage, page}) => { + const limit = docsPerPage + 1; + const skip = (page - 1) * docsPerPage; + + const mangoPayload = { + limit, + skip, + selector: { + _id: { + $gte: null + } + } + }; + + const schedulerDocsPayload = { + limit, + skip + }; + return supportNewApi() .then(newApi => { - const url = Helpers.getServerUrl('/_replicator/_all_docs?include_docs=true&limit=100'); - const docsPromise = get(url) + const url = Helpers.getServerUrl('/_replicator/_find'); + const docsPromise = post(url, mangoPayload) .then((res) => { - if (res.error) { - return []; - } - - return parseReplicationDocs(res.rows.filter(row => row.id.indexOf("_design/") === -1)); + const docs = res.error ? [] : res.docs; + return parseReplicationDocs(docs); }); if (!newApi) { - return docsPromise; + return docsPromise + .then(docs => { + const { + finalDocList, + canShowNext + } = removeOverflowDocsAndCalculateHasNext(docs, false, docsPerPage + 1); + return { + docs: finalDocList, + canShowNext + }; + }); } - const schedulerPromise = fetchSchedulerDocs(); + const schedulerPromise = fetchSchedulerDocs(schedulerDocsPayload); return FauxtonAPI.Promise.join(docsPromise, schedulerPromise, (docs, schedulerDocs) => { return combineDocsAndScheduler(docs, schedulerDocs); }) + .then(docs => { + const { + finalDocList, + canShowNext + } = removeOverflowDocsAndCalculateHasNext(docs, false, docsPerPage + 1); + return { + docs: finalDocList, + canShowNext + }; + }) .catch(() => { return []; }); }); }; -export const fetchSchedulerDocs = () => { - const url = Helpers.getServerUrl('/_scheduler/docs?include_docs=true'); +export const fetchSchedulerDocs = (params) => { + const url = Helpers.getServerUrl(`/_scheduler/docs?${app.utils.queryParams(params)}`); return get(url) .then((res) => { if (res.error) { diff --git a/app/addons/replication/assets/less/replication.less b/app/addons/replication/assets/less/replication.less index ec1c0ad6b..4bea76658 100644 --- a/app/addons/replication/assets/less/replication.less +++ b/app/addons/replication/assets/less/replication.less @@ -374,3 +374,10 @@ td.replication__empty-row { .replication__activity-caveat { padding-left: 80px; } + +.replication__paginate-footer { + position: fixed; + bottom: 0px; + right: 0px; + width: 100%; +} diff --git a/app/addons/replication/components/activity.js b/app/addons/replication/components/activity.js index 0d1ae981a..680605d85 100644 --- a/app/addons/replication/components/activity.js +++ b/app/addons/replication/components/activity.js @@ -15,6 +15,7 @@ import React from 'react'; import {DeleteModal} from './modals'; import {ReplicationTable} from './common-table'; import {ReplicationHeader} from './common-activity'; +import PaginationFooter from '../../documents/index-results/components/pagination/PaginationFooter'; export default class Activity extends React.Component { constructor (props) { @@ -48,7 +49,7 @@ export default class Activity extends React.Component { docs = this.props.docs.filter(doc => doc.selected); } - this.props.deleteDocs(docs); + this.props.deleteDocs(docs, this.props.pagination); this.closeModal(); } @@ -66,7 +67,14 @@ export default class Activity extends React.Component { selectAllDocs, someDocsSelected, allDocsSelected, - selectDoc + selectDoc, + pageStart, + pageEnd, + docsPerPage, + updatePerPageResults, + paginateNext, + paginatePrevious, + pagination } = this.props; const {modalVisible} = this.state; @@ -90,6 +98,25 @@ export default class Activity extends React.Component { column={activitySort.column} changeSort={changeActivitySort} /> +
| @@ -400,9 +400,9 @@ export class ReplicationTable extends React.Component { Target- | + | Start Time - + | Type
diff --git a/app/addons/replication/components/newreplication.js b/app/addons/replication/components/newreplication.js
index 08da3b3a9..bf78b9f9c 100644
--- a/app/addons/replication/components/newreplication.js
+++ b/app/addons/replication/components/newreplication.js
@@ -249,7 +249,8 @@ export default class NewReplicationController extends React.Component {
       sourceAuth,
       targetAuthType,
       targetAuth,
-      targetDatabasePartitioned
+      targetDatabasePartitioned,
+      pagination
     } = this.props;
 
     let _rev;
@@ -274,7 +275,8 @@ export default class NewReplicationController extends React.Component {
       sourceAuth,
       targetAuthType,
       targetAuth,
-      targetDatabasePartitioned
+      targetDatabasePartitioned,
+      pagination
     });
   }
 
diff --git a/app/addons/replication/container.js b/app/addons/replication/container.js
index 5be9079e6..29fd11178 100644
--- a/app/addons/replication/container.js
+++ b/app/addons/replication/container.js
@@ -21,7 +21,10 @@ import {
   changeActivitySort,
   deleteReplicates,
   selectAllReplicates,
-  selectReplicate
+  selectReplicate,
+  updatePerPageResults,
+  paginateNext,
+  paginatePrevious
 } from './actions';
 
 import {
@@ -53,7 +56,12 @@ import {
   isReplicateInfoLoading,
   getAllReplicateSelected,
   getReplicateInfo,
-  someReplicateSelected
+  someReplicateSelected,
+  getPagination,
+  getPageEnd,
+  getPageStart,
+  getDocsPerPage,
+  canShowNext
 } from './reducers';
 
 const mapStateToProps = ({replication, databases}, ownProps) => {
@@ -102,7 +110,13 @@ const mapStateToProps = ({replication, databases}, ownProps) => {
     replicateLoading: isReplicateInfoLoading(replication),
     replicateInfo: getReplicateInfo(replication),
     allReplicateSelected: getAllReplicateSelected(replication),
-    someReplicateSelected: someReplicateSelected(replication)
+    someReplicateSelected: someReplicateSelected(replication),
+    pagination: getPagination(replication),
+    pageStart: getPageStart(replication),
+    pageEnd: getPageEnd(replication),
+    docsPerPage: getDocsPerPage(replication),
+    canShowNext: canShowNext(replication)
+
   };
 };
 
@@ -114,7 +128,7 @@ const mapDispatchToProps = (dispatch) => {
     },
     clearReplicationForm: () => dispatch(clearReplicationForm()),
     initReplicator: (localSource) => dispatch(initReplicator(localSource)),
-    getReplicationActivity: () => dispatch(getReplicationActivity()),
+    getReplicationActivity: (params) => dispatch(getReplicationActivity(params)),
     getReplicateActivity: () => dispatch(getReplicateActivity()),
     getReplicationStateFrom: (id) => dispatch(getReplicationStateFrom(id)),
     getDatabasesList: () => dispatch(getDatabasesList()),
@@ -124,12 +138,15 @@ const mapDispatchToProps = (dispatch) => {
     filterReplicate: (filter) => dispatch(filterReplicate(filter)),
     filterDocs: (filter) => dispatch(filterDocs(filter)),
     selectDoc: (doc) => dispatch(selectDoc(doc)),
-    deleteDocs: (docs) => dispatch(deleteDocs(docs)),
+    deleteDocs: (docs, pagination) => dispatch(deleteDocs(docs, pagination)),
     selectAllDocs: () => dispatch(selectAllDocs()),
     changeActivitySort: (sort) => dispatch(changeActivitySort(sort)),
     selectAllReplicates: () => dispatch(selectAllReplicates()),
     deleteReplicates: (replicates) => dispatch(deleteReplicates(replicates)),
-    selectReplicate: (replicate) => dispatch(selectReplicate(replicate))
+    selectReplicate: (replicate) => dispatch(selectReplicate(replicate)),
+    updatePerPageResults: (amount) => dispatch(updatePerPageResults(amount)),
+    paginateNext: (params) => dispatch(paginateNext(params)),
+    paginatePrevious: (params) => dispatch(paginatePrevious(params))
   };
 };
 
diff --git a/app/addons/replication/controller.js b/app/addons/replication/controller.js
index 0c7cc00dd..e395a38a5 100644
--- a/app/addons/replication/controller.js
+++ b/app/addons/replication/controller.js
@@ -24,9 +24,14 @@ const {LoadLines, Polling, RefreshBtn} = Components;
 
 export default class ReplicationController extends React.Component {
 
+  constructor(props) {
+    super(props);
+    this.onRefresh = this.onRefresh.bind(this);
+  }
+
   loadReplicationInfo (props, oldProps) {
     this.props.initReplicator(props.routeLocalSource, props.localSource);
-    this.getAllActivity();
+    this.getAllActivity(props.pagination);
     this.loadReplicationStateFrom(props, oldProps);
   }
 
@@ -37,11 +42,15 @@ export default class ReplicationController extends React.Component {
     }
   }
 
-  getAllActivity () {
-    this.props.getReplicationActivity();
+  getAllActivity (pagination) {
+    this.props.getReplicationActivity(pagination);
     this.props.getReplicateActivity();
   }
 
+  onRefresh() {
+    this.getAllActivity(this.props.pagination);
+  }
+
   componentDidMount () {
     this.props.checkForNewApi();
     this.props.getDatabasesList();
@@ -70,7 +79,8 @@ export default class ReplicationController extends React.Component {
       hideConflictModal, isConflictModalVisible, filterDocs,
       filterReplicate, replicate, clearReplicationForm, selectAllDocs, changeActivitySort, selectDoc,
       deleteDocs, deleteReplicates, selectAllReplicates, selectReplicate,
-      sourceAuthType, sourceAuth, targetAuthType, targetAuth, targetDatabasePartitioned, allowNewPartitionedLocalDbs
+      sourceAuthType, sourceAuth, targetAuthType, targetAuth, pageStart, pageEnd, docsPerPage,
+      updatePerPageResults, paginateNext, pagination, paginatePrevious, targetDatabasePartitioned, allowNewPartitionedLocalDbs
     } = this.props;
 
     if (tabSection === 'new replication') {
@@ -106,6 +116,7 @@ export default class ReplicationController extends React.Component {
         checkReplicationDocID={checkReplicationDocID}
         authenticated={authenticated}
         submittedNoChange={submittedNoChange}
+        pagination={pagination}
       />;
     }
 
@@ -125,6 +136,8 @@ export default class ReplicationController extends React.Component {
         activitySort={activitySort}
         changeActivitySort={changeActivitySort}
         deleteDocs={deleteReplicates}
+        pageStart={pageStart}
+        pageEnd={pageEnd}
       />;
     }
 
@@ -143,6 +156,13 @@ export default class ReplicationController extends React.Component {
       deleteDocs={deleteDocs}
       activitySort={activitySort}
       changeActivitySort={changeActivitySort}
+      pageStart={pageStart}
+      pageEnd={pageEnd}
+      docsPerPage={docsPerPage}
+      updatePerPageResults={updatePerPageResults}
+      paginateNext={paginateNext}
+      paginatePrevious={paginatePrevious}
+      pagination={pagination}
     />;
   }
 
@@ -162,10 +182,10 @@ export default class ReplicationController extends React.Component {
           max={600}
           startValue={300}
           stepSize={60}
-          onPoll={this.getAllActivity.bind(this)}
+          onPoll={this.onRefresh}
         /> | 
|---|