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 (
+ -
+
+
+ );
+ })}
+
+ );
+};
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 );