AWS Amplify Env Bulk Add

Adds a button to bulk-add environment variables on the AWS Amplify page from a .env file format using a stylish dialog.

// ==UserScript==
// @name         AWS Amplify Env Bulk Add
// @namespace    http://tampermonkey.net/
// @version      4.0
// @description  Adds a button to bulk-add environment variables on the AWS Amplify page from a .env file format using a stylish dialog.
// @author       Your Name
// @match        https://*.console.aws.amazon.com/amplify/apps/*
// @grant        GM_addStyle
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // --- Modal Dialog Logic ---
    let modalElements = null;

    function createModal() {
        if (document.getElementById('env-bulk-add-modal-overlay')) return;

        // Inject the CSS for the modal
        GM_addStyle(`
            #env-bulk-add-modal-overlay {
                position: fixed;
                top: 0; left: 0;
                width: 100%; height: 100%;
                background: rgba(0, 0, 0, 0.7);
                z-index: 10000;
                display: none;
                justify-content: center;
                align-items: center;
            }
            #env-bulk-add-modal-content {
                background: #232f3e;
                color: #ffffff;
                padding: 24px;
                border-radius: 8px;
                width: 90%;
                max-width: 700px;
                box-shadow: 0 5px 15px rgba(0,0,0,0.5);
                display: flex;
                flex-direction: column;
                gap: 16px;
            }
            #env-bulk-add-modal-content h3 {
                margin: 0;
                color: #ffffff;
                font-size: 18px;
                font-weight: bold;
            }
            #env-bulk-add-textarea {
                width: 100%;
                height: 350px;
                background-color: #1a2430;
                color: #f0f0f0;
                border: 1px solid #4f5d6c;
                border-radius: 4px;
                font-family: 'Menlo', 'Monaco', 'Consolas', 'Courier New', monospace;
                font-size: 14px;
                padding: 12px;
                resize: vertical;
                box-sizing: border-box;
            }
            #env-bulk-add-modal-actions {
                display: flex;
                justify-content: flex-end;
                gap: 12px;
            }
            .env-modal-btn {
                padding: 8px 16px;
                border-radius: 4px;
                border: none;
                cursor: pointer;
                font-weight: bold;
                transition: background-color 0.2s;
            }
            .env-modal-btn-primary {
                background-color: #5d6af7;
                color: white;
            }
            .env-modal-btn-primary:hover {
                background-color: #4a58d6;
            }
            .env-modal-btn-secondary {
                background-color: #4f5d6c;
                color: white;
            }
            .env-modal-btn-secondary:hover {
                background-color: #6a7b8f;
            }
        `);

        // Create the HTML elements
        const overlay = document.createElement('div');
        overlay.id = 'env-bulk-add-modal-overlay';

        const modal = document.createElement('div');
        modal.id = 'env-bulk-add-modal-content';
        modal.innerHTML = `
            <h3>Bulk Add Environment Variables</h3>
            <p style="margin: 0; color: #d0d0d0;">Paste the contents of your .env file below.</p>
            <textarea id="env-bulk-add-textarea" spellcheck="false" placeholder="KEY=VALUE\nANOTHER_KEY=ANOTHER_VALUE"></textarea>
            <div id="env-bulk-add-modal-actions">
                <button id="env-modal-cancel" class="env-modal-btn env-modal-btn-secondary">Cancel</button>
                <button id="env-modal-import" class="env-modal-btn env-modal-btn-primary">Import</button>
            </div>
        `;

        overlay.appendChild(modal);
        document.body.appendChild(overlay);

        modalElements = {
            overlay,
            textarea: document.getElementById('env-bulk-add-textarea'),
            importBtn: document.getElementById('env-modal-import'),
            cancelBtn: document.getElementById('env-modal-cancel'),
        };

        // Add event listeners
        modalElements.cancelBtn.onclick = closeModal;
        overlay.onclick = (e) => { if (e.target === overlay) closeModal(); };
        document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModal(); });
        modalElements.importBtn.onclick = handleImport;
    }

    function openModal() {
        if (!modalElements) createModal();
        modalElements.overlay.style.display = 'flex';
        modalElements.textarea.value = '';
        modalElements.textarea.focus();
    }

    function closeModal() {
        if (modalElements) modalElements.overlay.style.display = 'none';
    }

    async function handleImport() {
        const envContent = modalElements.textarea.value;
        closeModal();
        if (envContent) {
            try {
                const variables = parseEnv(envContent);
                await fillVariables(variables);
            } catch (error) {
                console.error('An error occurred during bulk add:', error);
                alert('An unexpected error occurred. Check the browser console for details.');
            }
        }
    }


    // --- Core Script Logic ---

    function parseEnv(data) {
        return data.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#')).map(l => {
            const i = l.indexOf('=');
            if (i === -1) return null;
            let v = l.substring(i + 1).trim();
            if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
            return { key: l.substring(0, i).trim(), value: v };
        }).filter(Boolean);
    }

    function setReactInputValue(input, value) {
        const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
        nativeSetter.call(input, value);
        input.dispatchEvent(new Event('input', { bubbles: true }));
    }

    async function fillVariables(parsedVars) {
        if (!parsedVars || parsedVars.length === 0) {
            alert('No valid key=value pairs found in the provided text.');
            return;
        }

        const getRowCount = () => document.querySelectorAll('input[name^="environmentVariables["][name$="].name"]').length;
        let allKeyInputs = Array.from(document.querySelectorAll('input[name^="environmentVariables["][name$="].name"]'));
        let firstEmptyIndex = allKeyInputs.findIndex(input => !input.value.trim());
        if (firstEmptyIndex === -1) firstEmptyIndex = allKeyInputs.length;

        const rowsToAdd = (firstEmptyIndex + parsedVars.length) - getRowCount();

        if (rowsToAdd > 0) {
            const addButton = Array.from(document.querySelectorAll('button')).find(btn => btn.textContent.trim().includes('Add new'));
            if (!addButton) {
                alert('Error: Could not find the "Add new" button. The script may need updating.');
                return;
            }
            for (let i = 0; i < rowsToAdd; i++) addButton.click();

            await new Promise((res, rej) => {
                const int = setInterval(() => { if (getRowCount() >= (firstEmptyIndex + parsedVars.length)) { clearInterval(int); res(); } }, 100);
                setTimeout(() => { clearInterval(int); rej(new Error('Timeout adding new rows.')); }, 5000);
            }).catch(e => { alert(e.message); });
        }

        allKeyInputs = document.querySelectorAll('input[name^="environmentVariables["][name$="].name"]');
        const allValueInputs = document.querySelectorAll('input[name^="environmentVariables["][name$="].value"]');

        parsedVars.forEach((v, i) => {
            const idx = firstEmptyIndex + i;
            if (allKeyInputs[idx] && allValueInputs[idx]) {
                setReactInputValue(allKeyInputs[idx], v.key);
                setReactInputValue(allValueInputs[idx], v.value);
            }
        });

        alert(`Successfully populated ${parsedVars.length} environment variables!\n\nPlease review and click "Save".`);
    }

    function addBulkButtonToPage() {
        if (document.getElementById('bulk-add-env-btn')) return;

        const heading = Array.from(document.querySelectorAll('h1, h2, h3, div, span'))
            .find(el => el.textContent.trim() === 'Environment Variables' && el.offsetParent !== null);
        if (!heading || !heading.parentElement) return;

        const headerContainer = heading.parentElement;
        if (headerContainer.querySelector('#bulk-add-env-btn')) return;

        headerContainer.style.display = 'flex';
        headerContainer.style.justifyContent = 'space-between';
        headerContainer.style.alignItems = 'center';
        headerContainer.style.width = '100%';

        const bulkAddButton = document.createElement('button');
        bulkAddButton.textContent = 'Bulk Add from .env';
        bulkAddButton.id = 'bulk-add-env-btn';
        bulkAddButton.type = 'button';
        bulkAddButton.style.height = 'fit-content';
        const saveButton = Array.from(document.querySelectorAll('button')).find(btn => btn.textContent.trim() === 'Save');
        bulkAddButton.className = saveButton ? saveButton.className : 'amplify-button amplify-button--primary';

        bulkAddButton.onclick = openModal; // <-- This now opens our custom modal

        headerContainer.appendChild(bulkAddButton);
    }

    const observer = new MutationObserver(() => {
        const isCorrectPage = window.location.pathname.endsWith('/variables') || window.location.pathname.endsWith('/variables/EDIT') || window.location.pathname.includes('/hosting/environment-variables');
        if (isCorrectPage) {
            addBulkButtonToPage();
            createModal(); // Pre-build the modal so it's ready
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });

})();