PluginProbe
ElasticPress / 4.7.1
ElasticPress v4.7.1
5.3.5 5.3.4 3.6.5 3.6.6 4.0.0 4.0.1 4.1.0 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.4.0 4.4.1 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.7.0 4.7.1 4.7.2 5.0.0 5.0.1 5.0.2 All 108 releases
elasticpress / assets / js / sync / index.js

index.js in ElasticPress 4.7.1, at assets/js/sync/index.js

544 lines 11.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * External dependencies.
3 */
4 import { v4 as uuid } from 'uuid';
5
6 /**
7 * WordPress dependencies.
8 */
9 import {
10 createRoot,
11 render,
12 useCallback,
13 useEffect,
14 useRef,
15 useState,
16 WPElement,
17 } from '@wordpress/element';
18 import { __, sprintf } from '@wordpress/i18n';
19
20 /**
21 * Internal dependencies.
22 */
23 import { autoIndex, lastSyncDateTime, lastSyncFailed, isEpio, indexMeta } from './config';
24 import { useIndex } from './hooks';
25 import {
26 clearSyncParam,
27 getItemsProcessedFromIndexMeta,
28 getItemsTotalFromIndexMeta,
29 } from './utilities';
30 import SyncPage from './components/sync-page';
31
32 /**
33 * App component.
34 *
35 * @returns {WPElement} App component.
36 */
37 const App = () => {
38 /**
39 * Indexing methods.
40 */
41 const { cancelIndex, index, indexStatus } = useIndex();
42
43 /**
44 * Message log state.
45 */
46 const [log, setLog] = useState([]);
47
48 /**
49 * Sync state.
50 */
51 const [state, setState] = useState({
52 isComplete: false,
53 isDeleting: false,
54 isFailed: false,
55 isSyncing: false,
56 itemsProcessed: 0,
57 itemsTotal: 100,
58 lastSyncDateTime,
59 lastSyncFailed,
60 syncStartDateTime: null,
61 });
62
63 /**
64 * Current state reference.
65 */
66 const stateRef = useRef(state);
67
68 /**
69 * Update state, and current state ref.
70 *
71 * @param {object} newState New state properties.
72 * @returns {void}
73 */
74 const updateState = (newState) => {
75 stateRef.current = { ...stateRef.current, ...newState };
76 setState((state) => ({ ...state, ...newState }));
77 };
78
79 const logMessage = useCallback(
80 /**
81 * Log a message.
82 *
83 * @param {Array|string} message Message/s to log.
84 * @param {string} status Message status.
85 * @returns {void}
86 */
87 (message, status) => {
88 const { isDeleting } = stateRef.current;
89
90 const messages = Array.isArray(message) ? message : [message];
91
92 for (const message of messages) {
93 setLog((log) => [...log, { message, status, isDeleting, id: uuid() }]);
94 }
95 },
96 [],
97 );
98
99 const stopSync = useCallback(
100 /**
101 * Stop syncing.
102 *
103 * @returns {void}
104 */
105 () => {
106 updateState({ isPaused: false, isSyncing: false });
107 cancelIndex();
108 },
109 [cancelIndex],
110 );
111
112 const syncCompleted = useCallback(
113 /**
114 * Set sync state to completed, with success based on the number of
115 * failures in the index totals.
116 *
117 * @param {object} indexTotals Index totals.
118 * @returns {void}
119 */
120 (indexTotals) => {
121 updateState({
122 isComplete: true,
123 isPaused: false,
124 isSyncing: false,
125 lastSyncDateTime: indexTotals.end_date_time,
126 lastSyncFailed: indexTotals.failed > 0,
127 });
128
129 /**
130 * Hide the "just need to sync" notice, if it's present.
131 */
132 document.querySelector('[data-ep-notice="no_sync"]')?.remove();
133 },
134 [],
135 );
136
137 const syncFailed = useCallback(
138 /**
139 * Handle an error in the sync request.
140 *
141 * @param {object|Error} response Request response.
142 * @returns {void}
143 */
144 (response) => {
145 /**
146 * Any running requests are cancelled when a new request is made.
147 * We can handle this silently.
148 */
149 if (response.name === 'AbortError') {
150 return;
151 }
152
153 /**
154 * Log any error messages.
155 */
156 if (response.message) {
157 logMessage(response.message, 'error');
158 }
159
160 /**
161 * Log a final message and update the sync state.
162 */
163 logMessage(__('Sync failed', 'elasticpress'), 'error');
164
165 updateState({
166 isFailed: true,
167 isSyncing: false,
168 lastSyncDateTime: stateRef.current.syncStartDateTime,
169 lastSyncFailed: true,
170 });
171 },
172 [logMessage],
173 );
174
175 const syncInterrupted = useCallback(
176 /**
177 * Set sync state to interrupted.
178 *
179 * Logs an appropriate message based on the sync method and
180 * Elasticsearch hosting.
181 *
182 * @returns {void}
183 */
184 () => {
185 const { isDeleting } = stateRef.current;
186
187 const message = isDeleting
188 ? sprintf(
189 /* translators: %s: Index type. ElasticPress.io or Elasticsearch. */
190 __(
191 '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.',
192 'elasticpress',
193 ),
194 isEpio
195 ? __('ElasticPress.io', 'elasticpress')
196 : __('Elasticsearch', 'elasticpress'),
197 )
198 : __('Sync interrupted by WP-CLI command.', 'elasticpress');
199
200 logMessage(message, 'info');
201 updateState({ isSyncing: false });
202 },
203 [logMessage],
204 );
205
206 const syncInProgress = useCallback(
207 /**
208 * Set state for a sync in progress from its index meta.
209 *
210 * @param {object} indexMeta Index meta.
211 * @returns {void}
212 */
213 (indexMeta) => {
214 const isInitialSync = stateRef.current.lastSyncDateTime === null;
215
216 /**
217 * We should not appear to be deleting if this is the first sync.
218 */
219 const isDeleting = isInitialSync ? false : indexMeta.put_mapping;
220
221 updateState({
222 isCli: indexMeta.method === 'cli',
223 isDeleting,
224 isSyncing: true,
225 itemsProcessed: getItemsProcessedFromIndexMeta(indexMeta),
226 itemsTotal: getItemsTotalFromIndexMeta(indexMeta),
227 syncStartDateTime: indexMeta.start_date_time,
228 });
229 },
230 [],
231 );
232
233 const updateSyncState = useCallback(
234 /**
235 * Handle the response to a request to index.
236 *
237 * Updates the application state from the response data and logs any
238 * messages. Returns a Promise that resolves if syncing should
239 * continue.
240 *
241 * @param {object} response AJAX response.
242 * @returns {Promise} Promise that resolves if sync is to continue.
243 */
244 (response) => {
245 const { isPaused, isSyncing } = stateRef.current;
246 const { message, status, totals = [], index_meta: indexMeta } = response.data;
247
248 return new Promise((resolve) => {
249 /**
250 * Don't continue if syncing has been stopped.
251 */
252 if (!isSyncing) {
253 return;
254 }
255
256 /**
257 * Stop sync if there is an error.
258 */
259 if (status === 'error') {
260 syncFailed(response.data);
261 return;
262 }
263
264 /**
265 * Log any messages.
266 */
267 if (message) {
268 logMessage(message, status);
269 }
270
271 /**
272 * If totals are available the index is complete.
273 */
274 if (!Array.isArray(totals)) {
275 syncCompleted(totals);
276 return;
277 }
278
279 /**
280 * Update sync progress from index meta.
281 */
282 syncInProgress(indexMeta);
283
284 /**
285 * Don't continue if the sync was interrupted externally.
286 */
287 if (indexMeta.should_interrupt_sync) {
288 syncInterrupted();
289 return;
290 }
291
292 /**
293 * Don't continue if syncing has been paused.
294 */
295 if (isPaused) {
296 logMessage(__('Sync paused', 'elasticpress'), 'info');
297 return;
298 }
299
300 /**
301 * Syncing should continue.
302 */
303 resolve(indexMeta.method);
304 });
305 },
306 [syncCompleted, syncFailed, syncInProgress, syncInterrupted, logMessage],
307 );
308
309 const doIndexStatus = useCallback(
310 /**
311 * Check the status of a sync.
312 *
313 * Used to get the status of an external sync already in progress, such
314 * as a WP CLI index.
315 *
316 * @returns {void}
317 */
318 () => {
319 indexStatus().then(updateSyncState).then(doIndexStatus).catch(syncFailed);
320 },
321 [indexStatus, syncFailed, updateSyncState],
322 );
323
324 const doIndex = useCallback(
325 /**
326 * Start or continue a sync.
327 *
328 * @param {boolean} putMapping Whether to send mapping.
329 * @returns {void}
330 */
331 (putMapping) => {
332 index(putMapping)
333 .then(updateSyncState)
334 .then(
335 /**
336 * If an existing sync has been found just check its status,
337 * otherwise continue syncing.
338 *
339 * @param {string} method Sync method.
340 */
341 (method) => {
342 if (method === 'cli') {
343 doIndexStatus();
344 } else {
345 doIndex(putMapping);
346 }
347 },
348 )
349 .catch(syncFailed);
350 },
351 [doIndexStatus, index, syncFailed, updateSyncState],
352 );
353
354 const pauseSync = useCallback(
355 /**
356 * Stop syncing.
357 *
358 * @returns {void}
359 */
360 () => {
361 updateState({ isPaused: true, isSyncing: true });
362 },
363 [],
364 );
365
366 const resumeSync = useCallback(
367 /**
368 * Resume syncing.
369 *
370 * @returns {void}
371 */
372 () => {
373 const { isDeleting, lastSyncDateTime } = stateRef.current;
374 const isInitialSync = lastSyncDateTime === null;
375
376 /**
377 * Send mapping if we are deleting and syncing or if this is the
378 * first sync.
379 */
380 const putMapping = isInitialSync || isDeleting;
381
382 updateState({ isPaused: false, isSyncing: true });
383 doIndex(putMapping);
384 },
385 [doIndex],
386 );
387
388 const startSync = useCallback(
389 /**
390 * Start syncing.
391 *
392 * @param {boolean} deleteAndSync Whether to delete and sync.
393 * @returns {void}
394 */
395 (deleteAndSync) => {
396 const { lastSyncDateTime } = stateRef.current;
397 const isInitialSync = lastSyncDateTime === null;
398
399 /**
400 * We should not appear to be deleting if this is the first sync.
401 */
402 const isDeleting = isInitialSync ? false : deleteAndSync;
403
404 /**
405 * Send mapping if we are deleting and syncing or if this is the
406 * first sync.
407 */
408 const putMapping = isInitialSync || deleteAndSync;
409
410 updateState({
411 isComplete: false,
412 isFailed: false,
413 isDeleting,
414 isPaused: false,
415 isSyncing: true,
416 });
417
418 updateState({ itemsProcessed: 0, syncStartDateTime: Date.now() });
419 doIndex(putMapping);
420 },
421 [doIndex],
422 );
423
424 /**
425 * Handle clicking delete and sync button.
426 *
427 * @returns {void}
428 */
429 const onDelete = async () => {
430 startSync(true);
431 logMessage(__('Starting delete and sync…', 'elasticpress'), 'info');
432 };
433
434 /**
435 * Handle clicking pause button.
436 *
437 * @returns {void}
438 */
439 const onPause = () => {
440 pauseSync();
441 logMessage(__('Pausing sync…', 'elasticpress'), 'info');
442 };
443
444 /**
445 * Handle clicking play button.
446 *
447 * @returns {void}
448 */
449 const onResume = () => {
450 resumeSync();
451 logMessage(__('Resuming sync…', 'elasticpress'), 'info');
452 };
453
454 /**
455 * Handle clicking stop button.
456 *
457 * @returns {void}
458 */
459 const onStop = () => {
460 stopSync();
461 logMessage(__('Sync stopped', 'elasticpress'), 'info');
462 };
463
464 /**
465 * Handle clicking sync button.
466 *
467 * @returns {void}
468 */
469 const onSync = async () => {
470 startSync(false);
471 logMessage(__('Starting sync…', 'elasticpress'), 'info');
472 };
473
474 /**
475 * Initialize.
476 *
477 * @returns {void}
478 */
479 const init = () => {
480 /**
481 * Clear sync parameter from the URL to prevent a refresh triggering a new
482 * sync.
483 */
484 clearSyncParam();
485
486 /**
487 * If a sync is in progress, update state to reflect its progress.
488 */
489 if (indexMeta) {
490 syncInProgress(indexMeta);
491
492 /**
493 * If the sync is a CLI sync, start getting its status.
494 */
495 if (indexMeta.method === 'cli') {
496 doIndexStatus();
497 logMessage(__('WP CLI sync in progress', 'elasticpress'), 'info');
498 } else {
499 pauseSync();
500 logMessage(__('Sync paused', 'elasticpress'), 'info');
501 }
502
503 return;
504 }
505
506 /**
507 * Start an initial index.
508 */
509 if (autoIndex) {
510 startSync(true);
511 logMessage(__('Starting delete and sync…', 'elasticpress'), 'info');
512 }
513 };
514
515 /**
516 * Effects.
517 */
518 useEffect(init, [doIndexStatus, syncInProgress, logMessage, pauseSync, startSync]);
519
520 /**
521 * Render.
522 */
523 return (
524 <SyncPage
525 isEpio={isEpio}
526 log={log}
527 onDelete={onDelete}
528 onPause={onPause}
529 onResume={onResume}
530 onStop={onStop}
531 onSync={onSync}
532 {...state}
533 />
534 );
535 };
536
537 if (typeof createRoot === 'function') {
538 const root = createRoot(document.getElementById('ep-sync'));
539
540 root.render(<App />);
541 } else {
542 render(<App />, document.getElementById('ep-sync'));
543 }
544