diff --git a/assets/js/sync-ui/apps/sync.js b/assets/js/sync-ui/apps/sync.js index 7eeb66f239..06fb401b17 100644 --- a/assets/js/sync-ui/apps/sync.js +++ b/assets/js/sync-ui/apps/sync.js @@ -17,6 +17,7 @@ import Log from '../components/log'; import Objects from '../components/objects'; import Progress from '../components/progress'; import PutMapping from '../components/put-mapping'; +import SyncHistory from '../components/sync-history'; import { useSyncSettings } from '../provider'; /** @@ -26,7 +27,7 @@ import { useSyncSettings } from '../provider'; */ export default () => { const { createNotice } = useSettingsScreen(); - const { isComplete, isEpio, isSyncing, lastSyncDateTime, logMessage, startSync } = useSync(); + const { isComplete, isEpio, isSyncing, logMessage, startSync, syncHistory } = useSync(); const { autoIndex } = useSyncSettings(); /** @@ -56,7 +57,7 @@ export default () => { return ( <>

- {lastSyncDateTime + {syncHistory.length ? __( 'If you are missing data in your search results or have recently added custom content types to your site, you should run a sync to reflect these changes.', 'elasticpress', @@ -76,20 +77,25 @@ export default () => { {isSyncing || isComplete ? : null} - {lastSyncDateTime ? : null} + {syncHistory.length ? : null} - {lastSyncDateTime ? ( - - - - + {syncHistory.length ? ( + <> + + + + + + + + ) : null} diff --git a/assets/js/sync-ui/components/controls.js b/assets/js/sync-ui/components/controls.js index 6a57d86e45..fd5692d90f 100644 --- a/assets/js/sync-ui/components/controls.js +++ b/assets/js/sync-ui/components/controls.js @@ -20,12 +20,12 @@ export default () => { const { isPaused, isSyncing, - lastSyncDateTime, logMessage, pauseSync, resumeSync, startSync, stopSync, + syncHistory, } = useSync(); const { args } = useSyncSettings(); @@ -68,7 +68,7 @@ export default () => { const onSync = async () => { const { put_mapping } = args; - const putMapping = lastSyncDateTime ? put_mapping : true; + const putMapping = syncHistory.length ? put_mapping : true; const syncArgs = { ...args, put_mapping: putMapping }; startSync(syncArgs); diff --git a/assets/js/sync-ui/components/icons/error.js b/assets/js/sync-ui/components/icons/error.js new file mode 100644 index 0000000000..2d16608d88 --- /dev/null +++ b/assets/js/sync-ui/components/icons/error.js @@ -0,0 +1,15 @@ +import { SVG, Path } from '@wordpress/primitives'; + +export default () => { + return ( + + + + ); +}; diff --git a/assets/js/sync-ui/components/icons/success.js b/assets/js/sync-ui/components/icons/success.js new file mode 100644 index 0000000000..eeb679dc49 --- /dev/null +++ b/assets/js/sync-ui/components/icons/success.js @@ -0,0 +1,16 @@ +import { SVG, Path } from '@wordpress/primitives'; + +export default () => { + return ( + + + + + ); +}; diff --git a/assets/js/sync-ui/components/previous-sync.js b/assets/js/sync-ui/components/previous-sync.js new file mode 100644 index 0000000000..94c4031451 --- /dev/null +++ b/assets/js/sync-ui/components/previous-sync.js @@ -0,0 +1,109 @@ +/** + * External dependencies. + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies. + */ +import { Icon } from '@wordpress/components'; +import { useMemo, WPElement } from '@wordpress/element'; +import { dateI18n } from '@wordpress/date'; +import { __, _n, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies. + */ +import error from './icons/error'; +import success from './icons/success'; + +/** + * Delete checkbox component. + * + * @param {object} props Component props. + * @param {number} props.failures Number of failed items. + * @param {string} props.method Sync method. + * @param {string} props.stateDatetime Sync end date and time. + * @param {string} props.status Sync status. + * @param {string} props.trigger Sync trigger. + * @returns {WPElement} Sync page component. + */ +export default ({ failures, method, stateDatetime, status, trigger }) => { + /** + * When the sync was started. + */ + const when = useMemo(() => dateI18n('l F j, Y g:ia', stateDatetime), [stateDatetime]); + + /** + * How the sync completed. + */ + const how = useMemo(() => { + switch (status) { + case 'failed': + return __('Failed.', 'elasticpress'); + case 'with_errors': + return failures + ? sprintf( + _n( + 'Completed with %d error.', + 'Completed with %d errors.', + failures, + 'elasticpress', + ), + failures, + ) + : __('Completed with errors.', 'elasticpress'); + case 'aborted': + return __('Stopped.', 'elasticpress'); + case 'success': + return __('Completed successfully.', 'elasticpress'); + default: + return __('Completed.', 'elasticpress'); + } + }, [failures, status]); + + /** + * Why the sync was started. + */ + const why = useMemo(() => { + if (method === 'cli') { + return __('Manual sync from WP CLI.', 'elasticpress'); + } + + switch (trigger) { + case 'manual': + default: { + return __('Manual sync from Sync Settings.', 'elasticpress'); + } + } + }, [method, trigger]); + + /** + * Whether the sync has errors. + */ + const isError = useMemo(() => { + return status === 'failed' || status === 'with_errors' || status === 'aborted'; + }, [status]); + + /** + * Whether the sync was a success. + */ + const isSuccess = useMemo(() => { + return status === 'success'; + }, [status]); + + return ( +

+ +
+ {when} — {why} +
+
{how}
+
+ ); +}; diff --git a/assets/js/sync-ui/components/sync-history.js b/assets/js/sync-ui/components/sync-history.js new file mode 100644 index 0000000000..8c4288e690 --- /dev/null +++ b/assets/js/sync-ui/components/sync-history.js @@ -0,0 +1,37 @@ +/** + * WordPress dependencies. + */ +import { WPElement } from '@wordpress/element'; + +/** + * Internal dependencies. + */ +import { useSync } from '../../sync'; +import PreviousSync from './previous-sync'; + +/** + * Delete checkbox component. + * + * @returns {WPElement} Sync page component. + */ +export default () => { + const { syncHistory } = useSync(); + + return ( +
    + {syncHistory.map((s) => { + return ( +
  1. + +
  2. + ); + })} +
+ ); +}; diff --git a/assets/js/sync-ui/config.js b/assets/js/sync-ui/config.js index c5f1073593..e48e52aa7e 100644 --- a/assets/js/sync-ui/config.js +++ b/assets/js/sync-ui/config.js @@ -1,26 +1,7 @@ /** * Window dependencies. */ -const { - autoIndex, - apiUrl, - indexMeta, - indexables, - isEpio, - lastSyncDateTime, - lastSyncFailed, - nonce, - postTypes, -} = window.epDash; +const { autoIndex, apiUrl, indexMeta, indexables, isEpio, nonce, postTypes, syncHistory } = + window.epDash; -export { - autoIndex, - apiUrl, - indexables, - indexMeta, - isEpio, - lastSyncDateTime, - lastSyncFailed, - nonce, - postTypes, -}; +export { autoIndex, apiUrl, indexables, indexMeta, isEpio, nonce, postTypes, syncHistory }; diff --git a/assets/js/sync-ui/css/previous-sync.css b/assets/js/sync-ui/css/previous-sync.css new file mode 100644 index 0000000000..e02f7301ff --- /dev/null +++ b/assets/js/sync-ui/css/previous-sync.css @@ -0,0 +1,34 @@ +.ep-previous-sync { + align-items: center; + display: grid; + grid-column-gap: 8px; + grid-template-areas: + "icon title" + ". help"; + grid-template-columns: max-content auto; + justify-content: start; + + & svg { + display: block; + grid-area: icon; + } + + &.is-success svg { + fill: var(--ep-sync-color-success); + } + + &.is-error svg { + fill: var(--ep-sync-color-error); + } +} + +.ep-previous-sync__title { + grid-area: title; +} + +.ep-previous-sync__help { + color: rgb(117, 117, 117); + font-size: 12px; + font-style: normal; + grid-area: help; +} diff --git a/assets/js/sync-ui/css/sync-history.css b/assets/js/sync-ui/css/sync-history.css new file mode 100644 index 0000000000..7457f1209f --- /dev/null +++ b/assets/js/sync-ui/css/sync-history.css @@ -0,0 +1,11 @@ +.ep-sync-history { + display: flex; + flex-direction: column; + gap: 16px; + list-style: none; + margin-left: 0; + + & li { + margin: 0; + } +} diff --git a/assets/js/sync-ui/index.js b/assets/js/sync-ui/index.js index a88a2051c2..9389cb8b52 100644 --- a/assets/js/sync-ui/index.js +++ b/assets/js/sync-ui/index.js @@ -13,12 +13,11 @@ import { apiUrl, autoIndex, indexables, - lastSyncDateTime, - lastSyncFailed, indexMeta, isEpio, - postTypes, nonce, + postTypes, + syncHistory, } from './config'; import { SyncSettingsProvider } from './provider'; import Sync from './apps/sync'; @@ -36,8 +35,7 @@ import './style.css'; const App = () => ( { - updateState({ isPaused: false, isSyncing: false }); - cancelIndex(); - }, - [cancelIndex], - ); - const syncCompleted = useCallback( /** * Set sync state to completed, with success based on the number of @@ -165,8 +149,7 @@ export const SyncProvider = ({ isComplete: true, isPaused: false, isSyncing: false, - lastSyncDateTime: indexTotals.end_date_time, - lastSyncFailed: indexTotals.failed > 0, + syncHistory: [indexTotals, ...stateRef.current.syncHistory], }); /** @@ -200,6 +183,13 @@ export const SyncProvider = ({ logMessage(response.message, 'error'); } + /** + * If the error has totals, add to the sync history. + */ + const syncHistory = response.totals + ? [response.totals, ...stateRef.current.syncHistory] + : stateRef.current.syncHistory; + /** * Log a final message and update the sync state. */ @@ -208,8 +198,7 @@ export const SyncProvider = ({ updateState({ isFailed: true, isSyncing: false, - lastSyncDateTime: stateRef.current.syncStartDateTime, - lastSyncFailed: true, + syncHistory, }); }, [logMessage], @@ -265,6 +254,23 @@ export const SyncProvider = ({ [], ); + const syncStopped = useCallback( + /** + * Set state for a stopped sync. + * + * @param {object} response Cancel request response. + * @returns {void} + */ + (response) => { + const syncHistory = response.data + ? [response.data, ...stateRef.current.syncHistory] + : stateRef.current.syncHistory; + + updateState({ syncHistory }); + }, + [], + ); + const updateSyncState = useCallback( /** * Handle the response to a request to index. @@ -341,6 +347,18 @@ export const SyncProvider = ({ [syncCompleted, syncFailed, syncInProgress, syncInterrupted, logMessage], ); + const doCancelIndex = useCallback( + /** + * Cancel a sync. + * + * @returns {void} + */ + () => { + cancelIndex().then(syncStopped); + }, + [cancelIndex, syncStopped], + ); + const doIndexStatus = useCallback( /** * Check the status of a sync. @@ -420,8 +438,8 @@ export const SyncProvider = ({ * @returns {void} */ (args) => { - const { lastSyncDateTime } = stateRef.current; - const isInitialSync = lastSyncDateTime === null; + const { syncHistory } = stateRef.current; + const isInitialSync = !syncHistory.length; /** * We should not appear to be deleting if this is the first sync. @@ -442,6 +460,19 @@ export const SyncProvider = ({ [doIndex], ); + const stopSync = useCallback( + /** + * Stop syncing. + * + * @returns {void} + */ + () => { + updateState({ isPaused: false, isSyncing: false }); + doCancelIndex(); + }, + [doCancelIndex], + ); + /** * Initialize. * @@ -490,8 +521,7 @@ export const SyncProvider = ({ isSyncing, itemsProcessed, itemsTotal, - lastSyncDateTime, - lastSyncFailed, + syncHistory, syncStartDateTime, } = stateRef.current; @@ -506,8 +536,7 @@ export const SyncProvider = ({ isSyncing, itemsProcessed, itemsTotal, - lastSyncDateTime, - lastSyncFailed, + syncHistory, log, logMessage, pauseSync, diff --git a/includes/classes/REST/Sync.php b/includes/classes/REST/Sync.php index 8b9b398995..2b4c02c5af 100644 --- a/includes/classes/REST/Sync.php +++ b/includes/classes/REST/Sync.php @@ -208,7 +208,17 @@ public function cancel_sync( \WP_REST_Request $request ) { exit; } - IndexHelper::factory()->clear_index_meta(); + $index_meta = IndexHelper::factory()->get_index_meta(); + + if ( $index_meta ) { + IndexHelper::factory()->clear_index_meta(); + + wp_send_json_success( + IndexHelper::factory()->get_last_sync() + ); + + return; + } wp_send_json_success(); } diff --git a/includes/classes/Screen/Sync.php b/includes/classes/Screen/Sync.php index 481bf632d7..e14c786a4d 100644 --- a/includes/classes/Screen/Sync.php +++ b/includes/classes/Screen/Sync.php @@ -68,21 +68,20 @@ public function admin_enqueue_scripts() { $indices_comparison = Elasticsearch::factory()->get_indices_comparison(); $indices_missing = count( $indices_comparison['missing_indices'] ) > 0; - $last_sync = ! $indices_missing ? IndexHelper::factory()->get_last_sync() : []; - $post_types = Indexables::factory()->get( 'post' )->get_indexable_post_types(); $post_types = array_values( $post_types ); + $sync_history = ! $indices_missing ? IndexHelper::factory()->get_sync_history() : []; + $data = [ - 'apiUrl' => rest_url( 'elasticpress/v1/sync' ), - 'autoIndex' => isset( $_GET['do_sync'] ) && ( ! defined( 'EP_DASHBOARD_SYNC' ) || EP_DASHBOARD_SYNC ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended - 'indexMeta' => Utils\get_indexing_status(), - 'lastSyncDateTime' => ! empty( $last_sync['end_date_time'] ) ? $last_sync['end_date_time'] : null, - 'lastSyncFailed' => ! empty( $last_sync['failed'] ) || ! empty( $last_sync['errors'] ) ? true : false, - 'indexables' => array_map( fn( $indexable) => [ $indexable->slug, $indexable->labels['plural'] ], $indexables ), - 'isEpio' => Utils\is_epio(), - 'nonce' => wp_create_nonce( 'wp_rest' ), - 'postTypes' => array_map( fn( $post_type ) => [ $post_type, get_post_type_object( $post_type )->labels->name ], $post_types ), + 'apiUrl' => rest_url( 'elasticpress/v1/sync' ), + 'autoIndex' => isset( $_GET['do_sync'] ) && ( ! defined( 'EP_DASHBOARD_SYNC' ) || EP_DASHBOARD_SYNC ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended + 'indexMeta' => Utils\get_indexing_status(), + 'indexables' => array_map( fn( $indexable) => [ $indexable->slug, $indexable->labels['plural'] ], $indexables ), + 'isEpio' => Utils\is_epio(), + 'nonce' => wp_create_nonce( 'wp_rest' ), + 'postTypes' => array_map( fn( $post_type ) => [ $post_type, get_post_type_object( $post_type )->labels->name ], $post_types ), + 'syncHistory' => $sync_history, ]; wp_localize_script( 'ep_sync_scripts', 'epDash', $data );