diff --git a/src/dashboard/Settings/Modals/editParseVersionModal.react.js b/src/dashboard/Settings/Modals/editParseVersionModal.react.js index 23a776f45b..413c4b7964 100644 --- a/src/dashboard/Settings/Modals/editParseVersionModal.react.js +++ b/src/dashboard/Settings/Modals/editParseVersionModal.react.js @@ -1,13 +1,31 @@ import React, { useEffect, useState } from 'react'; import Button from 'components/Button/Button.react'; import Icon from 'components/Icon/Icon.react'; -import FormNote from 'components/FormNote/FormNote.react'; import styles from 'dashboard/Settings/Modals/editParseVersionModal.scss'; import modalStyles from 'components/B4aModal/B4aModal.scss'; import Popover from 'components/Popover/Popover.react'; import Position from 'lib/Position'; const origin = new Position(0, 0); +const MIGRATION_POLL_INTERVAL = 5000; +const MIGRATION_MAX_WAIT = 30 * 60 * 1000; +const MIGRATION_REQUEST_TIMEOUT = 15000; + +const NOTICE_TYPES = { + warning: { className: styles.noticeWarning, icon: 'b4a-warn-fill-icon', fill: '#F78400' }, + blue: { className: styles.noticeInfo, icon: 'b4a-info-circle', fill: '#15A9FF' }, + red: { className: styles.noticeError, icon: 'b4a-error-cross', fill: '#E85C3E' }, +}; + +const Notice = ({ type, children }) => { + const { className, icon, fill } = NOTICE_TYPES[type] || NOTICE_TYPES.red; + return ( +
+ +
{children}
+
+ ); +}; export const EditParseVersionModal = ({ context, setParentState, currentParseVersion }) => { const [processing, setProcessing] = useState(false); @@ -97,6 +115,7 @@ export const EditParseVersionModal = ({ context, setParentState, currentParseVer const npmModules = parseDependencies(selectedVersion?.dependencies ?? selectedVersion?.npmModules); const hasSelectedVersion = !!selectedVersion?.version; + const requiresDatabaseMigration = !!selectedVersion?.requiresDatabaseMigration && selectedVersion.version !== currentParseVersion; const visibleMigrations = isGDPR ? [] @@ -104,6 +123,32 @@ export const EditParseVersionModal = ({ context, setParentState, currentParseVer const close = () => setParentState({ showEditParseVersionModal: false }); + // The database migration runs in background on the server, so poll until it finishes + const waitForMigration = async (migrationId) => { + const startedAt = Date.now(); + while (Date.now() - startedAt < MIGRATION_MAX_WAIT) { + await new Promise((resolve) => setTimeout(resolve, MIGRATION_POLL_INTERVAL)); + let migration; + try { + // AJAX has no timeout, so a request that never answers would stop the polling + migration = await Promise.race([ + context.parseServerVersionMigrationStatus(migrationId), + new Promise((resolve, reject) => setTimeout(() => reject(new Error('timeout')), MIGRATION_REQUEST_TIMEOUT)), + ]); + } catch (e) { + // Keep polling on transient errors (e.g. gateway timeouts) + continue; + } + if (migration?.status === 'Completed') { + return; + } + if (migration?.status === 'Failed') { + throw { error: migration.error || 'Failed to migrate the database' }; + } + } + throw { error: 'The database migration is taking longer than expected. Please, reach us on chat.' }; + }; + const save = async () => { if (!selectedVersion?.version) { return; @@ -112,13 +157,18 @@ export const EditParseVersionModal = ({ context, setParentState, currentParseVer try { setProcessing(true); setNote(''); - await context.changeParseServerVersion(selectedVersion.version); + const result = await context.changeParseServerVersion(selectedVersion.version); + if (result?.migrationId) { + setNote('Upgrading your Parse Server and MongoDB versions. Please keep this window open, it can take a while.'); + setNoteColor('blue'); + await waitForMigration(result.migrationId); + } // Force UI refresh so the new parseVersion shows everywhere. + // Keeps processing until the page reloads, so the modal does not go back to its initial state. window.location.reload(); } catch (e) { setNote(e?.error || e?.message || 'Failed to save version'); setNoteColor('red'); - } finally { setProcessing(false); } }; @@ -212,6 +262,7 @@ export const EditParseVersionModal = ({ context, setParentState, currentParseVer } }} disabled={v.disabled} + title={v.disabled ? v.disabledReason : undefined} > {v.version === currentParseVersion ? `${v.version} - ${v.description || ''} (current)` @@ -242,12 +293,16 @@ export const EditParseVersionModal = ({ context, setParentState, currentParseVer + {requiresDatabaseMigration && !processing && ( + + This version requires MongoDB 8. Your database will be migrated automatically when you save, which can take a while. A short downtime may happen during the process. + + )} + {note !== '' && ( -
- - {note} - -
+ + {note} + )} @@ -262,7 +317,7 @@ export const EditParseVersionModal = ({ context, setParentState, currentParseVer />