Skip to content
73 changes: 64 additions & 9 deletions src/dashboard/Settings/Modals/editParseVersionModal.react.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className={[styles.notice, className].join(' ')}>
<Icon name={icon} width={16} height={16} fill={fill} />
<div>{children}</div>
</div>
);
};

export const EditParseVersionModal = ({ context, setParentState, currentParseVersion }) => {
const [processing, setProcessing] = useState(false);
Expand Down Expand Up @@ -97,13 +115,40 @@ 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
? []
: (migrationLinks || []).filter((m) => m && m.link && m.version !== selectedVersion?.version);

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;
Expand All @@ -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);
}
};
Expand Down Expand Up @@ -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)`
Expand Down Expand Up @@ -242,12 +293,16 @@ export const EditParseVersionModal = ({ context, setParentState, currentParseVer
</div>
</div>

{requiresDatabaseMigration && !processing && (
<Notice type='warning'>
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.
</Notice>
)}

{note !== '' && (
<div style={{ marginBottom: '10px' }}>
<FormNote show={note !== ''} color={noteColor}>
{note}
</FormNote>
</div>
<Notice type={noteColor}>
{note}
</Notice>
)}
</div>

Expand All @@ -262,7 +317,7 @@ export const EditParseVersionModal = ({ context, setParentState, currentParseVer
/>
<Button
primary={true}
value={processing ? 'Saving...' : 'Save Changes'}
value={processing ? (requiresDatabaseMigration ? 'Migrating...' : 'Saving...') : (requiresDatabaseMigration ? 'Migrate & Save' : 'Save Changes')}
color="green"
disabled={!hasSelectedVersion || processing}
onClick={save}
Expand Down
35 changes: 35 additions & 0 deletions src/dashboard/Settings/Modals/editParseVersionModal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -283,3 +283,38 @@
line-height: 1.45;
}

/* Notices (migration warning, progress and errors) */
.notice {
@include InterFont;
display: flex;
align-items: flex-start;
gap: 10px;
margin: 10px 0;
padding: 12px 16px;
border: 1px solid rgba($dark, 0.15);
border-radius: 8px;
color: $dark;
font-size: 13px;
line-height: 1.45;
text-align: left;

svg {
flex-shrink: 0;
margin-top: 2px;
}
}

.noticeWarning {
background: rgba($darkYellow, 0.06);
border-color: rgba($darkYellow, 0.4);
}

.noticeInfo {
background: rgba($old-blue, 0.06);
border-color: rgba($old-blue, 0.4);
}

.noticeError {
background: rgba($error-red, 0.06);
border-color: rgba($error-red, 0.4);
}
5 changes: 5 additions & 0 deletions src/lib/ParseApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,11 @@ export default class ParseApp {
return AJAX.post(path, { parseVersion });
}

parseServerVersionMigrationStatus(migrationId) {
const path = `/parse-version/${this.slug}/migration/${migrationId}`;
return AJAX.get(path);
}

checkStorage() {
const path = `/parse-app/${this.slug}/check-storage`;
return AJAX.post(path);
Expand Down
Loading