diff --git a/assets/css/sync.css b/assets/css/sync.css index f6cc9cd892..3d513296a1 100644 --- a/assets/css/sync.css +++ b/assets/css/sync.css @@ -1,334 +1,18 @@ -:root { - --ep-admin-color-base-white: #fff; - --ep-admin-color-blue-01: #00a0d2; - --ep-admin-color-grey-01: #f1f1f1; - --ep-admin-color-green-01: #46b450; - --ep-admin-color-red-01: #b52727; - --ep-admin-color-dark-01: #333; - --ep-admin-max-width: 1200px; - - --ep-admin-box-title: #1d2327; - - --ep-admin-delete-sync-button-bg-color: rgba(181, 39, 39, 0.03); - - --ep-admin-progress-bar-bg-color: rgba(0, 160, 210, 0.3); - - --ep-admin-output-tab-color: #1e1e1e; - - --ep-admin-log-bg-color: #1a1e24; - --ep-admin-log-line-number-color: #999; - --ep-admin-log-line-number-bg-color: #303030; - -} - -.elasticpress_page_elasticpress-sync .button:disabled { - cursor: not-allowed; -} - -.ep-sync-box__progress-wrapper { - display: none; -} - -.ep-sync-box__output { - background-color: var(--ep-admin-log-bg-color); - display: none; - margin-bottom: 20px; - max-height: 200px; - overflow-y: scroll; - position: relative; -} - -.ep-sync-box__output_active { - display: block; -} - -.ep-sync-box__output-wrapper { - color: var(--ep-admin-color-base-white); - margin-left: 30px; - min-height: 200px; -} - -.ep-sync-box__output-line { - position: relative; -} - -.ep-sync-box__output-line-number { - background-color: var(--ep-admin-log-line-number-bg-color); - color: var(--ep-admin-log-line-number-color); - left: -30px; - min-width: 20px; - padding: 0 3px 0 5px; - position: absolute; - text-align: right; - white-space: nowrap; -} - -.ep-sync-box__output-line-text { - font-size: 12px; - padding-left: 14px; -} - -.ep-sync-box__output-tabs { - align-items: center; - display: flex; -} - -.ep-sync-box__output-tabs_hide { - display: none; -} - -.ep-sync-box__output-tab { - color: var(--ep-admin-output-tab-color); - padding: 16px; -} - -.ep-sync-box__output-tab:hover { - cursor: pointer; -} - -.ep-sync-box__output-tab_active { - border-bottom: 4px solid var(--ep-admin-color-blue-01); - padding-bottom: 12px; -} - -.ep-sync-box__output-tab_active:hover { - cursor: default; -} - -.ep-sync-box__button-text { - height: 21px; -} - -.elasticpress_page_elasticpress-sync .card { - max-width: var(--ep-admin-max-width); - - & .ep-sync-box__description-actions { - display: flex; - flex-direction: column; - - @media (min-width: 768px) { - flex-direction: row; - justify-content: space-between; - } - } - - & .ep-sync-box__description { +@import "sync/button.css"; +@import "sync/controls.css"; +@import "sync/heading.css"; +@import "sync/messages.css"; +@import "sync/panel.css"; +@import "sync/progress.css"; +@import "sync/progress-bar.css"; +@import "sync/status.css"; +@import "sync/warning.css"; - @media (min-width: 768px) { - width: 69%; - } - } - - & .ep-sync-box__action { - align-items: center; - display: flex; - flex-direction: column; - margin-top: 40px; - - @media (min-width: 768px) { - width: 30%; - } - - & .ep-sync-box__button { - align-items: center; - display: flex; - font-size: 24px; - font-weight: 700; - height: 68px; - justify-content: space-between; - padding: 20px 40px; - width: 228px; - } - - & .ep-sync-box__icon-button { - font-size: 28px; - height: 28px; - width: 28px; - - } - - & .ep-sync-box__learn-more-link { - margin-top: 19px; - } - - } - - & .ep-sync-box__description_text { - font-size: 18px; - } - - & .ep-last-sync { - margin-bottom: 12px; - } - - & .ep-last-sync__title { - font-size: 20px; - font-weight: 700; - margin-bottom: 0.5em; - } - - & .ep-last-sync__icon-status { - margin-right: 5px; - vertical-align: text-bottom; - } - - & .ep-last-sync__date { - background-color: var(--ep-admin-color-grey-01); - padding: 6px; - } - - & .ep-sync-box__buttons { - display: flex; - - } - - & .ep-sync-box__button-resume, - & .ep-sync-box__button-pause, - & .ep-sync-box__button-stop { - align-items: center; - display: none; - flex-direction: column; - height: 68px; - justify-content: center; - width: 100px; - } - - & .ep-sync-box__button-stop { - background-color: var(--ep-admin-color-blue-01); - border-color: var(--ep-admin-color-blue-01); - margin-left: 24px; - } - - & .ep-sync-box__progress { - align-items: normal; - display: flex; - flex-direction: column; - margin-bottom: 5px; - margin-top: 19px; - - @media (min-width: 768px) { - align-items: center; - flex-direction: row; - } - } - - & .ep-sync-box__sync-in-progress { - display: flex; - flex-direction: row; - - @media (min-width: 768px) { - width: 30%; - } - } - - & .ep-sync-box__progressbar { - background-color: var(--ep-admin-color-grey-01); - border-radius: 24px; - height: 20px; - margin: 15px 0; - position: relative; - width: 100%; - - @media (min-width: 768px) { - margin: 0; - width: 65%; - } - } - - & .ep-sync-box__progressbar_animated { - background-color: var(--ep-admin-progress-bar-bg-color); - color: var(--ep-admin-color-dark-01); - display: block; - height: 100%; - margin: 0; - text-align: center; - transition: width 0.5s ease-in-out; - width: 0; - } - - & .ep-sync-box__progressbar_complete { - background-color: var(--ep-admin-color-green-01); - color: var(--ep-admin-color-base-white); - } - - & .ep-sync-box__sync-in-progress-info { - display: flex; - flex-direction: column; - margin-left: 12px; - } - - & .ep-sync-box__progress-info { - font-size: 14px; - font-weight: 500; - margin-bottom: 5px; - } - - & .ep-sync-box__start-time { - color: var(--ep-admin-color-blue-01); - font-size: 14px; - } - - & .ep-sync-box__start-time-date { - color: var(--ep-admin-color-dark-01); - } -} - -.elasticpress_page_elasticpress-sync .ep-delete-data-and-sync { - margin-top: 40px; - - & .card { - margin-top: 17px; - } - - & .ep-delete-data-and-sync__title { - color: var(--ep-admin-box-title); - font-size: 18px; - font-weight: 400; - } - - & .ep-delete-data-and-sync__warning { - align-items: flex-start; - display: flex; - - @media (min-width: 768px) { - width: 69%; - } - } - - & .ep-delete-data-and-sync__warning-icon { - margin-right: 9px; - margin-top: 17px; - } - - & .ep-delete-data-and-sync__button { - background-color: var(--ep-admin-delete-sync-button-bg-color); - border: 1px solid var(--ep-admin-color-red-01); - color: var(--ep-admin-color-red-01); - margin: 5px 0 12px; - } - - & .ep-delete-data-and-sync__button-cancel { - display: none; - } - - & .ep-sync-box__action { - flex-direction: column; - height: auto; - justify-content: center; - margin-top: 0; - width: 100%; - - @media (min-width: 768px) { - flex-direction: row; - height: 68px; - justify-content: space-between; - } - } - - & .ep-sync-box__buttons { - - @media (min-width: 768px) { - margin-right: 5%; - } - } +:root { + --ep-sync-color-black: #1a1e24; + --ep-sync-color-error: #b52727; + --ep-sync-color-light-grey: #f0f0f0; + --ep-sync-color-success: #46b450; + --ep-sync-color-warning: #ffb359; + --ep-sync-color-white: #fff; } diff --git a/assets/css/sync/button.css b/assets/css/sync/button.css new file mode 100644 index 0000000000..784c320788 --- /dev/null +++ b/assets/css/sync/button.css @@ -0,0 +1,25 @@ +.ep-sync-button { + + &.components-button.has-icon.has-text { + height: 4rem; + justify-content: center; + width: 100%; + + & svg { + height: 2em; + margin: 0; + width: 2em; + } + } +} + +.ep-sync-button--sync { + font-size: 1.5em; + font-weight: 700; +} + +.ep-sync-button--pause, +.ep-sync-button--resume, +.ep-sync-button--stop { + flex-direction: column; +} diff --git a/assets/css/sync/controls.css b/assets/css/sync/controls.css new file mode 100644 index 0000000000..2a9b63eb1e --- /dev/null +++ b/assets/css/sync/controls.css @@ -0,0 +1,16 @@ +.ep-sync-controls { + display: grid; + grid-gap: 1em; + grid-template-columns: 1fr 1fr; + margin: 0 auto; + max-width: 16rem; +} + +.ep-sync-controls__sync { + grid-column: 1 / -1; +} + +.ep-sync-controls__learn-more { + grid-column: 1 / -1; + text-align: center; +} diff --git a/assets/css/sync/heading.css b/assets/css/sync/heading.css new file mode 100644 index 0000000000..6a3ceb84a5 --- /dev/null +++ b/assets/css/sync/heading.css @@ -0,0 +1,12 @@ +.ep-sync-heading { + + @nest .wrap & { + font-weight: 400; + margin: 0.5rem 0 0.75rem; + padding: 0; + + &h2 { + color: inherit; + } + } +} diff --git a/assets/css/sync/messages.css b/assets/css/sync/messages.css new file mode 100644 index 0000000000..58233f4d4d --- /dev/null +++ b/assets/css/sync/messages.css @@ -0,0 +1,25 @@ +.ep-sync-messages { + align-content: start; + background: var(--ep-sync-color-black); + color: var(--ep-sync-color-white); + display: grid; + font-family: monospace; + grid-auto-flow: column; + grid-template-columns: min-content auto; + height: 21em; + line-height: 2; + overflow-y: auto; + white-space: pre-wrap; +} + +.ep-sync-messages__message { + grid-column: 2; +} + +.ep-sync-messages__line-number { + box-sizing: content-box; + min-width: 3ch; + opacity: 0.5; + padding: 0 0.5em; + text-align: right; +} diff --git a/assets/css/sync/panel.css b/assets/css/sync/panel.css new file mode 100644 index 0000000000..88340dc2ea --- /dev/null +++ b/assets/css/sync/panel.css @@ -0,0 +1,34 @@ +.ep-sync-panel { + margin-bottom: 2rem; + max-width: 1200px; +} + +.ep-sync-panel__body { + display: grid; + grid-column-gap: 2rem; + grid-row-gap: 1rem; + grid-template-columns: auto 16rem; + + &.is-opened { + padding: 2rem 2rem 1rem 2rem; + } + + & p, + & .components-toggle-control, + & .components-tab-panel__tab-content { + margin-bottom: 1rem; + margin-top: 0; + } + + @media (max-width: 960px) { + grid-template-columns: 100%; + } +} + +.ep-sync-panel__row { + grid-column: 1 / -1; +} + +.ep-sync-panel__introduction { + font-size: 18px; +} diff --git a/assets/css/sync/progress-bar.css b/assets/css/sync/progress-bar.css new file mode 100644 index 0000000000..c694e6a087 --- /dev/null +++ b/assets/css/sync/progress-bar.css @@ -0,0 +1,27 @@ +.ep-sync-progress-bar { + background: var(--ep-sync-color-light-grey); + display: flex; + overflow: hidden; + text-align: center; +} + +.ep-sync-progress-bar, +.ep-sync-progress-bar__progress { + border-radius: 0.875em; +} + +.ep-sync-progress-bar__progress { + background: var(--wp-admin-theme-color); + color: var(--ep-sync-color-white); + padding: 0 0.875em; + transition: all 500ms ease-in-out; + white-space: nowrap; + + @nest .ep-sync-progress-bar--complete & { + background: var(--ep-sync-color-success); + } + + @nest .ep-sync-progress-bar--paused & { + opacity: 0.5; + } +} diff --git a/assets/css/sync/progress.css b/assets/css/sync/progress.css new file mode 100644 index 0000000000..e80a2533a0 --- /dev/null +++ b/assets/css/sync/progress.css @@ -0,0 +1,52 @@ +@keyframes epSyncRotation { + + from { + transform: rotate(0deg); + } + + to { + transform: rotate(359deg); + } +} + +.ep-sync-progress { + align-items: center; + display: grid; + grid-row-gap: 1rem; + grid-template-columns: min-content minmax(max-content, 1fr) 3fr; + margin-bottom: 1rem; + + @media (max-width: 960px) { + grid-template-columns: min-content auto; + } + + & svg { + animation: epSyncRotation 1500ms infinite linear; + animation-play-state: paused; + height: 36px; + margin-right: 12px; + width: 36px; + } +} + +.ep-sync-progress--syncing { + + & svg { + animation-play-state: running; + } +} + +.ep-sync-progress__details { + + & strong { + display: block; + font-size: 14px; + } +} + +.ep-sync-progress__progress-bar { + + @media (max-width: 960px) { + grid-column: 1 / -1; + } +} diff --git a/assets/css/sync/status.css b/assets/css/sync/status.css new file mode 100644 index 0000000000..178a1cc270 --- /dev/null +++ b/assets/css/sync/status.css @@ -0,0 +1,23 @@ +.ep-sync-status { + align-items: center; + display: grid; + grid-gap: 0.5rem; + grid-template-columns: min-content auto; + + & svg { + fill: var(--ep-sync-color-error); + } +} + +.ep-sync-status--success { + + & svg { + fill: var(--ep-sync-color-success); + } +} + +.ep-sync-status__time { + background-color: var(--ep-sync-color-light-grey); + border-radius: 2px; + padding: 0.25em 0.5em; +} diff --git a/assets/css/sync/warning.css b/assets/css/sync/warning.css new file mode 100644 index 0000000000..a96cbbe375 --- /dev/null +++ b/assets/css/sync/warning.css @@ -0,0 +1,10 @@ +.ep-sync-warning { + display: grid; + grid-gap: 0.5rem; + grid-template-columns: min-content auto; + + & svg { + fill: var(--ep-sync-color-warning); + margin-top: -3px; + } +} diff --git a/assets/js/sync.js b/assets/js/sync.js deleted file mode 100644 index 62da139d92..0000000000 --- a/assets/js/sync.js +++ /dev/null @@ -1,855 +0,0 @@ -import apiFetch from '@wordpress/api-fetch'; -import { dateI18n } from '@wordpress/date'; - -/* eslint-disable camelcase, no-use-before-define */ -const { epDash, history } = window; -const { __, sprintf } = wp.i18n; - -const { ajax_url: ajaxurl = '', is_epio } = epDash; - -// Main elements of sync page -const syncBox = document.querySelector('.ep-sync-data'); -const deleteAndSyncBox = document.querySelector('.ep-delete-data-and-sync'); - -// It could be the syncBox or deleteAndSyncBox -let activeBox; - -// Buttons to start a sync or delete data -const syncButton = syncBox.querySelector('.ep-sync-box__button-sync'); -const deleteAndSyncButton = deleteAndSyncBox.querySelector( - '.ep-delete-data-and-sync__button-delete', -); - -// Log elements -const syncBoxFulllogTab = document.querySelector('.ep-sync-data .ep-sync-box__output-tab-fulllog'); -const syncBoxOutputFulllog = document.querySelector('.ep-sync-data .ep-sync-box__output-fulllog'); -const syncBoxErrorTab = document.querySelector('.ep-sync-data .ep-sync-box__output-tab-error'); -const syncBoxOutputError = document.querySelector('.ep-sync-data .ep-sync-box__output-error'); - -const deleteBoxFulllogTab = document.querySelector( - '.ep-delete-data-and-sync .ep-sync-box__output-tab-fulllog', -); -const deleteBoxErrorTab = document.querySelector( - '.ep-delete-data-and-sync .ep-sync-box__output-tab-error', -); -const deleteBoxOutputFulllog = document.querySelector( - '.ep-delete-data-and-sync .ep-sync-box__output-fulllog', -); -const deleteBoxOutputError = document.querySelector( - '.ep-delete-data-and-sync .ep-sync-box__output-error', -); - -syncButton.addEventListener('click', function () { - activeBox = syncBox; - - disableButtonsInDeleteBox(); - - syncButton.style.display = 'none'; - updateDisabledAttribute(syncButton, true); - - const learnMoreLink = activeBox.querySelector('.ep-sync-box__learn-more-link'); - learnMoreLink.style.display = 'none'; - - showPauseStopButtons(); - showProgress(); - addLineToOutput(__('Indexing data…', 'elasticpress')); - - const progressInfoElement = activeBox.querySelector('.ep-sync-box__progress-info'); - const progressBar = activeBox.querySelector('.ep-sync-box__progressbar_animated'); - const startDateTime = activeBox.querySelector('.ep-sync-box__start-time-date'); - - progressInfoElement.innerText = __('Sync in progress', 'elasticpress'); - - progressBar.style.width = `0`; - progressBar.innerText = ``; - - startDateTime.innerText = ''; - - startSyncProcess(); -}); - -deleteAndSyncButton.addEventListener('click', deleteAndSync); - -function deleteAndSync() { - activeBox = deleteAndSyncBox; - - disableButtonsInSyncBox(); - updateDisabledAttribute(deleteAndSyncButton, true); - showPauseStopButtons(); - showProgress(); - - addLineToOutput(__('Deleting data…', 'elasticpress')); - - const progressInfoElement = activeBox.querySelector('.ep-sync-box__progress-info'); - const progressBar = activeBox.querySelector('.ep-sync-box__progressbar_animated'); - const startDateTime = activeBox.querySelector('.ep-sync-box__start-time-date'); - - progressInfoElement.innerText = __('Deleting in progress', 'elasticpress'); - - progressBar.style.width = `0`; - progressBar.innerText = ``; - - startDateTime.innerText = ''; - - startSyncProcess(true); -} - -/** - * Show Pause and Stop buttons on the active box - */ -function showPauseStopButtons() { - if (activeBox) { - showStopButton(); - showPauseButton(); - } -} - -/** - * Hide Pause and Stop buttons on the active box - */ -function hidePauseStopButtons() { - hideStopButton(); - hidePauseButton(); -} - -/** - * Show Pause button on the active box - */ -function showPauseButton() { - if (activeBox) { - const pauseButton = activeBox.querySelector('.ep-sync-box__button-pause'); - - updateDisabledAttribute(pauseButton, false); - - pauseButton.style.display = 'flex'; - } -} - -/** - * Hide Pause button on the active box - */ -function hidePauseButton() { - if (activeBox) { - const pauseButton = activeBox.querySelector('.ep-sync-box__button-pause'); - - pauseButton.style.display = 'none'; - } -} - -/** - * Show Resume button on the active box - */ -function showResumeButton() { - if (activeBox) { - const resumeButton = activeBox.querySelector('.ep-sync-box__button-resume'); - - updateDisabledAttribute(resumeButton, false); - - resumeButton.style.display = 'flex'; - } -} - -/** - * Hide Pause button on the active box - */ -function hideResumeButton() { - if (activeBox) { - const resumeButton = activeBox.querySelector('.ep-sync-box__button-resume'); - - resumeButton.style.display = 'none'; - } -} - -/** - * Show Stop button on the active box - */ -function showStopButton() { - if (activeBox) { - const stopButton = activeBox.querySelector('.ep-sync-box__button-stop'); - - updateDisabledAttribute(stopButton, false); - - stopButton.style.display = 'flex'; - } -} - -/** - * Hide Stop button on the active box - */ -function hideStopButton() { - if (activeBox) { - const stopButton = activeBox.querySelector('.ep-sync-box__button-stop'); - - stopButton.style.display = 'none'; - } -} - -function showProgress() { - const progressWrapper = activeBox?.querySelector('.ep-sync-box__progress-wrapper'); - - if (progressWrapper?.style) { - progressWrapper.style.display = 'block'; - } -} - -let syncStatus = 'sync'; -let syncStack; -let processed = 0; -let toProcess = 0; -let totalProcessed = 0; - -updateLastSyncDateTime(epDash?.ep_last_sync_date); - -if (epDash.index_meta) { - if (epDash.index_meta.method === 'cli') { - syncStatus = 'wpcli'; - processed = epDash?.index_meta?.items_indexed; - toProcess = epDash?.index_meta?.total_items; - - activeBox = epDash.index_meta.put_mapping ? deleteAndSyncBox : syncBox; - - const progressInfoElement = activeBox.querySelector('.ep-sync-box__progress-info'); - - progressInfoElement.innerText = __('WP-CLI sync in progress', 'elasticpress'); - - updateStartDateTime(epDash?.index_meta?.start_date_time); - - updateDisabledAttribute(syncButton, true); - updateDisabledAttribute(deleteAndSyncButton, true); - - showProgress(); - - updateSyncDash(); - cliSync(); - } else { - processed = epDash.index_meta.offset; - toProcess = epDash.index_meta.found_items; - - if (epDash.index_meta.sync_stack) { - syncStack = epDash.index_meta.sync_stack; - } - - if ((!syncStack || !syncStack.length) && toProcess === 0 && !epDash.index_meta.start) { - // Sync finished - syncStatus = 'finished'; - } else { - syncStatus = 'pause'; - } - activeBox = epDash.index_meta?.put_mapping ? deleteAndSyncBox : syncBox; - - disableButtonsInSyncBox(); - disableButtonsInDeleteBox(); - - if (activeBox === syncBox) { - syncButton.style.display = 'none'; - - const learnMoreLink = activeBox.querySelector('.ep-sync-box__learn-more-link'); - - learnMoreLink.style.display = 'none'; - } - - showResumeButton(); - showStopButton(); - - showProgress(); - - updateSyncDash(); - } -} else if (epDash.auto_start_index) { - deleteAndSync(); - - history.pushState( - {}, - document.title, - document.location.pathname + document.location.search.replace(/&do_sync/, ''), - ); -} - -/** - * Change the disabled attribute of an element - * - * @param {HTMLElement} element Element to be updated - * @param {boolean} value The value used in disabled attribute - */ -function updateDisabledAttribute(element, value) { - element.disabled = value; -} - -/** - * Update dashboard with syncing information - */ -function updateSyncDash() { - const progressBar = activeBox.querySelector('.ep-sync-box__progressbar_animated'); - - const isSyncing = ['initialsync', 'sync', 'pause', 'wpcli'].includes(syncStatus); - - let progressBarWidth; - if (isSyncing) { - progressBarWidth = - toProcess === 0 ? 0 : (parseInt(processed, 10) / parseInt(toProcess, 10)) * 100; - } else { - progressBarWidth = 100; - } - - if ( - typeof progressBarWidth === 'number' && - !Number.isNaN(progressBarWidth) && - Number.isFinite(progressBarWidth) - ) { - const width = Math.min(100, progressBarWidth); - progressBar.style.width = `${width}%`; - progressBar.innerText = `${Math.trunc(width)}%`; - } - - if (isSyncing) { - progressBar.classList.remove('ep-sync-box__progressbar_complete'); - } else if (syncStatus === 'error') { - const progressInfoElement = activeBox.querySelector('.ep-sync-box__progress-info'); - - progressInfoElement.innerText = __('Sync failed', 'elasticpress'); - - updateStartDateTime(new Date()); - updateDisabledAttribute(deleteAndSyncButton, false); - updateDisabledAttribute(syncButton, false); - - hidePauseStopButtons(); - hideResumeButton(); - - syncButton.style.display = 'flex'; - - const learnMoreLink = activeBox.querySelector('.ep-sync-box__learn-more-link'); - - if (learnMoreLink?.style) { - learnMoreLink.style.display = 'block'; - } - } else if (syncStatus === 'interrupt') { - const progressInfoElement = activeBox.querySelector('.ep-sync-box__progress-info'); - - progressInfoElement.innerText = __('Sync interrupted', 'elasticpress'); - - updateDisabledAttribute(deleteAndSyncButton, false); - updateDisabledAttribute(syncButton, false); - - hidePauseStopButtons(); - hideResumeButton(); - - syncButton.style.display = 'flex'; - - const learnMoreLink = activeBox.querySelector('.ep-sync-box__learn-more-link'); - - if (learnMoreLink?.style) { - learnMoreLink.style.display = 'block'; - } - } else { - const progressInfoElement = activeBox.querySelector('.ep-sync-box__progress-info'); - - progressInfoElement.innerText = __('Sync completed', 'elasticpress'); - - progressBar.classList.add('ep-sync-box__progressbar_complete'); - - updateDisabledAttribute(deleteAndSyncButton, false); - updateDisabledAttribute(syncButton, false); - - hidePauseStopButtons(); - hideResumeButton(); - - syncButton.style.display = 'flex'; - - const learnMoreLink = activeBox.querySelector('.ep-sync-box__learn-more-link'); - - if (learnMoreLink?.style) { - learnMoreLink.style.display = 'block'; - } - } -} - -/** - * Cancel a sync - */ -function cancelSync() { - toProcess = 0; - processed = 0; - totalProcessed = 0; - - apiFetch({ - url: ajaxurl, - method: 'POST', - body: new URLSearchParams({ - action: 'ep_cancel_index', - nonce: epDash.nonce, - }), - }); -} - -function cliSync() { - const requestSettings = { - url: ajaxurl, - method: 'POST', - body: new URLSearchParams({ - action: 'ep_cli_index', - nonce: epDash.nonce, - }), - }; - - apiFetch(requestSettings).then((response) => { - if (syncStatus === 'interrupt') { - return; - } - - if (syncStatus === 'wpcli') { - toProcess = response.data?.index_meta?.total_items; - processed = response.data?.index_meta?.items_indexed; - - if (response.data.index_meta?.current_sync_item?.failed) { - const message = response.data?.message; - if (Array.isArray(message)) { - message.forEach((item) => { - addErrorToOutput(item); - addLineToOutput(item); - }); - } else if (typeof message === 'string') { - addErrorToOutput(message); - addLineToOutput(message); - } - } else { - addLineToOutput(response.data.message); - } - - updateSyncDash(); - - if (response.data?.index_meta?.indexing) { - cliSync(); - return; - } - } - - syncStatus = 'finished'; - addLineToOutput('==============================='); - addLineToOutput(__('WP-CLI sync is finished', 'elasticpress')); - updateSyncDash(); - }); -} - -/** - * Add a line to the active output - * - * @param {string} text Message to show on output - */ -function addLineToOutput(text) { - if (activeBox && text) { - const wrapperElement = activeBox.querySelector('.ep-sync-box__output-wrapper'); - - const lastLineNumberElement = activeBox.querySelector( - '.ep-sync-box__output-line:last-child .ep-sync-box__output-line-number', - ); - const lastLineNumber = Number(lastLineNumberElement?.innerText); - - const lineNumber = document.createElement('div'); - lineNumber.className = 'ep-sync-box__output-line-number'; - lineNumber.innerText = - typeof lastLineNumber === 'number' && !Number.isNaN(lastLineNumber) - ? lastLineNumber + 1 - : 1; - - const lineText = document.createElement('div'); - lineText.className = 'ep-sync-box__output-line-text'; - lineText.innerText = text; - - const line = document.createElement('div'); - line.className = 'ep-sync-box__output-line'; - line.append(lineNumber); - line.append(lineText); - - wrapperElement.append(line); - - const outputElement = activeBox.querySelector('.ep-sync-box__output_active'); - outputElement.scrollTo(0, wrapperElement.scrollHeight); - } -} - -function addErrorToOutput(text) { - if (activeBox) { - const wrapperElement = activeBox.querySelector( - '.ep-sync-box__output-error .ep-sync-box__output-wrapper', - ); - - const lastLineNumberElement = activeBox.querySelector( - '.ep-sync-box__output-error .ep-sync-box__output-line:last-child .ep-sync-box__output-line-number', - ); - const lastLineNumber = Number(lastLineNumberElement?.innerText); - - const lineNumber = document.createElement('div'); - lineNumber.className = 'ep-sync-box__output-line-number'; - lineNumber.innerText = - typeof lastLineNumber === 'number' && !Number.isNaN(lastLineNumber) - ? lastLineNumber + 1 - : 1; - - const lineText = document.createElement('div'); - lineText.className = 'ep-sync-box__output-line-text'; - lineText.innerText = text; - - const line = document.createElement('div'); - line.className = 'ep-sync-box__output-line'; - line.append(lineNumber); - line.append(lineText); - - wrapperElement.append(line); - - const errorTab = activeBox.querySelector('.ep-sync-box__output-tab-error'); - - errorTab.innerText = sprintf( - // translators: Number of errors - __('Errors (%d)', 'elasticpress'), - lineNumber.innerText, - ); - - const outputElement = activeBox.querySelector('.ep-sync-box__output-error'); - outputElement.scrollTo(0, wrapperElement.scrollHeight); - } -} - -/** - * Update the start datetime on active box - * - * @param {Date | string} dateValue The datetime value - */ -function updateStartDateTime(dateValue) { - if (dateValue) { - const startDateTime = activeBox.querySelector('.ep-sync-box__start-time-date'); - - if (startDateTime) { - startDateTime.innerText = dateI18n( - // translators: index start date format, see https://wordpress.org/support/article/formatting-date-and-time/ - __('D, F d, Y H:i', 'elasticpress'), - dateValue, - ); - } - } -} - -/** - * Update the last sync datetime - * - * @param {Date | string} dateValue Date object or string, parsable by moment.js. - */ -function updateLastSyncDateTime(dateValue) { - if (dateValue) { - const lastSyncDate = document.querySelector('.ep-last-sync__date'); - - if (lastSyncDate) { - lastSyncDate.innerText = dateI18n( - // translators: last sync datetime format, see https://wordpress.org/support/article/formatting-date-and-time/ - __('D, F d, Y H:i', 'elasticpress'), - dateValue, - ); - } - } -} - -/** - * Check if a destructive index is running - * - * @returns {boolean} Wheter or not is a destructive index - */ -function isDestructiveIndex() { - return activeBox === deleteAndSyncBox; -} - -/** - * Interrupt the sync process - * - * @param {boolean} value True to interrupt the sync process - */ -function shouldInterruptSync(value) { - if (!value) { - return; - } - - syncStatus = 'interrupt'; - - let logMessage = __('Sync interrupted by WP-CLI command', 'elasticpress'); - if (isDestructiveIndex()) { - logMessage = sprintf( - // translators: 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', - ), - is_epio ? 'ElasticPress.io' : 'Elasticsearch', - ); - } - stopIndex(__('Sync interrupted', 'elasticpress'), logMessage); -} - -/** - * Perform an elasticpress sync - * - * @param {boolean} putMapping Whetever mapping should be sent or not. - */ -function sync(putMapping = false) { - const requestSettings = { - url: ajaxurl, - method: 'POST', - body: new URLSearchParams({ - action: 'ep_index', - put_mapping: putMapping ? 1 : 0, - nonce: epDash.nonce, - }), - }; - - apiFetch(requestSettings) - .then((response) => { - if (response.data.index_meta?.current_sync_item?.failed) { - const message = response.data?.message; - if (Array.isArray(message)) { - message.forEach((item) => { - addErrorToOutput(item); - addLineToOutput(item); - }); - } else if (typeof message === 'string') { - addErrorToOutput(message); - addLineToOutput(message); - } - } else { - addLineToOutput(response.data.message); - } - updateStartDateTime(response?.data?.index_meta?.start_date_time); - shouldInterruptSync(response.data?.index_meta?.should_interrupt_sync); - - if (response.data?.method === 'cli') { - syncStatus = 'wpcli'; - cliSync(); - return; - } - - if (syncStatus !== 'sync') { - return; - } - - if (!response.data.index_meta) { - syncStatus = 'finished'; - - const lastSyncStatusIcon = document.querySelector('.ep-last-sync__icon-status'); - const lastSyncStatus = document.querySelector('.ep-last-sync__status'); - - lastSyncStatusIcon.src = response.data.totals.failed - ? lastSyncStatusIcon.src?.replace(/thumbsup/, 'thumbsdown') - : lastSyncStatusIcon.src?.replace(/thumbsdown/, 'thumbsup'); - lastSyncStatus.innerText = response.data.totals.failed - ? __('Sync unsuccessful on ', 'elasticpress') - : __('Sync success on ', 'elasticpress'); - - updateLastSyncDateTime(response.data?.totals?.end_date_time); - - updateSyncDash(); - - addLineToOutput('==============================='); - - if (epDash.install_sync) { - document.location.replace(epDash.install_complete_url); - } - - activeBox = undefined; - - processed = 0; - toProcess = 0; - totalProcessed = 0; - - return; - } - - if (!toProcess) { - toProcess = response.data?.index_meta?.current_sync_item?.found_items; - - if (response.data?.index_meta?.sync_stack) { - syncStack = response.data.index_meta.sync_stack; - - toProcess = syncStack?.reduce((previousValue, currentSync) => { - return previousValue + currentSync.found_items; - }, toProcess); - } - } - - if (response.data.index_meta.offset === 0 && processed > 0) { - totalProcessed = processed; - } - - processed = totalProcessed + response.data.index_meta.offset; - - updateSyncDash(); - sync(putMapping); - }) - .catch((response) => { - if (response && response.code === 'invalid_json') { - syncStatus = 'error'; - updateSyncDash(); - cancelSync(); - addErrorToOutput(response.message); - } - - if ( - response && - response.status && - parseInt(response.status, 10) >= 400 && - parseInt(response.status, 10) < 600 - ) { - syncStatus = 'error'; - updateSyncDash(); - - cancelSync(); - } - - updateDisabledAttribute(syncButton, false); - updateDisabledAttribute(deleteAndSyncButton, false); - }); -} - -/** - * Start sync process - * - * @param {boolean} putMapping Determines whether to send the mapping and delete all data before sync. - */ -function startSyncProcess(putMapping) { - syncStatus = 'initialsync'; - - const progressWrapperElement = activeBox.querySelector('.ep-sync-box__progress-wrapper'); - const progressInfoElement = activeBox.querySelector('.ep-sync-box__progress-info'); - const progressBar = activeBox.querySelector('.ep-sync-box__progressbar_animated'); - const startDateTime = activeBox.querySelector('.ep-sync-box__start-time-date'); - - progressWrapperElement.style.display = 'block'; - progressInfoElement.innerText = __('Sync in progress', 'elasticpress'); - - progressBar.style.width = `0`; - progressBar.innerText = ``; - - startDateTime.innerText = ''; - - updateSyncDash(); - - syncStatus = 'sync'; - - sync(putMapping); -} - -/** - * Disable buttons in the Sync box - */ -function disableButtonsInSyncBox() { - const buttons = syncBox.querySelectorAll('.ep-sync-data button'); - - buttons.forEach((button) => updateDisabledAttribute(button, true)); -} - -/** - * Disable buttons in the Delete box - */ -function disableButtonsInDeleteBox() { - const buttons = deleteAndSyncBox.querySelectorAll('.ep-delete-data-and-sync button'); - - buttons.forEach((button) => updateDisabledAttribute(button, true)); -} - -document.querySelectorAll('.ep-sync-box__button-pause')?.forEach((button) => { - button?.addEventListener('click', function () { - syncStatus = 'pause'; - - const progressInfoElement = activeBox?.querySelector('.ep-sync-box__progress-info'); - - if (progressInfoElement?.innerText) { - progressInfoElement.innerText = __('Sync paused', 'elasticpress'); - } - - updateSyncDash(); - - hidePauseButton(); - showResumeButton(); - - addLineToOutput(__('Sync paused', 'elasticpress')); - }); -}); - -document.querySelectorAll('.ep-sync-box__button-resume')?.forEach((button) => { - button?.addEventListener('click', function () { - syncStatus = 'sync'; - - const progressInfoElement = activeBox.querySelector('.ep-sync-box__progress-info'); - - progressInfoElement.innerText = __('Sync in progress', 'elasticpress'); - - updateSyncDash(); - - hideResumeButton(); - showPauseButton(); - - sync(); - }); -}); - -function stopIndex(syncMessage, logMessage) { - syncStatus = syncStatus === 'wpcli' ? 'interrupt' : 'cancel'; - - const progressInfoElement = activeBox.querySelector('.ep-sync-box__progress-info'); - const progressBar = activeBox.querySelector('.ep-sync-box__progressbar_animated'); - - updateSyncDash(); - - cancelSync(); - - progressInfoElement.innerText = syncMessage; - - progressBar.style.width = `0`; - progressBar.innerText = ``; - - addLineToOutput(logMessage); -} -document.querySelectorAll('.ep-sync-box__button-stop')?.forEach((button) => { - button?.addEventListener('click', () => { - stopIndex(__('Sync stopped', 'elasticpress'), __('Sync stopped', 'elasticpress')); - }); -}); - -document.querySelectorAll('.ep-sync-box__show-hide-log')?.forEach((element) => { - element.addEventListener('click', function (event) { - event.preventDefault(); - - if (element.nextElementSibling?.classList?.toggle('ep-sync-box__output-tabs_hide')) { - element.innerText = __('Show log', 'elasticpress'); - } else { - element.innerText = __('Hide log', 'elasticpress'); - } - }); -}); - -syncBoxFulllogTab.addEventListener('click', function () { - syncBoxFulllogTab.classList.add('ep-sync-box__output-tab_active'); - syncBoxOutputFulllog.classList.add('ep-sync-box__output_active'); - - syncBoxErrorTab.classList.remove('ep-sync-box__output-tab_active'); - syncBoxOutputError.classList.remove('ep-sync-box__output_active'); -}); - -syncBoxErrorTab.addEventListener('click', function () { - syncBoxErrorTab.classList.add('ep-sync-box__output-tab_active'); - syncBoxOutputError.classList.add('ep-sync-box__output_active'); - - syncBoxFulllogTab.classList.remove('ep-sync-box__output-tab_active'); - syncBoxOutputFulllog.classList.remove('ep-sync-box__output_active'); -}); - -deleteBoxFulllogTab.addEventListener('click', function () { - deleteBoxFulllogTab.classList.add('ep-sync-box__output-tab_active'); - deleteBoxOutputFulllog.classList.add('ep-sync-box__output_active'); - - deleteBoxErrorTab.classList.remove('ep-sync-box__output-tab_active'); - deleteBoxOutputError.classList.remove('ep-sync-box__output_active'); -}); - -deleteBoxErrorTab.addEventListener('click', function () { - deleteBoxErrorTab.classList.add('ep-sync-box__output-tab_active'); - deleteBoxOutputError.classList.add('ep-sync-box__output_active'); - - deleteBoxFulllogTab.classList.remove('ep-sync-box__output-tab_active'); - deleteBoxOutputFulllog.classList.remove('ep-sync-box__output_active'); -}); diff --git a/assets/js/sync/components/common/date-time.js b/assets/js/sync/components/common/date-time.js new file mode 100644 index 0000000000..2c2f7342e0 --- /dev/null +++ b/assets/js/sync/components/common/date-time.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies. + */ +import { dateI18n } from '@wordpress/date'; +import { WPElement } from '@wordpress/element'; + +/** + * Log component. + * + * @param {object} props Component props. + * @param {string} props.dateTime Date and time. + * @returns {WPElement} Component. + */ +export default ({ dateTime, ...props }) => { + return ( + + ); +}; diff --git a/assets/js/sync/components/common/message-log.js b/assets/js/sync/components/common/message-log.js new file mode 100644 index 0000000000..f555197d95 --- /dev/null +++ b/assets/js/sync/components/common/message-log.js @@ -0,0 +1,35 @@ +/** + * WordPress dependencies. + */ +import { WPElement } from '@wordpress/element'; + +/** + * Log component. + * + * @param {object} props Component props. + * @param {object[]} props.messages Log messages. + * @returns {WPElement} Component. + */ +export default ({ messages }) => { + return ( +
+ {messages.map((m, i) => ( +
+ {i + 1} +
+ ))} + {messages.map((m) => ( +
+ {m.message} +
+ ))} +
+ ); +}; diff --git a/assets/js/sync/components/common/progress-bar.js b/assets/js/sync/components/common/progress-bar.js new file mode 100644 index 0000000000..8bd4ce7d2c --- /dev/null +++ b/assets/js/sync/components/common/progress-bar.js @@ -0,0 +1,32 @@ +/** + * WordPress dependencies. + */ +import { WPElement } from '@wordpress/element'; + +/** + * Progress bar component. + * + * @param {object} props Component props. + * @param {number} props.current Current value. + * @param {number} props.total Current total. + * @param {boolean} props.isComplete If operation is complete. + * @returns {WPElement} Component. + */ +export default ({ isComplete, current, total }) => { + const now = Math.floor((current / total) * 100); + + return ( +
+
{`${now}%`}
+
+ ); +}; diff --git a/assets/js/sync/components/icons/pause.js b/assets/js/sync/components/icons/pause.js new file mode 100644 index 0000000000..64c45c5334 --- /dev/null +++ b/assets/js/sync/components/icons/pause.js @@ -0,0 +1,15 @@ +import { SVG, Path } from '@wordpress/primitives'; + +export default () => { + return ( + + + + ); +}; diff --git a/assets/js/sync/components/icons/play.js b/assets/js/sync/components/icons/play.js new file mode 100644 index 0000000000..9052734fd3 --- /dev/null +++ b/assets/js/sync/components/icons/play.js @@ -0,0 +1,15 @@ +import { SVG, Path } from '@wordpress/primitives'; + +export default () => { + return ( + + + + ); +}; diff --git a/assets/js/sync/components/icons/stop.js b/assets/js/sync/components/icons/stop.js new file mode 100644 index 0000000000..027bd0998b --- /dev/null +++ b/assets/js/sync/components/icons/stop.js @@ -0,0 +1,15 @@ +import { SVG, Path } from '@wordpress/primitives'; + +export default () => { + return ( + + + + ); +}; diff --git a/assets/js/sync/components/icons/sync.js b/assets/js/sync/components/icons/sync.js new file mode 100644 index 0000000000..c8e23d3b8a --- /dev/null +++ b/assets/js/sync/components/icons/sync.js @@ -0,0 +1,15 @@ +import { SVG, Path } from '@wordpress/primitives'; + +export default () => { + return ( + + + + ); +}; diff --git a/assets/js/sync/components/icons/thumbs-down.js b/assets/js/sync/components/icons/thumbs-down.js new file mode 100644 index 0000000000..7677253ea0 --- /dev/null +++ b/assets/js/sync/components/icons/thumbs-down.js @@ -0,0 +1,15 @@ +import { SVG, Path } from '@wordpress/primitives'; + +export default () => { + return ( + + + + ); +}; diff --git a/assets/js/sync/components/icons/thumbs-up.js b/assets/js/sync/components/icons/thumbs-up.js new file mode 100644 index 0000000000..3e76177d50 --- /dev/null +++ b/assets/js/sync/components/icons/thumbs-up.js @@ -0,0 +1,15 @@ +import { SVG, Path } from '@wordpress/primitives'; + +export default () => { + return ( + + + + ); +}; diff --git a/assets/js/sync/components/sync-page.js b/assets/js/sync/components/sync-page.js new file mode 100644 index 0000000000..2d19077dbd --- /dev/null +++ b/assets/js/sync/components/sync-page.js @@ -0,0 +1,182 @@ +/** + * WordPress dependencies. + */ +import { Button, Icon, Panel, PanelBody } from '@wordpress/components'; +import { WPElement } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { warning } from '@wordpress/icons'; + +/** + * Internal dependencies. + */ +import SyncControls from './sync/controls'; +import SyncLog from './sync/log'; +import SyncProgress from './sync/progress'; +import SyncStatus from './sync/status'; + +/** + * Sync page component. + * + * @param {object} props Component props. + * @param {boolean} props.isCli If sync is a CLI sync. + * @param {boolean} props.isComplete If sync is complete. + * @param {boolean} props.isDeleting If sync is a delete and sync. + * @param {boolean} props.isPaused If sync is paused. + * @param {boolean} props.isSyncing If sync is running. + * @param {number} props.itemsProcessed Number of items processed. + * @param {number} props.itemsTotal Number of items to process. + * @param {string} props.lastSyncDateTime Date and time of last sync in ISO-8601. + * @param {boolean} props.lastSyncFailed If the last sync had failures. + * @param {object[]} props.log Sync message log. + * @param {Function} props.onDelete Callback for clicking delete and sync. + * @param {Function} props.onPause Callback for clicking pause. + * @param {Function} props.onResume Callback for clicking resume. + * @param {Function} props.onStop Callback for clicking stop. + * @param {Function} props.onSync Callback for clicking sync. + * @param {string} props.syncStartDateTime Date and time of current sync in ISO 8601. + * @returns {WPElement} Sync page component. + */ +export default ({ + isCli, + isComplete, + isDeleting, + isPaused, + isSyncing, + itemsProcessed, + itemsTotal, + lastSyncDateTime, + lastSyncFailed, + log, + onDelete, + onPause, + onResume, + onStop, + onSync, + syncStartDateTime, +}) => { + return ( + <> +

{__('Sync Settings', 'elasticpress')}

+ + + +
+

+ {__( + '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', + )} +

+ + {lastSyncDateTime ? ( + <> +

+ {__('Last Sync', 'elasticpress')} +

+ + + ) : null} +
+ +
+ +
+ + {!isDeleting && (isSyncing || isComplete) ? ( +
+ +
+ ) : null} + +
+ !m.isDeleting)} /> +
+
+
+ +

