PluginProbe
ElasticPress / 4.3.0
ElasticPress v4.3.0
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.3.0, at assets/js/sync/index.js

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