app/addons/replication/actions.js (358 lines of code) (raw):

// Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy of // the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // 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 base64 from 'base-64'; import FauxtonAPI from '../../core/api'; import {get, post} from '../../core/ajax'; import ActionTypes from './actiontypes'; import Helpers from './helpers'; import MainHelper from '../../helpers'; import Constants from './constants'; import { supportNewApi, createReplicationDoc, fetchReplicateInfo, fetchReplicationDocs, decodeFullUrl, deleteReplicatesApi, createReplicatorDB } from './api'; export const initReplicator = (routeLocalSource, localSource) => dispatch => { if (routeLocalSource && routeLocalSource !== localSource) { dispatch({ type: ActionTypes.INIT_REPLICATION, options: { localSource: routeLocalSource } }); } }; export const getDatabasesList = () => dispatch => { const url = MainHelper.getServerUrl("/_all_dbs"); get(url) .then((databases) => { dispatch({ type: ActionTypes.REPLICATION_DATABASES_LOADED, options: { databases } }); }); }; export const replicate = (params, pageLimit) => dispatch => { const replicationDoc = createReplicationDoc(params); const url = MainHelper.getServerUrl("/_replicator"); const promise = post(url, replicationDoc); const source = Helpers.getDatabaseLabel(replicationDoc.source); const target = Helpers.getDatabaseLabel(replicationDoc.target); dispatch({ type: ActionTypes.REPLICATION_STARTING, }); const handleError = (json) => { FauxtonAPI.addNotification({ msg: json.reason, type: 'error', clear: true }); }; // Return promise for testing return promise .then(json => { if (!json.ok) { throw json; } FauxtonAPI.addNotification({ msg: `Replication from "${decodeURIComponent(source)}" to "${decodeURIComponent(target)}" has been scheduled.`, type: 'success', escape: false, clear: true }); dispatch(getReplicationActivity(pageLimit)); FauxtonAPI.navigate('#/replication'); }).catch(json => { if (json.error && json.error === "not_found") { return createReplicatorDB().then(() => { return replicate(params, pageLimit)(dispatch); }).catch(handleError); } handleError(json); }); }; export const updateFormField = (fieldName, value) => { return { type: ActionTypes.REPLICATION_UPDATE_FORM_FIELD, options: { fieldName: fieldName, value: value } }; }; export const clearReplicationForm = () => { return { type: ActionTypes.REPLICATION_CLEAR_FORM }; }; export const getReplicationActivity = (pageLimit) => (dispatch) => { dispatch({ type: ActionTypes.REPLICATION_FETCHING_STATUS, }); fetchReplicationDocs(pageLimit) .then(docs => { dispatch({ type: ActionTypes.REPLICATION_STATUS, options: docs }); }); }; export const getReplicateActivity = () => dispatch => { supportNewApi() .then(newApi => { if (!newApi) { return; } dispatch({ type: ActionTypes.REPLICATION_FETCHING_REPLICATE_STATUS, }); fetchReplicateInfo() .then(replicateInfo => { dispatch({ type: ActionTypes.REPLICATION_REPLICATE_STATUS, options: replicateInfo }); }); }); }; export const filterDocs = (filter) => { return { type: ActionTypes.REPLICATION_FILTER_DOCS, options: filter }; }; export const filterReplicate = (filter) => { return { type: ActionTypes.REPLICATION_FILTER_REPLICATE, options: filter }; }; export const selectAllDocs = () => { return { type: ActionTypes.REPLICATION_TOGGLE_ALL_DOCS }; }; export const selectDoc = (id) => { return { type: ActionTypes.REPLICATION_TOGGLE_DOC, options: id }; }; export const selectAllReplicates = () => { return { type: ActionTypes.REPLICATION_TOGGLE_ALL_REPLICATE }; }; export const selectReplicate = (id) => { return { type: ActionTypes.REPLICATION_TOGGLE_REPLICATE, options: id }; }; export const clearSelectedDocs = () => { return { type: ActionTypes.REPLICATION_CLEAR_SELECTED_DOCS }; }; export const clearSelectedReplicates = () => { return { type: ActionTypes.REPLICATION_CLEAR_SELECTED_REPLICATES }; }; export const deleteDocs = (docs, pageLimit) => dispatch => { const bulkDocs = docs.map(({raw: doc}) => { doc._deleted = true; return doc; }); FauxtonAPI.addNotification({ msg: `Deleting doc${bulkDocs.length > 1 ? 's' : ''}.`, type: 'success', escape: false, clear: true }); const url = MainHelper.getServerUrl('/_replicator/_bulk_docs'); post(url, {docs: bulkDocs}, {raw: true}) .then(resp => { if (!resp.ok) { throw resp; } return resp; }) .then(() => { let msg = 'The selected documents have been deleted.'; if (docs.length === 1) { msg = `Document "${docs[0]._id}" has been deleted`; } FauxtonAPI.addNotification({ msg: msg, type: 'success', escape: false, clear: true }); dispatch(clearSelectedDocs()); dispatch(getReplicationActivity(pageLimit)); }) .catch(resp => { resp.json() .then(error => { FauxtonAPI.addNotification({ msg: error.reason, type: 'error', clear: true }); }); }); }; export const deleteReplicates = (replicates) => dispatch => { FauxtonAPI.addNotification({ msg: `Deleting _replicate${replicates.length > 1 ? 's' : ''}.`, type: 'success', escape: false, clear: true }); deleteReplicatesApi(replicates) .then(() => { let msg = 'The selected replications have been deleted.'; if (replicates.length === 1) { msg = `Replication "${replicates[0]._id}" has been deleted`; } dispatch(clearSelectedReplicates()); dispatch(getReplicateActivity()); FauxtonAPI.addNotification({ msg: msg, type: 'success', escape: false, clear: true }); }, (xhr) => { const errorMessage = JSON.parse(xhr.responseText); FauxtonAPI.addNotification({ msg: errorMessage.reason, type: 'error', clear: true }); }); }; const getAuthTypeAndCredentials = (repSourceOrTarget) => { const authTypeAndCreds = { type: Constants.REPLICATION_AUTH_METHOD.NO_AUTH, creds: {} }; if (repSourceOrTarget.headers && repSourceOrTarget.headers.Authorization) { // Removes 'Basic ' prefix const encodedCreds = repSourceOrTarget.headers.Authorization.substring(6); try { const decodedCreds = base64.decode(encodedCreds); authTypeAndCreds.type = Constants.REPLICATION_AUTH_METHOD.BASIC; authTypeAndCreds.creds = { username: decodedCreds.split(':')[0], password: decodedCreds.split(':')[1] }; return authTypeAndCreds; } catch (ex) { throw new Error(`Error decoding Authorization header from replication document`); } } // Tries to get creds using one of the custom auth methods // The extension should provide: // - 'getCredentials(obj)' method that extracts the credentials from obj which is the 'target'/'source' field of the replication doc. // - 'typeValue' field with an arbitrary ID representing the auth type the extension supports. const authExtensions = FauxtonAPI.getExtensions('Replication:Auth'); let credentials = undefined; let customAuthType = undefined; if (authExtensions) { authExtensions.map(ext => { if (!credentials && ext.getCredentials) { credentials = ext.getCredentials(repSourceOrTarget); customAuthType = ext.typeValue; } }); } if (credentials) { authTypeAndCreds.type = customAuthType; authTypeAndCreds.creds = credentials; } return authTypeAndCreds; }; const getTargetDatabasePartitioned = (createTargetParams) => { if (createTargetParams && createTargetParams.partitioned === true) { return true; } return false; }; export const getReplicationStateFrom = (id) => dispatch => { dispatch({ type: ActionTypes.REPLICATION_FETCHING_FORM_STATE }); const url = MainHelper.getServerUrl(`/_replicator/${encodeURIComponent(id)}`); get(url) .then((doc) => { const stateDoc = { replicationDocName: doc._id, replicationType: doc.continuous ? Constants.REPLICATION_TYPE.CONTINUOUS : Constants.REPLICATION_TYPE.ONE_TIME, }; const sourceUrl = _.isObject(doc.source) ? doc.source.url : doc.source; const targetUrl = _.isObject(doc.target) ? doc.target.url : doc.target; if (sourceUrl.indexOf(window.location.hostname) > -1) { const url = new URL(sourceUrl); stateDoc.replicationSource = Constants.REPLICATION_SOURCE.LOCAL; stateDoc.localSource = decodeURIComponent(url.pathname.slice(1)); } else { stateDoc.replicationSource = Constants.REPLICATION_SOURCE.REMOTE; stateDoc.remoteSource = decodeFullUrl(sourceUrl); } const sourceAuth = getAuthTypeAndCredentials(doc.source); stateDoc.sourceAuthType = sourceAuth.type; stateDoc.sourceAuth = sourceAuth.creds; if (targetUrl.indexOf(window.location.hostname) > -1) { const url = new URL(targetUrl); stateDoc.replicationTarget = Constants.REPLICATION_TARGET.EXISTING_LOCAL_DATABASE; stateDoc.localTarget = decodeURIComponent(url.pathname.slice(1)); } else { stateDoc.replicationTarget = Constants.REPLICATION_TARGET.EXISTING_REMOTE_DATABASE; stateDoc.remoteTarget = decodeFullUrl(targetUrl); } const targetAuth = getAuthTypeAndCredentials(doc.target); stateDoc.targetAuthType = targetAuth.type; stateDoc.targetAuth = targetAuth.creds; stateDoc.targetDatabasePartitioned = getTargetDatabasePartitioned(doc.create_target_params); dispatch({ type: ActionTypes.REPLICATION_SET_STATE_FROM_DOC, options: stateDoc }); }) .catch(error => { FauxtonAPI.addNotification({ msg: error.reason || error.message, type: 'error', clear: true }); }); }; export const showConflictModal = () => { return { type: ActionTypes.REPLICATION_SHOW_CONFLICT_MODAL }; }; export const hideConflictModal = () => { return { type: ActionTypes.REPLICATION_HIDE_CONFLICT_MODAL }; }; export const changeActivitySort = (sort) => { return { type: ActionTypes.REPLICATION_CHANGE_ACTIVITY_SORT, options: sort }; }; export const checkForNewApi = () => dispatch => { supportNewApi().then(newApi => { dispatch({ type: ActionTypes.REPLICATION_SUPPORT_NEW_API, options: newApi }); }); }; export const setPageLimit = (limit) => dispatch => { dispatch({ type: ActionTypes.REPLICATION_SET_PAGE_LIMIT, options: limit, }); getReplicationActivity(limit)(dispatch); };