/** * External dependencies. */ import { v4 as uuid } from 'uuid'; /** * WordPress dependencies. */ import { render, useCallback, useEffect, useRef, useState, WPElement } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies. */ import { autoIndex, lastSyncDateTime, lastSyncFailed, isEpio, indexMeta } from './config'; import { useIndex } from './hooks'; import { clearSyncParam, getItemsProcessedFromIndexMeta, getItemsTotalFromIndexMeta, } from './utilities'; import SyncPage from './components/sync-page'; /** * App component. * * @returns {WPElement} App component. */ const App = () => { /** * Indexing methods. */ const { cancelIndex, index, indexStatus } = useIndex(); /** * Message log state. */ const [log, setLog] = useState([]); /** * Sync state. */ const [state, setState] = useState({ isComplete: false, isDeleting: false, isSyncing: false, itemsProcessed: 0, itemsTotal: 100, lastSyncDateTime, lastSyncFailed, syncStartDateTime: null, }); /** * Current state reference. */ const stateRef = useRef(state); /** * Update state, and current state ref. * * @param {object} newState New state properties. * @returns {void} */ const updateState = (newState) => { stateRef.current = { ...stateRef.current, ...newState }; setState((state) => ({ ...state, ...newState })); }; const logMessage = useCallback( /** * Log a message. * * @param {Array|string} message Message/s to log. * @param {string} status Message status. * @returns {void} */ (message, status) => { const { isDeleting } = stateRef.current; const messages = Array.isArray(message) ? message : [message]; for (const message of messages) { setLog((log) => [...log, { message, status, isDeleting, id: uuid() }]); } }, [], ); const syncCompleted = useCallback( /** * Set sync state to completed, with success based on the number of * failures in the index totals. * * @param {object} indexTotals Index totals. * @returns {void} */ (indexTotals) => { updateState({ isComplete: true, isPaused: false, isSyncing: false, lastSyncDateTime: indexTotals.end_date_time, lastSyncFailed: indexTotals.failed > 0, }); }, [], ); const syncFailed = useCallback( /** * Handle an error in the sync request. * * @param {Error} error Request error. * @returns {void} */ (error) => { /** * Any running requests are cancelled when a new request is made. * We can handle this silently. */ if (error.name === 'AbortError') { return; } /** * Log any messages. */ if (error.message) { logMessage(error.message, 'error'); } logMessage(__('Sync failed', 'elasticpress'), 'error'); updateState({ isSyncing: false }); }, [logMessage], ); const syncInterrupted = useCallback( /** * Set sync state to interrupted. * * Logs an appropriate message based on the sync method and * Elasticsearch hosting. * * @returns {void} */ () => { const { isDeleting } = stateRef.current; const message = isDeleting ? sprintf( /* translators: %s: Index type. ElasticPress.io or Elasticsearch. */ __( 'Your indexing process has been stopped by WP-CLI and your %s index could be missing content. To restart indexing, please click the Start button or use WP-CLI commands to perform the reindex. Please note that search results could be incorrect or incomplete until the reindex finishes.', 'elasticpress', ), isEpio ? __('ElasticPress.io', 'elasticpress') : __('Elasticsearch', 'elasticpress'), ) : __('Sync interrupted by WP-CLI command.', 'elasticpress'); logMessage(message, 'info'); updateState({ isSyncing: false }); }, [logMessage], ); const syncInProgress = useCallback( /** * Set state for a sync in progress from its index meta. * * @param {object} indexMeta Index meta. * @returns {void} */ (indexMeta) => { updateState({ isCli: indexMeta.method === 'cli', isComplete: false, isDeleting: indexMeta.put_mapping, isSyncing: true, itemsProcessed: getItemsProcessedFromIndexMeta(indexMeta), itemsTotal: getItemsTotalFromIndexMeta(indexMeta), syncStartDateTime: indexMeta.start_date_time, }); }, [], ); const updateSyncState = useCallback( /** * Handle the response to a request to index. * * Updates the application state from the response data and logs any * messages. Returns a Promise that resolves if syncing should * continue. * * @param {object} response AJAX response. * @returns {Promise} Promise that resolves if sync is to continue. */ (response) => { const { isPaused, isSyncing } = stateRef.current; const { message, status, totals = [], index_meta: indexMeta } = response.data; return new Promise((resolve) => { /** * Don't continue if syncing has been stopped. */ if (!isSyncing) { return; } /** * Log any messages. */ if (message) { logMessage(message, status); } /** * If totals are available the index is complete. */ if (!Array.isArray(totals)) { syncCompleted(totals); return; } /** * Update sync progress from index meta. */ syncInProgress(indexMeta); /** * Don't continue if the sync was interrupted externally. */ if (indexMeta.should_interrupt_sync) { syncInterrupted(); return; } /** * Don't continue if syncing has been paused. */ if (isPaused) { logMessage(__('Sync paused', 'elasticpress'), 'info'); return; } /** * Syncing should continue. */ resolve(indexMeta.method); }); }, [syncCompleted, syncInProgress, syncInterrupted, logMessage], ); const doIndexStatus = useCallback( /** * Check the status of a sync. * * Used to get the status of an external sync already in progress, such * as a WP CLI index. * * @returns {void} */ () => { indexStatus().then(updateSyncState).then(doIndexStatus).catch(syncFailed); }, [indexStatus, syncFailed, updateSyncState], ); const doIndex = useCallback( /** * Start or continues a sync. * * @param {boolean} isDeleting Whether to delete and sync. * @returns {void} */ (isDeleting) => { index(isDeleting) .then(updateSyncState) .then( /** * If an existing sync has been found just check its status, * otherwise continue syncing. * * @param {string} method Sync method. */ (method) => { if (method === 'cli') { doIndexStatus(); } else { doIndex(isDeleting); } }, ) .catch(syncFailed); }, [doIndexStatus, index, syncFailed, updateSyncState], ); const pauseSync = useCallback( /** * Stop syncing. * * @returns {void} */ () => { updateState({ isComplete: false, isPaused: true, isSyncing: true }); }, [], ); const stopSync = useCallback( /** * Stop syncing. * * @returns {void} */ () => { updateState({ isComplete: false, isPaused: false, isSyncing: false }); cancelIndex(); }, [cancelIndex], ); const resumeSync = useCallback( /** * Resume syncing. * * @returns {void} */ () => { updateState({ isComplete: false, isPaused: false, isSyncing: true }); doIndex(stateRef.current.isDeleting); }, [doIndex], ); const startSync = useCallback( /** * Stop syncing. * * @param {boolean} isDeleting Whether to delete and sync. * @returns {void} */ (isDeleting) => { updateState({ isComplete: false, isDeleting, isPaused: false, isSyncing: true }); updateState({ itemsProcessed: 0, syncStartDateTime: Date.now() }); doIndex(isDeleting); }, [doIndex], ); /** * Handle clicking delete and sync button. * * @returns {void} */ const onDelete = async () => { startSync(true); logMessage(__('Starting delete and sync…', 'elasticpress'), 'info'); }; /** * Handle clicking pause button. * * @returns {void} */ const onPause = () => { pauseSync(); logMessage(__('Pausing sync…', 'elasticpress'), 'info'); }; /** * Handle clicking play button. * * @returns {void} */ const onResume = () => { resumeSync(); logMessage(__('Resuming sync…', 'elasticpress'), 'info'); }; /** * Handle clicking stop button. * * @returns {void} */ const onStop = () => { stopSync(); logMessage(__('Sync stopped', 'elasticpress'), 'info'); }; /** * Handle clicking sync button. * * @returns {void} */ const onSync = async () => { startSync(false); logMessage(__('Starting sync…', 'elasticpress'), 'info'); }; /** * Initialize. * * @returns {void} */ const init = () => { /** * Clear sync parameter from the URL to prevent a refresh triggering a new * sync. */ clearSyncParam(); /** * If a sync is in progress, update state to reflect its progress. */ if (indexMeta) { syncInProgress(indexMeta); /** * If the sync is a CLI sync, start getting its status. */ if (indexMeta.method === 'cli') { doIndexStatus(); logMessage(__('WP CLI sync in progress', 'elasticpress'), 'info'); } else { pauseSync(); logMessage(__('Sync paused', 'elasticpress'), 'info'); } return; } /** * Start an initial index. */ if (autoIndex) { startSync(true); logMessage(__('Starting delete and sync…', 'elasticpress'), 'info'); } }; /** * Effects. */ useEffect(init, [doIndexStatus, syncInProgress, logMessage, pauseSync, startSync]); /** * Render. */ return ( ); }; render(, document.getElementById('ep-sync'));