mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
474 lines
17 KiB
JavaScript
474 lines
17 KiB
JavaScript
(() => {
|
|
const Context = window.InstallerStepsContext || {};
|
|
const State = window.InstallerStepsState || {};
|
|
const Validation = window.InstallerStepsValidation || {};
|
|
const UI = window.InstallerStepsUI || {};
|
|
const Progress = window.InstallerStepsProgress || {};
|
|
const Tooltips = window.InstallerTooltips || null;
|
|
|
|
const {
|
|
INSTALLATION_STEPS,
|
|
clampStep,
|
|
isUpgradeMode,
|
|
getEnabledDatabases
|
|
} = Context;
|
|
|
|
const {
|
|
formState,
|
|
dispatchStateChange,
|
|
applyBodyDefaults,
|
|
applyLockPayload,
|
|
clearInstallLock,
|
|
clearInstallId,
|
|
isInstallLocked,
|
|
syncInstallLockFlag,
|
|
getInstallLock,
|
|
getLockedDatabase
|
|
} = State;
|
|
|
|
const {
|
|
isValidEmail,
|
|
isValidPort,
|
|
isValidHostnameInput,
|
|
isValidPassword
|
|
} = Validation;
|
|
|
|
const {
|
|
clearFieldErrors,
|
|
setFieldError,
|
|
bindErrorClear,
|
|
updateDatabaseSelection,
|
|
setupResetButtons,
|
|
setupAccordion,
|
|
openAccordion,
|
|
disableControls,
|
|
generateSecretKey,
|
|
copyToClipboard,
|
|
setTooltipText,
|
|
resetTooltipText,
|
|
updateReviewSummary
|
|
} = UI;
|
|
|
|
let reviewListener = null;
|
|
|
|
const bindInputToState = (input, key) => {
|
|
if (!input) return;
|
|
const update = () => {
|
|
formState[key] = input.value;
|
|
dispatchStateChange?.(key);
|
|
};
|
|
input.addEventListener('input', update);
|
|
input.addEventListener('change', update);
|
|
update();
|
|
};
|
|
|
|
const lockDatabaseSelection = (root, lockedDatabase) => {
|
|
if (lockedDatabase) {
|
|
const radios = root.querySelectorAll('input[name="database"]');
|
|
radios.forEach((radio) => {
|
|
const isLockedChoice = radio.value === lockedDatabase;
|
|
const card = radio.closest('.selector-card');
|
|
radio.disabled = !isLockedChoice;
|
|
if (card) {
|
|
card.classList.toggle('is-disabled', !isLockedChoice);
|
|
}
|
|
if (isLockedChoice) {
|
|
radio.checked = true;
|
|
updateDatabaseSelection?.(radio, root);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
const applyEnabledDatabases = (root) => {
|
|
const enabled = getEnabledDatabases?.() || [];
|
|
const radios = root.querySelectorAll('input[name="database"]');
|
|
radios.forEach((radio) => {
|
|
if (!enabled.includes(radio.value)) {
|
|
const card = radio.closest('.selector-card');
|
|
if (card) {
|
|
card.remove();
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
const bindDatabaseSelection = (root) => {
|
|
const radios = root.querySelectorAll('input[name="database"]');
|
|
radios.forEach((radio) => {
|
|
radio.addEventListener('change', () => {
|
|
formState.database = radio.value;
|
|
updateDatabaseSelection?.(radio, root);
|
|
});
|
|
});
|
|
};
|
|
|
|
const hydrateStep1State = (root) => {
|
|
State.setStateIfEmpty?.('appDomain', root.querySelector('#hostname')?.value);
|
|
State.setStateIfEmpty?.('database', root.querySelector('input[name="database"]:checked')?.value);
|
|
State.setStateIfEmpty?.('httpPort', root.querySelector('#http-port')?.value);
|
|
State.setStateIfEmpty?.('httpsPort', root.querySelector('#https-port')?.value);
|
|
State.setStateIfEmpty?.('emailCertificates', root.querySelector('#ssl-email')?.value);
|
|
State.setStateIfEmpty?.('assistantOpenAIKey', root.querySelector('#assistant-openai-key')?.value);
|
|
};
|
|
|
|
const applyStep1State = (root) => {
|
|
const hostname = root.querySelector('#hostname');
|
|
if (hostname && formState.appDomain) hostname.value = formState.appDomain;
|
|
|
|
const httpPort = root.querySelector('#http-port');
|
|
if (httpPort && formState.httpPort) httpPort.value = formState.httpPort;
|
|
|
|
const httpsPort = root.querySelector('#https-port');
|
|
if (httpsPort && formState.httpsPort) httpsPort.value = formState.httpsPort;
|
|
|
|
const sslEmail = root.querySelector('#ssl-email');
|
|
if (sslEmail && formState.emailCertificates) sslEmail.value = formState.emailCertificates;
|
|
|
|
const assistantKey = root.querySelector('#assistant-openai-key');
|
|
if (assistantKey && formState.assistantOpenAIKey) {
|
|
assistantKey.value = formState.assistantOpenAIKey;
|
|
}
|
|
|
|
if (formState.database) {
|
|
const radio = root.querySelector(`input[name="database"][value="${formState.database}"]`);
|
|
if (radio) {
|
|
radio.checked = true;
|
|
updateDatabaseSelection?.(radio, root);
|
|
}
|
|
}
|
|
};
|
|
|
|
const initStep1 = (root) => {
|
|
if (!root) return;
|
|
syncInstallLockFlag?.();
|
|
applyLockPayload?.();
|
|
applyBodyDefaults?.();
|
|
hydrateStep1State(root);
|
|
applyStep1State(root);
|
|
|
|
if (isInstallLocked?.()) {
|
|
openAccordion?.(root);
|
|
disableControls?.(root);
|
|
return;
|
|
}
|
|
|
|
applyEnabledDatabases(root);
|
|
|
|
const lockedDatabase = getLockedDatabase?.() || '';
|
|
if (lockedDatabase) {
|
|
lockDatabaseSelection(root, lockedDatabase);
|
|
} else {
|
|
bindDatabaseSelection(root);
|
|
}
|
|
|
|
const hostname = root.querySelector('#hostname');
|
|
const httpPort = root.querySelector('#http-port');
|
|
const httpsPort = root.querySelector('#https-port');
|
|
const sslEmail = root.querySelector('#ssl-email');
|
|
const assistantKey = root.querySelector('#assistant-openai-key');
|
|
|
|
bindInputToState(hostname, 'appDomain');
|
|
bindInputToState(httpPort, 'httpPort');
|
|
bindInputToState(httpsPort, 'httpsPort');
|
|
bindInputToState(sslEmail, 'emailCertificates');
|
|
bindInputToState(assistantKey, 'assistantOpenAIKey');
|
|
|
|
bindErrorClear?.(hostname);
|
|
bindErrorClear?.(httpPort);
|
|
bindErrorClear?.(httpsPort);
|
|
bindErrorClear?.(sslEmail);
|
|
bindErrorClear?.(assistantKey);
|
|
|
|
const checked = root.querySelector('input[name="database"]:checked');
|
|
if (checked) {
|
|
updateDatabaseSelection?.(checked, root);
|
|
}
|
|
|
|
setupResetButtons?.(root);
|
|
setupAccordion?.(root);
|
|
Tooltips?.setupTooltipPortals?.(root);
|
|
};
|
|
|
|
const hydrateStep2State = (root) => {
|
|
const value = root.querySelector('#secret-key')?.value;
|
|
if (formState.opensslKey) return;
|
|
if (value) {
|
|
formState.opensslKey = value;
|
|
}
|
|
};
|
|
|
|
const applyStep2State = (root) => {
|
|
const input = root.querySelector('#secret-key');
|
|
if (input && formState.opensslKey) {
|
|
input.value = formState.opensslKey;
|
|
}
|
|
};
|
|
|
|
const initStep2 = (root) => {
|
|
if (!root) return;
|
|
syncInstallLockFlag?.();
|
|
applyLockPayload?.();
|
|
applyBodyDefaults?.();
|
|
hydrateStep2State(root);
|
|
if (!isUpgradeMode?.() && (!formState.opensslKey || !formState.opensslKey.trim())) {
|
|
formState.opensslKey = generateSecretKey?.();
|
|
dispatchStateChange?.('opensslKey');
|
|
}
|
|
applyStep2State(root);
|
|
|
|
const input = root.querySelector('#secret-key');
|
|
if (input) {
|
|
bindInputToState(input, 'opensslKey');
|
|
bindErrorClear?.(input);
|
|
}
|
|
|
|
const copyButton = root.querySelector('[data-copy-target]');
|
|
const tooltipWrapper = copyButton?.closest('.tooltip-wrapper');
|
|
|
|
if (tooltipWrapper) {
|
|
tooltipWrapper.addEventListener('mouseenter', () => resetTooltipText?.(tooltipWrapper));
|
|
tooltipWrapper.addEventListener('focusin', () => resetTooltipText?.(tooltipWrapper));
|
|
}
|
|
|
|
if (copyButton) {
|
|
copyButton.addEventListener('click', () => {
|
|
const targetId = copyButton.getAttribute('data-copy-target');
|
|
const targetInput = targetId ? root.querySelector(`#${targetId}`) : null;
|
|
const value = targetInput?.value || '';
|
|
copyToClipboard?.(value, targetInput);
|
|
copyButton.blur();
|
|
|
|
if (tooltipWrapper) {
|
|
const successText = tooltipWrapper.dataset.tooltipSuccess || 'Copied';
|
|
setTooltipText?.(tooltipWrapper, successText);
|
|
}
|
|
});
|
|
}
|
|
|
|
const regenerateButton = root.querySelector('[data-regenerate-target]');
|
|
if (regenerateButton && !isInstallLocked?.()) {
|
|
regenerateButton.addEventListener('click', () => {
|
|
const targetId = regenerateButton.getAttribute('data-regenerate-target');
|
|
const targetInput = targetId ? root.querySelector(`#${targetId}`) : null;
|
|
if (!targetInput) return;
|
|
regenerateButton.classList.remove('is-rotating');
|
|
void regenerateButton.offsetWidth;
|
|
regenerateButton.classList.add('is-rotating');
|
|
const handleAnimationEnd = () => {
|
|
regenerateButton.classList.remove('is-rotating');
|
|
};
|
|
regenerateButton.addEventListener('animationend', handleAnimationEnd, { once: true });
|
|
targetInput.value = generateSecretKey?.();
|
|
targetInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
});
|
|
}
|
|
|
|
if (isInstallLocked?.()) {
|
|
disableControls?.(root);
|
|
}
|
|
};
|
|
|
|
const hydrateStep3State = (root) => {
|
|
State.setStateIfEmpty?.('accountEmail', root.querySelector('#account-email')?.value);
|
|
State.setStateIfEmpty?.('accountPassword', root.querySelector('#account-password')?.value);
|
|
};
|
|
|
|
const applyStep3State = (root) => {
|
|
const email = root.querySelector('#account-email');
|
|
if (email && formState.accountEmail) email.value = formState.accountEmail;
|
|
|
|
const password = root.querySelector('#account-password');
|
|
if (password && formState.accountPassword) password.value = formState.accountPassword;
|
|
};
|
|
|
|
const initStep3 = (root) => {
|
|
if (!root) return;
|
|
syncInstallLockFlag?.();
|
|
applyLockPayload?.();
|
|
applyBodyDefaults?.();
|
|
hydrateStep3State(root);
|
|
applyStep3State(root);
|
|
|
|
const email = root.querySelector('#account-email');
|
|
const password = root.querySelector('#account-password');
|
|
const passwordToggle = root.querySelector('[data-password-toggle="account-password"]');
|
|
|
|
bindInputToState(email, 'accountEmail');
|
|
bindInputToState(password, 'accountPassword');
|
|
|
|
bindErrorClear?.(email);
|
|
bindErrorClear?.(password);
|
|
|
|
if (password && passwordToggle) {
|
|
passwordToggle.addEventListener('click', () => {
|
|
const isVisible = passwordToggle.classList.toggle('is-visible');
|
|
password.type = isVisible ? 'text' : 'password';
|
|
passwordToggle.setAttribute('aria-label', isVisible ? 'Hide password' : 'Show password');
|
|
});
|
|
}
|
|
|
|
if (isInstallLocked?.()) {
|
|
disableControls?.(root);
|
|
}
|
|
};
|
|
|
|
const initStep4 = (root) => {
|
|
if (!root) return;
|
|
syncInstallLockFlag?.();
|
|
applyLockPayload?.();
|
|
applyBodyDefaults?.();
|
|
updateReviewSummary?.(root);
|
|
if (reviewListener) {
|
|
document.removeEventListener('installer:state-change', reviewListener);
|
|
}
|
|
reviewListener = () => updateReviewSummary?.(root);
|
|
document.addEventListener('installer:state-change', reviewListener);
|
|
if (isInstallLocked?.()) {
|
|
disableControls?.(root);
|
|
}
|
|
};
|
|
|
|
const initStep6 = (root) => {
|
|
if (!root) return;
|
|
syncInstallLockFlag?.();
|
|
applyLockPayload?.();
|
|
applyBodyDefaults?.();
|
|
|
|
const checkbox = root.querySelector('#run-migration');
|
|
if (checkbox) {
|
|
if (formState.migrate !== undefined) {
|
|
checkbox.checked = formState.migrate;
|
|
} else {
|
|
formState.migrate = checkbox.checked;
|
|
}
|
|
checkbox.addEventListener('change', () => {
|
|
formState.migrate = checkbox.checked;
|
|
dispatchStateChange?.('migrate');
|
|
});
|
|
}
|
|
|
|
if (isInstallLocked?.()) {
|
|
disableControls?.(root);
|
|
}
|
|
};
|
|
|
|
const initStep = (step, container) => {
|
|
if (!container) return;
|
|
const root = container.querySelector('.step-layout') || container;
|
|
const normalized = clampStep?.(step) ?? 1;
|
|
Tooltips?.cleanupTooltipPortals?.();
|
|
if (normalized !== 4 && reviewListener) {
|
|
document.removeEventListener('installer:state-change', reviewListener);
|
|
reviewListener = null;
|
|
}
|
|
if (normalized !== 5) {
|
|
Progress.cleanupInstallFlow?.();
|
|
}
|
|
if (normalized === 1) initStep1(root);
|
|
if (normalized === 2) initStep2(root);
|
|
if (normalized === 3) initStep3(root);
|
|
if (normalized === 4) initStep4(root);
|
|
if (normalized === 5) Progress.initStep5?.(root);
|
|
if (normalized === 6) initStep6(root);
|
|
};
|
|
|
|
window.InstallerSteps = {
|
|
initStep1,
|
|
initStep2,
|
|
initStep3,
|
|
initStep4,
|
|
initStep5: Progress.initStep5,
|
|
installationSteps: INSTALLATION_STEPS || [],
|
|
isInstallLocked,
|
|
getInstallLock,
|
|
clearInstallLock,
|
|
initStep,
|
|
validateStep: (step, container) => {
|
|
const root = container?.querySelector('.step-layout') || container;
|
|
const normalized = clampStep?.(step) ?? 1;
|
|
if (normalized === 1) {
|
|
clearFieldErrors?.(root);
|
|
let valid = true;
|
|
const hostname = root?.querySelector('#hostname');
|
|
const httpPort = root?.querySelector('#http-port');
|
|
const httpsPort = root?.querySelector('#https-port');
|
|
const sslEmail = root?.querySelector('#ssl-email');
|
|
|
|
if (!hostname || !hostname.value.trim()) {
|
|
setFieldError?.(hostname, 'Please enter your Appwrite hostname');
|
|
valid = false;
|
|
} else if (!isValidHostnameInput?.(hostname.value.trim())) {
|
|
setFieldError?.(hostname, 'Please enter a valid hostname');
|
|
valid = false;
|
|
}
|
|
|
|
const parsePort = (input, label) => {
|
|
const value = input?.value;
|
|
if (!value || !isValidPort?.(value)) {
|
|
setFieldError?.(input, `Please enter a valid ${label} port (1-65535)`);
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
if (!parsePort(httpPort, 'HTTP')) valid = false;
|
|
if (!parsePort(httpsPort, 'HTTPS')) valid = false;
|
|
|
|
if (sslEmail && sslEmail.value.trim() && !isValidEmail?.(sslEmail.value.trim())) {
|
|
setFieldError?.(sslEmail, 'Please enter a valid email address');
|
|
valid = false;
|
|
}
|
|
|
|
if (!valid) {
|
|
openAccordion?.(root);
|
|
}
|
|
|
|
return valid;
|
|
}
|
|
|
|
if (normalized === 2) {
|
|
clearFieldErrors?.(root);
|
|
const secretKey = root?.querySelector('#secret-key');
|
|
const secretValue = secretKey?.value.trim() || '';
|
|
if (!secretKey || !secretValue) {
|
|
setFieldError?.(secretKey, 'Please enter or generate a secret API key');
|
|
return false;
|
|
}
|
|
if (secretValue.length > 64) {
|
|
setFieldError?.(secretKey, 'Secret API key must be 1-64 characters');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (normalized === 3) {
|
|
clearFieldErrors?.(root);
|
|
let valid = true;
|
|
const email = root?.querySelector('#account-email');
|
|
const password = root?.querySelector('#account-password');
|
|
|
|
if (!email || !email.value.trim()) {
|
|
setFieldError?.(email, 'This field is required');
|
|
valid = false;
|
|
} else if (!isValidEmail?.(email.value.trim())) {
|
|
setFieldError?.(email, 'Please enter a valid email address');
|
|
valid = false;
|
|
}
|
|
|
|
const passwordValue = password?.value ?? '';
|
|
if (!password || !/\S/.test(passwordValue)) {
|
|
setFieldError?.(password, 'This field is required');
|
|
valid = false;
|
|
} else if (!isValidPassword?.(passwordValue)) {
|
|
setFieldError?.(password, 'Password must be at least 8 characters long');
|
|
valid = false;
|
|
}
|
|
|
|
return valid;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
};
|
|
})();
|