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} /> +
+ 1} + perPage={docsPerPage} + toggleShowAllColumns={false} + docs={this.props.docs} + pageStart={pageStart} + pageEnd={pageEnd} + updatePerPageResults={updatePerPageResults} + paginateNext={paginateNext} + paginatePrevious={paginatePrevious} + queryOptionsParams={{}} + fetchParams={pagination} + /> +
+ -
@@ -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} /> ); diff --git a/app/addons/replication/reducers.js b/app/addons/replication/reducers.js index 40a276a1d..c9539e8c5 100644 --- a/app/addons/replication/reducers.js +++ b/app/addons/replication/reducers.js @@ -34,6 +34,20 @@ const loadActivitySort = () => { return sort; }; +const loadDocsPerPage = () => { + let docsPerPage = app.utils.localStorageGet('replication-docs-per-page'); + + if (!docsPerPage) { + docsPerPage = 10; + } + + return docsPerPage; +}; + +const saveDocsPerPage = (docsPerPage) => { + app.utils.localStorageSet('replication-docs-per-page', docsPerPage); +}; + const validFieldMap = { remoteSource: 'remoteSource', remoteTarget: 'remoteTarget', @@ -89,7 +103,12 @@ const initialState = { replicateInfo: [], checkingAPI: true, - activitySort: loadActivitySort() + activitySort: loadActivitySort(), + pagination: { + docsPerPage: loadDocsPerPage(), + page: 1, + canShowNext: false + } }; const clearForm = (state) => { @@ -261,7 +280,11 @@ const replication = (state = initialState, {type, options}) => { return { ...state, activityLoading: false, - statusDocs: options + statusDocs: options.docs, + pagination: { + ...state.pagination, + canShowNext: options.canShowNext + } }; case ActionTypes.REPLICATION_FILTER_DOCS: @@ -348,6 +371,40 @@ const replication = (state = initialState, {type, options}) => { allReplicateSelected: false }; + case ActionTypes.REPLICATION_UPDATE_PER_PAGE_RESULTS: + const newResultsState = { ...state }; + newResultsState.pagination.docsPerPage = options; + saveDocsPerPage(options); + return { + ...state, + pagination: { + ...state.pagination, + docsPerPage: options, + page: 1, + canShowNext: false + } + }; + + case ActionTypes.REPLICATION_NEXT_PAGE: + return { + ...state, + pagination: { + ...state.pagination, + docsPerPage: state.pagination.docsPerPage, + page: state.pagination.page + 1 + } + }; + + case ActionTypes.REPLICATION_PREVIOUS_PAGE: + return { + ...state, + pagination: { + ...state.pagination, + docsPerPage: state.pagination.docsPerPage, + page: state.pagination.page - 1 + } + }; + default: return state; } @@ -406,4 +463,16 @@ export const getReplicateInfo = (state) => { }); }; +export const getPagination = (state) => state.pagination; +export const getPageStart = (state) => 1 + (state.pagination.page - 1) * state.pagination.docsPerPage; +export const getPageEnd = (state) => { + if (state.isLoading || !state.statusDocs || state.statusDocs.length === 0) { + return false; + } + const pageStart = (state.pagination.page - 1) * state.pagination.docsPerPage; + return pageStart + state.statusDocs.length; +}; +export const getDocsPerPage = (state) => state.pagination.docsPerPage; +export const canShowNext = (state) => state.pagination.canShowNext; + export default replication; diff --git a/app/app.js b/app/app.js index 713b8de50..4679a376d 100644 --- a/app/app.js +++ b/app/app.js @@ -9,13 +9,13 @@ // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. +import 'react-toastify/dist/ReactToastify.min.css'; import "jquery"; import app from "./initialize"; import _ from "lodash"; import Helpers from "./helpers"; import Utils from "./core/utils"; import FauxtonAPI from "./core/api"; -import 'react-toastify/dist/ReactToastify.min.css'; import "../assets/less/fauxton.less"; // Make sure we have a console.log