{__('Delete All Data and Sync', 'elasticpress')}

+ + + +
+

+ {__( + 'If you are still having issues with your search results, you may need to do a completely fresh sync.', + 'elasticpress', + )} +

+ +

+ +

+
+ +
+ +
+ + {isDeleting && (isSyncing || isComplete) ? ( +
+ +
+ ) : null} + +
+ m.isDeleting)} /> +
+ +
+

+ + {__( + 'All indexed data on ElasticPress will be deleted without affecting anything on your WordPress website. This may take a few hours depending on the amount of content that needs to be synced and indexed. While this is happenening, searches will use the default WordPress results', + 'elasticpress', + )} +

+
+
+
+ + ); +}; diff --git a/assets/js/sync/components/sync/controls.js b/assets/js/sync/components/sync/controls.js new file mode 100644 index 0000000000..d32817447e --- /dev/null +++ b/assets/js/sync/components/sync/controls.js @@ -0,0 +1,102 @@ +/** + * WordPress dependencies. + */ +import { Button } from '@wordpress/components'; +import { WPElement } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { update } from '@wordpress/icons'; + +/** + * Internal dependencies. + */ +import pause from '../icons/pause'; +import play from '../icons/play'; +import stop from '../icons/stop'; + +/** + * Sync button component. + * + * @param {object} props Component props. + * @param {boolean} props.disabled If controls are disabled. + * @param {boolean} props.isPaused If syncing is paused. + * @param {boolean} props.isSyncing If syncing is in progress. + * @param {Function} props.onPause Pause button click callback. + * @param {Function} props.onResume Play button click callback. + * @param {Function} props.onStop Stop button click callback. + * @param {Function} props.onSync Sync button click callback. + * @param {boolean} props.showSync If sync button is shown. + * @returns {WPElement} Component. + */ +export default ({ disabled, isPaused, isSyncing, onPause, onResume, onStop, onSync, showSync }) => { + /** + * Render. + */ + return ( +
+ {showSync && !isSyncing ? ( +
+ +
+ ) : null} + + {isSyncing ? ( + <> +
+ {isPaused ? ( + + ) : ( + + )} +
+ +
+ +
+ + ) : null} + + {showSync ? ( +
+ +
+ ) : null} +
+ ); +}; diff --git a/assets/js/sync/components/sync/log.js b/assets/js/sync/components/sync/log.js new file mode 100644 index 0000000000..c9e1c2921a --- /dev/null +++ b/assets/js/sync/components/sync/log.js @@ -0,0 +1,72 @@ +/** + * WordPress dependencies. + */ +import { TabPanel, ToggleControl } from '@wordpress/components'; +import { useState, WPElement } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies. + */ +import MessageLog from '../common/message-log'; + +/** + * Sync logs component. + * + * @param {object} props Component props. + * @param {object[]} props.messages Log messages. + * @returns {WPElement} Component. + */ +export default ({ messages }) => { + const [isOpen, setIsOpen] = useState(false); + + /** + * Messages with the error status. + */ + const errorMessages = messages.filter((m) => m.status === 'error' || m.status === 'warning'); + + /** + * Log tabs. + */ + const tabs = [ + { + messages, + name: 'full', + title: __('Full Log', 'elasticpress'), + }, + { + messages: errorMessages, + name: 'error', + title: sprintf( + /* translators: %d: Error message count. */ + __('Errors (%d)', 'elasticpress'), + errorMessages.length, + ), + }, + ]; + + /** + * Handle clicking show log button. + * + * @param {boolean} checked If toggle is checked. + * @returns {void} + */ + const onToggle = (checked) => { + setIsOpen(checked); + }; + + return ( + <> + + {isOpen ? ( + + {({ messages }) => } + + ) : null} + + ); +}; diff --git a/assets/js/sync/components/sync/progress.js b/assets/js/sync/components/sync/progress.js new file mode 100644 index 0000000000..f6e98fe9cf --- /dev/null +++ b/assets/js/sync/components/sync/progress.js @@ -0,0 +1,79 @@ +/** + * WordPress dependencies. + */ +import { Icon } from '@wordpress/components'; +import { useMemo, WPElement } from '@wordpress/element'; +import { dateI18n } from '@wordpress/date'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies. + */ +import DateTime from '../common/date-time'; +import ProgressBar from '../common/progress-bar'; +import sync from '../icons/sync'; + +/** + * Sync button component. + * + * @param {object} props Component props. + * @param {boolean} props.isCli If progress is for a CLI sync. + * @param {boolean} props.isComplete If sync is complete. + * @param {boolean} props.isPaused If sync is paused. + * @param {number} props.itemsProcessed Number of items processed. + * @param {number} props.itemsTotal Total number of items. + * @param {string} props.dateTime Start date and time. + * @returns {WPElement} Component. + */ +export default ({ isCli, isComplete, isPaused, itemsProcessed, itemsTotal, dateTime }) => { + /** + * Sync progress label. + */ + const label = useMemo( + /** + * Determine appropriate sync status label. + * + * @returns {string} Sync progress label. + */ + () => { + if (isComplete) { + return __('Sync complete', 'elasticpress'); + } + + if (isPaused) { + return __('Sync paused', 'elasticpress'); + } + + if (isCli) { + return __('WP CLI sync in progress', 'elasticpress'); + } + + return __('Sync in progress', 'elasticpress'); + }, + [isCli, isComplete, isPaused], + ); + + return ( +
+ + +
+ {label} + {dateTime ? ( + <> + {__('Started on', 'elasticpress')}{' '} + + + ) : null} +
+ +
+ +
+
+ ); +}; diff --git a/assets/js/sync/components/sync/status.js b/assets/js/sync/components/sync/status.js new file mode 100644 index 0000000000..4ac77d6839 --- /dev/null +++ b/assets/js/sync/components/sync/status.js @@ -0,0 +1,40 @@ +/** + * WordPress dependencies. + */ +import { Icon } from '@wordpress/components'; +import { dateI18n } from '@wordpress/date'; +import { WPElement } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies. + */ +import DateTime from '../common/date-time'; +import thumbsDown from '../icons/thumbs-down'; +import thumbsUp from '../icons/thumbs-up'; + +/** + * Sync button component. + * + * @param {object} props Component props. + * @param {string} props.dateTime Sync date and time. + * @param {boolean} props.isSuccess If sync was a success. + * @returns {WPElement} Component. + */ +export default ({ dateTime, isSuccess }) => { + return ( +

+ + + {isSuccess + ? __('Sync success on', 'elasticpress') + : __('Sync unsuccessful on', 'elasticpress')}{' '} + + +

+ ); +}; diff --git a/assets/js/sync/config.js b/assets/js/sync/config.js new file mode 100644 index 0000000000..b667ebdfd0 --- /dev/null +++ b/assets/js/sync/config.js @@ -0,0 +1,14 @@ +/** + * Window dependencies. + */ +const { + auto_start_index: autoIndex, + ajax_url: ajaxUrl, + index_meta: indexMeta = null, + is_epio: isEpio, + ep_last_sync_date: lastSyncDateTime = null, + ep_last_sync_failed: lastSyncFailed = false, + nonce, +} = window.epDash; + +export { autoIndex, ajaxUrl, indexMeta, isEpio, lastSyncDateTime, lastSyncFailed, nonce }; diff --git a/assets/js/sync/hooks.js b/assets/js/sync/hooks.js new file mode 100644 index 0000000000..84f4651b36 --- /dev/null +++ b/assets/js/sync/hooks.js @@ -0,0 +1,131 @@ +/** + * WordPress dependencies. + */ +import apiFetch from '@wordpress/api-fetch'; +import { useCallback, useRef } from '@wordpress/element'; + +/** + * Internal dependencies. + */ +import { ajaxUrl, nonce } from './config'; + +/** + * Indexing hook. + * + * Provides methods for indexing, getting indexing status, and cancelling + * indexing. Methods share an abort controller so that requests can + * interrupt eachother to avoid multiple sync requests causing race conditions + * or duplicate output, such as by rapidly pausing and unpausing indexing. + * + * @returns {object} Sync, sync status, and cancel functions. + */ +export const useIndex = () => { + const abort = useRef(new AbortController()); + const request = useRef(null); + + const sendRequest = useCallback( + /** + * Send AJAX request. + * + * Silently catches abort errors and clears the current request on + * completion. + * + * @param {object} options Request options. + * @throws {Error} Any non-abort errors. + * @returns {Promise} Current request promise. + */ + (options) => { + request.current = apiFetch(options).finally(() => { + request.current = null; + }); + + return request.current; + }, + [], + ); + + const cancelIndex = useCallback( + /** + * Send a request to cancel sync. + * + * @returns {Promise} Fetch request promise. + */ + async () => { + abort.current.abort(); + abort.current = new AbortController(); + + const body = new FormData(); + + body.append('action', 'ep_cancel_index'); + body.append('nonce', nonce); + + const options = { + url: ajaxUrl, + method: 'POST', + body, + signal: abort.current.signal, + }; + + return sendRequest(options); + }, + [sendRequest], + ); + + const index = useCallback( + /** + * Send a request to sync. + * + * @param {boolean} putMapping Whether to put mapping. + * @returns {Promise} Fetch request promise. + */ + async (putMapping) => { + abort.current.abort(); + abort.current = new AbortController(); + + const body = new FormData(); + + body.append('action', 'ep_index'); + body.append('put_mapping', putMapping ? 1 : 0); + body.append('nonce', nonce); + + const options = { + url: ajaxUrl, + method: 'POST', + body, + signal: abort.current.signal, + }; + + return sendRequest(options); + }, + [sendRequest], + ); + + const indexStatus = useCallback( + /** + * Send a request for CLI sync status. + * + * @returns {Promise} Fetch request promise. + */ + async () => { + abort.current.abort(); + abort.current = new AbortController(); + + const body = new FormData(); + + body.append('action', 'ep_index_status'); + body.append('nonce', nonce); + + const options = { + url: ajaxUrl, + method: 'POST', + body, + signal: abort.current.signal, + }; + + return sendRequest(options); + }, + [sendRequest], + ); + + return { cancelIndex, index, indexStatus }; +}; diff --git a/assets/js/sync/index.js b/assets/js/sync/index.js new file mode 100644 index 0000000000..194bb2bb48 --- /dev/null +++ b/assets/js/sync/index.js @@ -0,0 +1,469 @@ +/** + * 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')); diff --git a/assets/js/sync/utilities.js b/assets/js/sync/utilities.js new file mode 100644 index 0000000000..57be5b6c9e --- /dev/null +++ b/assets/js/sync/utilities.js @@ -0,0 +1,59 @@ +/** + * Clear sync parameter from the URL. + * + * @returns {void} + */ +export const clearSyncParam = () => { + window.history.replaceState( + {}, + document.title, + document.location.pathname + document.location.search.replace(/&do_sync/, ''), + ); +}; + +/** + * Get the total number of items from index meta. + * + * @param {object} indexMeta Index meta. + * @returns {number} Number of items. + */ +export const getItemsTotalFromIndexMeta = (indexMeta) => { + let itemsTotal = 0; + + if (indexMeta.current_sync_item) { + itemsTotal += indexMeta.current_sync_item.found_items; + } + + itemsTotal = indexMeta.sync_stack.reduce( + (itemsTotal, sync) => itemsTotal + sync.found_items, + itemsTotal, + ); + + itemsTotal += indexMeta.totals.failed; + itemsTotal += indexMeta.totals.skipped; + itemsTotal += indexMeta.totals.synced; + + return itemsTotal; +}; + +/** + * Get the number of processed items from index meta. + * + * @param {object} indexMeta Index meta. + * @returns {number} Number of processed items. + */ +export const getItemsProcessedFromIndexMeta = (indexMeta) => { + let itemsProcessed = 0; + + if (indexMeta.current_sync_item) { + itemsProcessed += indexMeta.current_sync_item.failed; + itemsProcessed += indexMeta.current_sync_item.skipped; + itemsProcessed += indexMeta.current_sync_item.synced; + } + + itemsProcessed += indexMeta.totals.failed; + itemsProcessed += indexMeta.totals.skipped; + itemsProcessed += indexMeta.totals.synced; + + return itemsProcessed; +}; diff --git a/images/pause.svg b/images/pause.svg deleted file mode 100644 index 7e602ca8dd..0000000000 --- a/images/pause.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/images/resume.svg b/images/resume.svg deleted file mode 100644 index 4d09d15dcc..0000000000 --- a/images/resume.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/images/stop.svg b/images/stop.svg deleted file mode 100644 index 99ea466ff3..0000000000 --- a/images/stop.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/images/thumbsdown.svg b/images/thumbsdown.svg deleted file mode 100644 index cf22ed0ade..0000000000 --- a/images/thumbsdown.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/images/thumbsup.svg b/images/thumbsup.svg deleted file mode 100644 index 42612aafc1..0000000000 --- a/images/thumbsup.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/includes/classes/Command.php b/includes/classes/Command.php index 1973bb8ccb..7e2c3fd15b 100644 --- a/includes/classes/Command.php +++ b/includes/classes/Command.php @@ -261,7 +261,7 @@ private function put_mapping_helper( $args, $assoc_args ) { continue; } - WP_CLI::line( sprintf( esc_html__( 'Adding %1$s mapping for site %2$d...', 'elasticpress' ), esc_html( strtolower( $indexable->labels['singular'] ) ), (int) $site['blog_id'] ) ); + WP_CLI::line( sprintf( esc_html__( 'Adding %1$s mapping for site %2$d…', 'elasticpress' ), esc_html( strtolower( $indexable->labels['singular'] ) ), (int) $site['blog_id'] ) ); $indexable->delete_index(); $result = $indexable->put_mapping(); @@ -296,7 +296,7 @@ private function put_mapping_helper( $args, $assoc_args ) { continue; } - WP_CLI::line( sprintf( esc_html__( 'Adding %s mapping...', 'elasticpress' ), esc_html( strtolower( $indexable->labels['singular'] ) ) ) ); + WP_CLI::line( sprintf( esc_html__( 'Adding %s mapping…', 'elasticpress' ), esc_html( strtolower( $indexable->labels['singular'] ) ) ) ); $indexable->delete_index(); $result = $indexable->put_mapping(); @@ -332,7 +332,7 @@ private function put_mapping_helper( $args, $assoc_args ) { continue; } - WP_CLI::line( sprintf( esc_html__( 'Adding %s mapping...', 'elasticpress' ), esc_html( strtolower( $indexable->labels['singular'] ) ) ) ); + WP_CLI::line( sprintf( esc_html__( 'Adding %s mapping…', 'elasticpress' ), esc_html( strtolower( $indexable->labels['singular'] ) ) ) ); $indexable->delete_index(); $result = $indexable->put_mapping(); @@ -507,7 +507,7 @@ public function delete_index( $args, $assoc_args ) { foreach ( $non_global_indexable_objects as $indexable ) { - WP_CLI::line( sprintf( esc_html__( 'Deleting %1$s index for site %2$d...', 'elasticpress' ), esc_html( strtolower( $indexable->labels['singular'] ) ), (int) $site['blog_id'] ) ); + WP_CLI::line( sprintf( esc_html__( 'Deleting %1$s index for site %2$d…', 'elasticpress' ), esc_html( strtolower( $indexable->labels['singular'] ) ), (int) $site['blog_id'] ) ); $result = $indexable->delete_index(); @@ -522,7 +522,7 @@ public function delete_index( $args, $assoc_args ) { } } else { foreach ( $non_global_indexable_objects as $indexable ) { - WP_CLI::line( sprintf( esc_html__( 'Deleting index for %s...', 'elasticpress' ), esc_html( strtolower( $indexable->labels['plural'] ) ) ) ); + WP_CLI::line( sprintf( esc_html__( 'Deleting index for %s…', 'elasticpress' ), esc_html( strtolower( $indexable->labels['plural'] ) ) ) ); $result = $indexable->delete_index(); @@ -535,7 +535,7 @@ public function delete_index( $args, $assoc_args ) { } foreach ( $global_indexable_objects as $indexable ) { - WP_CLI::line( sprintf( esc_html__( 'Deleting index for %s...', 'elasticpress' ), esc_html( strtolower( $indexable->labels['plural'] ) ) ) ); + WP_CLI::line( sprintf( esc_html__( 'Deleting index for %s…', 'elasticpress' ), esc_html( strtolower( $indexable->labels['plural'] ) ) ) ); $result = $indexable->delete_index(); @@ -564,7 +564,7 @@ public function recreate_network_alias( $args, $assoc_args ) { $indexables = Indexables::factory()->get_all( false ); foreach ( $indexables as $indexable ) { - WP_CLI::line( sprintf( esc_html__( 'Recreating %s network alias...', 'elasticpress' ), esc_html( strtolower( $indexable->labels['singular'] ) ) ) ); + WP_CLI::line( sprintf( esc_html__( 'Recreating %s network alias…', 'elasticpress' ), esc_html( strtolower( $indexable->labels['singular'] ) ) ) ); $indexable->delete_network_alias(); @@ -1132,7 +1132,7 @@ public function stop_indexing( $args, $assoc_args ) { if ( empty( \ElasticPress\Utils\get_indexing_status() ) ) { WP_CLI::warning( esc_html__( 'There is no indexing operation running.', 'elasticpress' ) ); } else { - WP_CLI::line( esc_html__( 'Stopping indexing...', 'elasticpress' ) ); + WP_CLI::line( esc_html__( 'Stopping indexing…', 'elasticpress' ) ); if ( isset( $indexing_status['method'] ) && 'cli' === $indexing_status['method'] ) { set_transient( 'ep_wpcli_sync_interrupted', true, MINUTE_IN_SECONDS ); diff --git a/includes/classes/Feature/Search/Search.php b/includes/classes/Feature/Search/Search.php index 6f2e454b6b..47ebbe9a26 100644 --- a/includes/classes/Feature/Search/Search.php +++ b/includes/classes/Feature/Search/Search.php @@ -59,7 +59,7 @@ public function __construct() { $this->requires_install_reindex = false; - $this->default_settings = [ + $this->default_settings = [ 'decaying_enabled' => '1', 'synonyms_editor_mode' => 'simple', 'highlight_enabled' => '0', diff --git a/includes/classes/IndexHelper.php b/includes/classes/IndexHelper.php index 5d27626b87..e81913bf8d 100644 --- a/includes/classes/IndexHelper.php +++ b/includes/classes/IndexHelper.php @@ -287,7 +287,7 @@ protected function process_sync_item() { $this->output_success( sprintf( /* translators: 1: Indexable name, 2: Site ID */ - esc_html__( 'Indexing %1$s on site %2$d...', 'elasticpress' ), + esc_html__( 'Indexing %1$s on site %2$d…', 'elasticpress' ), esc_html( strtolower( $indexable->labels['plural'] ) ), $this->index_meta['current_sync_item']['blog_id'] ) @@ -295,9 +295,9 @@ protected function process_sync_item() { } else { $message_string = ( $indexable->global ) ? /* translators: 1: Indexable name */ - esc_html__( 'Indexing %1$s (globally)...', 'elasticpress' ) : + esc_html__( 'Indexing %1$s (globally)…', 'elasticpress' ) : /* translators: 1: Indexable name */ - esc_html__( 'Indexing %1$s...', 'elasticpress' ); + esc_html__( 'Indexing %1$s…', 'elasticpress' ); $this->output_success( sprintf( @@ -410,7 +410,7 @@ protected function index_objects() { $this->output( sprintf( /* translators: 1. Number of objects skipped 2. Indexable type */ - esc_html__( 'Skipping %1$d %2$s...', 'elasticpress' ), + esc_html__( 'Skipping %1$d %2$s…', 'elasticpress' ), $this->index_meta['from'], esc_html( strtolower( $indexable->labels['plural'] ) ) ), @@ -751,9 +751,9 @@ protected function index_cleanup() { $current_sync_item = $this->index_meta['current_sync_item']; - if ( $current_sync_item['failed'] ) { - $this->index_meta['current_sync_item']['failed'] = 0; + $this->index_meta['current_sync_item'] = null; + if ( $current_sync_item['failed'] ) { if ( ! empty( $current_sync_item['blog_id'] ) && defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) { $message = sprintf( /* translators: 1: indexable (plural), 2: Blog ID, 3: number of failed objects */ @@ -774,8 +774,7 @@ protected function index_cleanup() { $this->output( $message, 'warning' ); } - $this->index_meta['offset'] = 0; - $this->index_meta['current_sync_item'] = null; + $this->index_meta['offset'] = 0; if ( ! empty( $current_sync_item['blog_id'] ) && defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) { $message = sprintf( @@ -878,7 +877,7 @@ protected function create_network_alias() { $this->output_success( sprintf( /* translators: 1: Indexable name */ - esc_html__( 'Network alias created for %1$s ...', 'elasticpress' ), + esc_html__( 'Network alias created for %1$s', 'elasticpress' ), esc_html( strtolower( $indexable->labels['plural'] ) ) ) ); @@ -886,7 +885,7 @@ protected function create_network_alias() { $this->output_error( sprintf( /* translators: 1: Indexable name */ - esc_html__( 'Network alias creation failed for %1$s ...', 'elasticpress' ), + esc_html__( 'Network alias creation failed for %1$s', 'elasticpress' ), esc_html( strtolower( $indexable->labels['plural'] ) ) ) ); diff --git a/includes/classes/Indexable/User/User.php b/includes/classes/Indexable/User/User.php index 40136f9367..ada5fe2d5c 100644 --- a/includes/classes/Indexable/User/User.php +++ b/includes/classes/Indexable/User/User.php @@ -134,7 +134,7 @@ public function format_args( $query_vars, $query ) { // If there are no specific roles named, make sure the user is a member of the site. if ( empty( $query_vars['role'] ) && empty( $query_vars['role__in'] ) && empty( $query_vars['role__not_in'] ) ) { - $filter['bool']['must'][] = array( + $filter['bool']['must'][] = array( 'exists' => array( 'field' => 'capabilities.' . $blog_id . '.roles', ), @@ -747,7 +747,7 @@ public function query_db( $args ) { * WP_User_Query doesn't let us get users across all blogs easily. This is the best * way to do that. */ - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $objects = $wpdb->get_results( $wpdb->prepare( "SELECT SQL_CALC_FOUND_ROWS ID FROM {$wpdb->users} {$orderby} LIMIT %d, %d", (int) $args['offset'], (int) $args['number'] ) ); return [ diff --git a/includes/classes/Screen/Sync.php b/includes/classes/Screen/Sync.php index 8a419f9ac6..03515b6f74 100644 --- a/includes/classes/Screen/Sync.php +++ b/includes/classes/Screen/Sync.php @@ -27,8 +27,8 @@ class Sync { * Initialize class */ public function setup() { - add_action( 'wp_ajax_ep_cli_index', [ $this, 'action_wp_ajax_ep_cli_index' ] ); add_action( 'wp_ajax_ep_index', [ $this, 'action_wp_ajax_ep_index' ] ); + add_action( 'wp_ajax_ep_index_status', [ $this, 'action_wp_ajax_ep_index_status' ] ); add_action( 'wp_ajax_ep_cancel_index', [ $this, 'action_wp_ajax_ep_cancel_index' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] ); @@ -39,9 +39,9 @@ public function setup() { * * @since 3.6.0 */ - public function action_wp_ajax_ep_cli_index() { + public function action_wp_ajax_ep_index_status() { if ( ! check_ajax_referer( 'ep_dashboard_nonce', 'nonce', false ) || ! EP_DASHBOARD_SYNC ) { - wp_send_json_error(); + wp_send_json_error( null, 403 ); exit; } @@ -65,6 +65,7 @@ public function action_wp_ajax_ep_cli_index() { wp_send_json_success( [ 'is_finished' => true, + 'totals' => Utils\get_option( 'ep_last_index' ), ] ); } @@ -76,14 +77,14 @@ public function action_wp_ajax_ep_cli_index() { */ public function action_wp_ajax_ep_index() { if ( ! check_ajax_referer( 'ep_dashboard_nonce', 'nonce', false ) || ! EP_DASHBOARD_SYNC ) { - wp_send_json_error(); + wp_send_json_error( null, 403 ); exit; } $index_meta = Utils\get_indexing_status(); if ( isset( $index_meta['method'] ) && 'cli' === $index_meta['method'] ) { - wp_send_json_success( $index_meta ); + $this->action_wp_ajax_ep_index_status(); exit; } @@ -104,7 +105,7 @@ public function action_wp_ajax_ep_index() { */ public function action_wp_ajax_ep_cancel_index() { if ( ! check_ajax_referer( 'ep_dashboard_nonce', 'nonce', false ) || ! EP_DASHBOARD_SYNC ) { - wp_send_json_error(); + wp_send_json_error( null, 403 ); exit; } @@ -131,6 +132,7 @@ public function admin_enqueue_scripts() { if ( 'sync' !== Screen::factory()->get_current_screen() ) { return; } + wp_enqueue_script( 'ep_sync_scripts', EP_URL . 'dist/js/sync-script.min.js', @@ -139,6 +141,9 @@ public function admin_enqueue_scripts() { true ); + wp_enqueue_style( 'wp-components' ); + wp_enqueue_style( 'wp-edit-post' ); + wp_enqueue_style( 'ep_sync_style', EP_URL . 'dist/css/sync-styles.min.css', @@ -168,7 +173,8 @@ public function admin_enqueue_scripts() { $ep_last_index = IndexHelper::factory()->get_last_index(); if ( ! empty( $ep_last_index ) ) { - $data['ep_last_sync_date'] = ! empty( $ep_last_index['end_date_time'] ) ? $ep_last_index['end_date_time'] : false; + $data['ep_last_sync_date'] = ! empty( $ep_last_index['end_date_time'] ) ? $ep_last_index['end_date_time'] : false; + $data['ep_last_sync_failed'] = ! empty( $ep_last_index['failed'] ) ? true : false; } /** diff --git a/includes/partials/sync-page.php b/includes/partials/sync-page.php index 0889e2379b..9ae08ab989 100644 --- a/includes/partials/sync-page.php +++ b/includes/partials/sync-page.php @@ -6,200 +6,11 @@ * @package elasticpress */ -$ep_last_index = \ElasticPress\IndexHelper::factory()->get_last_index(); -$ep_last_sync_has_error = ! empty( $ep_last_index['failed'] ); +if ( ! defined( 'ABSPATH' ) ) { + exit; // Exit if accessed directly. +} +require_once __DIR__ . '/header.php'; ?> - -
-

- -
-
-
-
-

- -

- -
-

- -

- - - - - - - -
-
-
- - - - - -
- - - - - -
-
-
-
-
-
- -
-
-
-
-
- - - - -
-
- - - -
-
-
- -
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-

- -

-
-
-

- -

-
-
- - - - -
- - - - - -
-
-
-
-
- -
-
-
-
-
- - - - -
-
- - - -
-
-
- -
-
- -
-
-
-
-
-
-
-
-
-
-
- -
- -

- -

-
-
-
-
+
diff --git a/package-lock.json b/package-lock.json index 179c39e8d5..8d0bc20c87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "devDependencies": { "@wordpress/env": "^4.2.2", "10up-toolkit": "^3.0.0", + "classnames": "^2.3.1", "cypress": "^9.5.0", "cypress-file-upload": "^5.0.8", "eslint-plugin-cypress": "^2.12.1", @@ -6323,7 +6324,8 @@ }, "node_modules/classnames": { "version": "2.3.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" }, "node_modules/clean-css": { "version": "5.2.4", @@ -24188,7 +24190,9 @@ "dev": true }, "classnames": { - "version": "2.3.1" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" }, "clean-css": { "version": "5.2.4", diff --git a/package.json b/package.json index 2e5517091b..6382e06c40 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "devDependencies": { "@wordpress/env": "^4.2.2", "10up-toolkit": "^3.0.0", + "classnames": "^2.3.1", "cypress": "^9.5.0", "cypress-file-upload": "^5.0.8", "eslint-plugin-cypress": "^2.12.1", @@ -54,7 +55,7 @@ "ordering-script.min": "./assets/js/ordering/index.js", "related-posts-block-script.min": "./assets/js/blocks/related-posts/block.js", "settings-script.min": "./assets/js/settings.js", - "sync-script.min": "./assets/js/sync.js", + "sync-script.min": "./assets/js/sync/index.js", "sites-admin-script.min": "./assets/js/sites-admin.js", "stats-script.min": "./assets/js/stats.js", "synonyms-script.min": "./assets/js/synonyms/index.js", diff --git a/tests/cypress/integration/dashboard-sync.spec.js b/tests/cypress/integration/dashboard-sync.spec.js index c28d7daba3..e00dbf3c53 100644 --- a/tests/cypress/integration/dashboard-sync.spec.js +++ b/tests/cypress/integration/dashboard-sync.spec.js @@ -18,10 +18,10 @@ describe('Dashboard Sync', () => { } function resumeAndWait() { - cy.get('.ep-delete-data-and-sync .resume-sync').click(); - cy.get('.ep-delete-data-and-sync .ep-sync-box__progress-info', { + cy.get('.ep-sync-button--resume').click(); + cy.get('.ep-sync-progress strong', { timeout: Cypress.config('elasticPressIndexTimeout'), - }).should('contain.text', 'Sync completed'); + }).should('contain.text', 'Sync complete'); } before(() => { @@ -37,10 +37,10 @@ describe('Dashboard Sync', () => { it('Can index content and see indexes names in the Health Screen', () => { cy.visitAdminPage('admin.php?page=elasticpress-sync'); - cy.get('.ep-delete-data-and-sync__button-delete').click(); - cy.get('.ep-delete-data-and-sync .ep-sync-box__progress-info', { + cy.get('.ep-sync-button--delete').click(); + cy.get('.ep-sync-progress strong', { timeout: Cypress.config('elasticPressIndexTimeout'), - }).should('contain.text', 'Sync completed'); + }).should('contain.text', 'Sync complete'); canSeeIndexesNames(); }); @@ -55,10 +55,10 @@ describe('Dashboard Sync', () => { ); cy.visitAdminPage('admin.php?page=elasticpress-sync'); - cy.get('.ep-delete-data-and-sync__button-delete').click(); - cy.get('.ep-delete-data-and-sync .ep-sync-box__progress-info', { + cy.get('.ep-sync-button--delete').click(); + cy.get('.ep-sync-progress strong', { timeout: Cypress.config('elasticPressIndexTimeout'), - }).should('contain.text', 'Sync completed'); + }).should('contain.text', 'Sync complete'); cy.visitAdminPage('admin.php?page=elasticpress-health'); cy.get('.wrap').should( @@ -85,10 +85,10 @@ describe('Dashboard Sync', () => { ); cy.visitAdminPage('network/admin.php?page=elasticpress-sync'); - cy.get('.ep-delete-data-and-sync__button-delete').click(); - cy.get('.ep-delete-data-and-sync .ep-sync-box__progress-info', { + cy.get('.ep-sync-button--delete').click(); + cy.get('.ep-sync-progress strong', { timeout: Cypress.config('elasticPressIndexTimeout'), - }).should('contain.text', 'Sync completed'); + }).should('contain.text', 'Sync complete'); cy.visitAdminPage('network/admin.php?page=elasticpress-health'); cy.get('.wrap').should( @@ -120,17 +120,15 @@ describe('Dashboard Sync', () => { cy.visitAdminPage('admin.php?page=elasticpress-sync'); cy.intercept('POST', '/wp-admin/admin-ajax.php*').as('ajaxRequest'); - cy.get('.ep-delete-data-and-sync__button-delete').click(); + cy.get('.ep-sync-button--delete').click(); cy.wait('@ajaxRequest').its('response.statusCode').should('eq', 200); - cy.get('.ep-delete-data-and-sync .pause-sync').should('be.visible'); + cy.get('.ep-sync-button--pause').should('be.visible'); cy.visitAdminPage('index.php'); cy.visitAdminPage('admin.php?page=elasticpress-sync'); - cy.get('.ep-delete-data-and-sync .ep-sync-box__progress-info').should( - 'contain.text', - 'Sync in progress', - ); + cy.get('.ep-sync-button--resume').should('be.visible'); + cy.get('.ep-sync-progress strong').should('contain.text', 'Sync paused'); resumeAndWait(); @@ -144,7 +142,7 @@ describe('Dashboard Sync', () => { cy.visitAdminPage('admin.php?page=elasticpress-sync'); cy.intercept('POST', '/wp-admin/admin-ajax.php*').as('ajaxRequest'); - cy.get('.ep-delete-data-and-sync__button-delete').click(); + cy.get('.ep-sync-button--delete').click(); cy.wait('@ajaxRequest').its('response.statusCode').should('eq', 200); cy.visitAdminPage('admin.php?page=elasticpress'); @@ -164,12 +162,11 @@ describe('Dashboard Sync', () => { cy.visitAdminPage('admin.php?page=elasticpress-sync'); cy.intercept('POST', '/wp-admin/admin-ajax.php*').as('ajaxRequest'); - cy.get('.ep-delete-data-and-sync__button-delete').click(); + cy.get('.ep-sync-button--delete').click(); cy.wait('@ajaxRequest').its('response.statusCode').should('eq', 200); - cy.get('.ep-delete-data-and-sync .pause-sync').should('be.visible'); - cy.get('.ep-delete-data-and-sync .pause-sync').click(); - cy.wait('@ajaxRequest').its('response.statusCode').should('eq', 200); + cy.get('.ep-sync-button--pause').should('be.visible'); + cy.get('.ep-sync-button--pause').click(); cy.wpCli('wp elasticpress index', true) .its('stderr') diff --git a/tests/cypress/integration/features/protected-content.spec.js b/tests/cypress/integration/features/protected-content.spec.js index 9f5e88437b..738d202237 100644 --- a/tests/cypress/integration/features/protected-content.spec.js +++ b/tests/cypress/integration/features/protected-content.spec.js @@ -10,9 +10,9 @@ describe('Protected Content Feature', () => { return true; }); - cy.get('.ep-delete-data-and-sync .ep-sync-box__progress-info', { + cy.get('.ep-sync-progress strong', { timeout: Cypress.config('elasticPressIndexTimeout'), - }).should('contain.text', 'Sync completed'); + }).should('contain.text', 'Sync complete'); cy.wpCli('elasticpress list-features').its('stdout').should('contain', 'protected_content'); }); diff --git a/tests/cypress/integration/features/woocommerce.spec.js b/tests/cypress/integration/features/woocommerce.spec.js index 8cc64b668c..a5b9ed6fd5 100644 --- a/tests/cypress/integration/features/woocommerce.spec.js +++ b/tests/cypress/integration/features/woocommerce.spec.js @@ -29,9 +29,9 @@ describe('WooCommerce Feature', () => { return true; }); - cy.get('.ep-delete-data-and-sync .ep-sync-box__progress-info', { + cy.get('.ep-sync-progress strong', { timeout: Cypress.config('elasticPressIndexTimeout'), - }).should('contain.text', 'Sync completed'); + }).should('contain.text', 'Sync complete'); cy.wpCli('elasticpress list-features').its('stdout').should('contain', 'woocommerce'); }); diff --git a/tests/cypress/integration/indexables/user.spec.js b/tests/cypress/integration/indexables/user.spec.js index b4e8c45119..14abd65b94 100644 --- a/tests/cypress/integration/indexables/user.spec.js +++ b/tests/cypress/integration/indexables/user.spec.js @@ -44,9 +44,9 @@ describe('User Indexable', () => { return true; }); - cy.get('.ep-delete-data-and-sync .ep-sync-box__progress-info', { + cy.get('.ep-sync-progress strong', { timeout: Cypress.config('elasticPressIndexTimeout'), - }).should('contain.text', 'Sync completed'); + }).should('contain.text', 'Sync complete'); cy.wpCli('elasticpress list-features').its('stdout').should('contain', 'users'); });