Aviso:

Para brindarle información de soporte completa de manera más rápida, el contenido de esta página ha sido traducido al español mediante traducción automática. Para consultar la información de soporte más precisa, consulte la versión en inglés de este contenido.

Configurar el Directorio del Client

Ahora configuremos el componente del client. El directorio del client contiene:

  • El archivo index.html que contiene el código HTML para la aplicación frontend
  • El archivo main.css que contiene el código CSS
  • El archivo main.js que contiene el código JavaScript
  • El archivo de configuración client-package.json

Codificaremos index.html, main.js y main.css.

Nota: Revise el código en esta sección para asegurarse de que lo comprende completamente. Discutiremos el código al final de esta sección.

Copie el código a continuación y péguelo en los archivos respectivos ubicados en el directorio client/ de su proyecto usando un IDE y guarde los archivos.

index.html
copy
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Catalyst File Vault</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link rel="stylesheet" href="main.css">
</head>
<body class="min-h-screen bg-gradient-to-br from-[#DCE7FF] to-blue-100 text-gray-800 flex items-center justify-center p-4">
    <main class="w-full max-w-6xl">
        <div class="bg-white p-6 rounded-lg shadow-2xl border border-blue-100 animate-card-fade-in-scale">
            <div class="flex flex-col sm:flex-row items-center justify-between mb-6 pb-4 border-b border-gray-200">
                <button id="logout-button"
                    class="bg-transparent border-2 border-red-500 text-red-600 hover:bg-red-100 hover:text-red-700 font-semibold py-2 px-5 rounded-full shadow hover:shadow-lg transition-all duration-300 flex items-center justify-center text-sm order-2 sm:order-1 mt-4 sm:mt-0">
                    <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none"
                        stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
                        <polyline points="17 17 22 12 17 7"></polyline>
                        <line x1="22" y1="12" x2="12" y2="12"></line>
                    </svg>
                    LOGOUT
                </button>
                <h1 class="text-4xl sm:text-5xl font-extrabold text-blue-700 text-center flex-grow order-1 sm:order-2">Catalyst File Vault</h1>
                <button id="upload-button"
                    class="bg-transparent border-2 border-green-500 text-green-600 hover:bg-green-100 hover:text-green-700 font-semibold py-2 px-5 rounded-full shadow hover:shadow-lg transition-all duration-300 flex items-center justify-center text-sm order-3 sm:order-3 mt-4 sm:mt-0">
                    <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none"
                        stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
                        <polyline points="17 8 12 3 7 8"></polyline>
                        <line x1="12" y1="3" x2="12" y2="15"></line>
                    </svg>
                    UPLOAD FILE
                </button>
                <input type="file" id="file-input" class="hidden">
            </div>
            <div class="overflow-x-auto">
                <table class="min-w-full bg-white border border-gray-200 rounded-lg overflow-hidden">
                    <thead>
                        <tr class="table-header-bg text-sm uppercase tracking-wider">
                            <th class="py-3 px-4 text-left font-bold">File Name</th>
                            <th class="py-3 px-4 text-left font-bold">Uploaded Time</th>
                            <th class="py-3 px-4 text-left font-bold">File Size</th>
                            <th class="py-3 px-4 text-left font-bold">Stratus Upload</th>
                            <th class="py-3 px-4 text-left font-bold">WorkDrive Sync</th>
                            <th class="py-3 px-4 text-center font-bold">Download File</th>
                            <th class="py-3 px-4 text-center font-bold">Delete File</th>
                        </tr>
                    </thead>
                    <tbody id="file-table-body" class="divide-y divide-gray-100">
                        <tr id="loading-row">
                            <td colspan="7" class="py-6 text-center">
                                <div class="flex flex-col items-center justify-center">
                                    <div
                                        class="animate-spin-custom rounded-full h-10 w-10 border-t-4 border-b-4 border-[#0059E9] mb-3">
                                    </div>
                                    <p class="text-gray-500">Loading files...</p>
                                </div>
                            </td>
                        </tr>
                        <tr id="no-files-row" class="hidden">
                            <td colspan="7" class="py-16 text-center bg-blue-50/50 rounded-b-lg">
                                <div class="flex flex-col items-center justify-center text-gray-700">
                                    <svg xmlns="http://www.w3.org/2000/svg" class="h-20 w-20 text-blue-500 mb-4"
                                        viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
                                        stroke-linecap="round" stroke-linejoin="round">
                                        <path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z">
                                        </path>
                                        <polyline points="14 2 14 8 20 8"></polyline>
                                        <line x1="12" y1="18" x2="12" y2="12"></line>
                                        <line x1="9" y1="15" x2="15" y2="15"></line>
                                    </svg>
                                    <p class="text-3xl font-extrabold mb-2 text-blue-800">No files here yet!</p>
                                    <p class="text-lg text-gray-600 mt-4">Your vault is empty. Click the 'Upload File' button in the top right to add your first file!</p>
                                </div>
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>
    </main>
    <div id="message-overlay" class="message-overlay hidden">
        <div class="message-box">
            <p id="message-text" class="text-lg font-semibold mb-4"></p>
            <button id="message-close-button"
                class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">Close</button>
        </div>
    </div>
    <script src="https://static.zohocdn.com/catalyst/sdk/js/4.6.1-beta/catalystWebSDK.js"></script>
    <script src="/__catalyst/sdk/init.js"></script>
    <script src="main.js" defer></script>
</body>
</html>
View more
main.css
copy
@keyframes spin {
    from {
        transform: rotate(0deg);
    }
    to {
        transform: rotate(360deg);
    }
}
.animate-spin-custom {
    animation: spin 1s linear infinite;
}
body {
    font-family: 'Inter', sans-serif;
}
.table-header-bg {
    background-image: linear-gradient(to right, #DBEAFE, #BFDBFE);
    color: #1E40AF;
}
#file-table-body tr:nth-child(even) {
    background-color: #F9FAFB;
}
@keyframes cardFadeInScale {
    from {
        opacity: 0;
        transform: scale(0.98);
    }
    to {
        opacity: 1;
        transform: scale(1);
    }
}
.animate-card-fade-in-scale {
    animation: cardFadeInScale 0.8s ease-out forwards;
    animation-delay: 0.2s;
}
.message-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.5);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1000;
}
.message-box {
    background-color: white;
    padding: 2rem;
    border-radius: 0.5rem;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    text-align: center;
    max-width: 400px;
    width: 90%;
}
View more
main.js
copy

const bucket_name = "file-vault"; // Reemplace con el nombre real de su bucket
const loginURL = "https://p1-807759706.development.catalystserverless.com/__catalyst/auth/login"; // Reemplace con su URL de inicio de sesión
document.addEventListener('DOMContentLoaded', async () => {
    const fileTableBody = document.getElementById('file-table-body');
    const loadingRow = document.getElementById('loading-row');
    const noFilesRow = document.getElementById('no-files-row');
    const logoutButton = document.getElementById('logout-button');
    const uploadButton = document.getElementById('upload-button');
    const fileInput = document.getElementById('file-input');
    const messageOverlay = document.getElementById('message-overlay');
    const messageText = document.getElementById('message-text');
    const messageCloseButton = document.getElementById('message-close-button');
    async function checkSignInStatus() {
        let userIsActuallySignedIn = false;
        try {
            await catalyst.auth.isUserAuthenticated();
            userIsActuallySignedIn = true;
            console.log("User is signed in.");
        } catch (err) {
            console.log("User not signed in:", err);
            userIsActuallySignedIn = false;
        }
        try {
            if (!userIsActuallySignedIn) {
                fileTableBody.innerHTML = `
                    <tr>
                        <td colspan="7" class="py-6 text-center text-red-600 font-semibold">
                            Error: Not signed in. Redirecting to login...
                        </td>
                    </tr>
                `;
                window.location.href = loginURL; 
            }
        } catch (error) {
            console.error("Failed to redirect:", error);
        }
    }
    function showMessage(text, isError = false) {
        messageText.textContent = text;
        if (isError) {
            messageText.classList.add('text-red-600');
            messageText.classList.remove('text-gray-800');
        } else {
            messageText.classList.add('text-gray-800');
            messageText.classList.remove('text-red-600');
        }
        messageOverlay.classList.remove('hidden');
    }
    function hideMessage() {
        messageOverlay.classList.add('hidden');
    }
    messageCloseButton.addEventListener('click', hideMessage);
    function createFileStatusBadge(status) {
        let bgColorClass, textColorClass, dotColorClass;
        switch (status) {
            case 'Uploaded':
                bgColorClass = 'bg-green-100';
                textColorClass = 'text-green-800';
                dotColorClass = 'text-green-400';
                break;
            case 'Deleted':
                bgColorClass = 'bg-gray-200';
                textColorClass = 'text-gray-700';
                dotColorClass = 'text-gray-500';
                break;
            case 'Failed':
                bgColorClass = 'bg-red-100';
                textColorClass = 'text-red-800';
                dotColorClass = 'text-red-400';
                break;
            default:
                bgColorClass = 'bg-yellow-100';
                textColorClass = 'text-yellow-800';
                dotColorClass = 'text-yellow-400';
        }
        return `
            <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${bgColorClass} ${textColorClass}">
                <svg class="-ml-0.5 mr-1.5 h-2 w-2 ${dotColorClass}" fill="currentColor" viewBox="0 0 8 8">
                    <circle cx="4" cy="4" r="3" />
                </svg>
                ${status}
            </span>
        `;
    }
    function createFileRow(file) {
        const row = document.createElement('tr');
        row.classList.add('hover:bg-blue-50', 'transition-colors', 'duration-150');
        row.dataset.rowId = file.ROWID;
        row.dataset.fileName = file.FileName;
        const fileSizeFormatted = file.FileSize ? (file.FileSize / 1024).toFixed(2) + ' KB' : 'N/A';
        const uploadedTimeFormatted = file.UploadedTime || 'N/A';
        const isActionDisabled = file.StratusUpload === 'Deleted' || file.StratusUpload === 'Failed';
        const disabledClass = isActionDisabled ? 'opacity-50 cursor-not-allowed' : '';
        const downloadButtonState = isActionDisabled ? 'disabled' : '';
        const deleteButtonState = isActionDisabled ? 'disabled' : '';
        row.innerHTML = `
            <td class="py-3 px-4 text-left text-gray-800 font-medium">${file.FileName}</td>
            <td class="py-3 px-4 text-left text-gray-600">${uploadedTimeFormatted}</td>
            <td class="py-3 px-4 text-left text-gray-600">${file.FileSize ? (file.FileSize / 1024).toFixed(2) + ' KB' : 'N/A'}</td>
            <td class="py-3 px-4 text-left text-gray-600">${createFileStatusBadge(file.StratusUpload)}</td>
            <td class="py-3 px-4 text-left text-gray-600">${createFileStatusBadge(file.WorkDriveSync)}</td>
            <td class="py-3 px-4 text-center">
                <button class="p-2 rounded-full text-blue-600 hover:bg-blue-50 hover:text-blue-800 transition-colors duration-200 download-btn ${disabledClass}"
                    title="Download ${file.FileName}" ${downloadButtonState}>
                    <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
                        <path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
                    </svg>
                </button>
            </td>
            <td class="py-3 px-4 text-center">
                <button class="p-2 rounded-full text-red-600 hover:bg-red-50 hover:text-red-800 transition-colors duration-200 delete-btn ${disabledClass}"
                    title="Delete ${file.FileName}" ${deleteButtonState}>
                    <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
                        <path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
                    </svg>
                </button>
            </td>
        `;
        return row;
    }
    logoutButton.addEventListener('click', async () => {
        console.log('Logging out...');
        showMessage('Logging out...', false);
        try {
            await catalyst.auth.signOut(loginURL); 
        } catch (error) {
            console.error('Logout failed:', error);
            showMessage('Logout failed. Please try again.', true);
        }
    });
    uploadButton.addEventListener('click', () => {
        fileInput.click();
    });
    fileInput.addEventListener('change', async (event) => {
        const file = event.target.files[0];
        if (file) {
            showMessage(`Uploading "${file.name}"...`, false);
            console.log('Selected file for upload:', file.name, file.type, file.size);
            let uploadSuccess = false;
            let rowId = null;
            try {
                const table = catalyst.table.tableId('FileVault');
                const details = [
                    { "FileName": file.name, "FileSize": file.size, "StratusUpload": "Pending", "WorkDriveSync": "Pending" }
                ];
                const insertResponse = await table.addRow(details);
                rowId = insertResponse.content[0].ROWID;
                const bucket = catalyst.stratus.bucket(bucket_name);
                const putObject = await bucket.putObject(file.name, file);
                const putObjectResponse = await putObject.start();
                if (putObjectResponse.status !== 200) {
                    throw new Error("Something went wrong!");
                }
                uploadSuccess = true;
                const date = new Date();
                const options = {
                    month: "long",
                    day: "2-digit",
                    year: "numeric",
                    hour: "2-digit",
                    minute: "2-digit",
                    hour12: true,
                };
                const parts = new Intl.DateTimeFormat("en-US", options).formatToParts(date);
                const lookup = Object.fromEntries(parts.map(p => [p.type, p.value]));
                const currentTime = `${lookup.month} ${lookup.day}, ${lookup.year} ${lookup.hour}:${lookup.minute} ${lookup.dayPeriod}`;
                const updateDetails = [
                    { "UploadedTime": currentTime, "StratusUpload": "Uploaded", "ROWID": rowId }
                ];
                await table.updateRow(updateDetails);
            } catch (err) {
                console.error('Error during upload process:', err);
                if (rowId) {
                    try {
                        const table = catalyst.table.tableId('FileVault');
                        const row = table.rowId(rowId);
                        await row.delete();
                    } catch (updateErr) {
                        console.error('Failed to update row status after upload error:', updateErr);
                    }
                }
            }
            if (uploadSuccess) {
                showMessage(`"${file.name}" uploaded successfully!`, false);
                fetchFiles();
                setTimeout(hideMessage, 1500);
            } else {
                showMessage(`Failed to upload "${file.name}". Please check console for details.`, true);
                setTimeout(hideMessage, 2000);
            }
        }
        event.target.value = null;
    });
    async function fetchFiles() {
        fileTableBody.innerHTML = '';
        loadingRow.classList.remove('hidden');
        noFilesRow.classList.add('hidden');
        fileTableBody.appendChild(loadingRow);
        try {
            const table = catalyst.table.tableId('FileVault');
            const response = await table.getPagedRows({});
            loadingRow.classList.add('hidden');
            if (response.status === 200) {
                if (response.content && response.content.length > 0) {
                    renderFiles(response.content);
                    noFilesRow.classList.add('hidden');
                } else {
                    fileTableBody.innerHTML = '';
                    noFilesRow.classList.remove('hidden');
                    fileTableBody.appendChild(noFilesRow);
                }
            } else {
                fileTableBody.innerHTML = `
                    <tr>
                        <td colspan="7" class="py-6 text-center text-red-600 font-semibold">
                            Error: Failed to load files. ${response.statusText || 'Please check your Catalyst Function.'}
                        </td>
                    </tr>
                `;
                console.error('Failed to fetch files:', response.statusText);
            }
        } catch (error) {
            loadingRow.classList.add('hidden');
            fileTableBody.innerHTML = `
                <tr>
                    <td colspan="7" class="py-6 text-center text-red-600 font-semibold">
                        Network Error: Could not connect or retrieve data.
                        <p class="text-sm text-gray-500 mt-2">Check console for details.</p>
                    </td>
                </tr>
            `;
            console.error('Error during file fetch:', error);
        }
    }
    function renderFiles(files) {
        files.forEach(file => {
            fileTableBody.appendChild(createFileRow(file));
        });
    }
    async function downloadFile(fileName) {
        showMessage(`Preparing to download "${fileName}"...`, false);
        try {
            const bucket = catalyst.stratus.bucket(bucket_name);
            const getObject = await bucket.getObject(fileName);
            const getObjectResponse = await getObject.start();
            const fileBlob = getObjectResponse.content;
            if (fileBlob) {
                const blobUrl = URL.createObjectURL(fileBlob);
                const a = document.createElement('a');
                a.href = blobUrl;
                a.download = fileName;
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                URL.revokeObjectURL(blobUrl);
                hideMessage();
            } else {
                showMessage(`Failed to retrieve file content for "${fileName}".`, true);
                setTimeout(hideMessage, 2000);
            }
        } catch (error) {
            console.error('Error during file download:', error);
            let errorMessage = `Error downloading "${fileName}". Please try again.`;
            if (error && error.message && error.message.includes("404")) {
                errorMessage = `File "${fileName}" not found in Stratus.`;
            }
            showMessage(errorMessage, true);
            setTimeout(hideMessage, 3000);
        }
    }
    async function deleteFile(rowId, fileName) {
        showMessage(`Are you sure you want to delete "${fileName}"? This will remove it from storage and mark it as deleted.`, false);
        messageCloseButton.textContent = 'Cancel';
        const confirmDeleteButton = document.createElement('button');
        confirmDeleteButton.textContent = 'Delete';
        confirmDeleteButton.className = 'ml-4 px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600';
        const originalCloseHandler = messageCloseButton.onclick;
        messageCloseButton.onclick = () => {
            hideMessage();
            messageCloseButton.textContent = 'Close';
            messageCloseButton.onclick = originalCloseHandler;
            confirmDeleteButton.remove();
        };
        messageCloseButton.parentNode.appendChild(confirmDeleteButton);
        confirmDeleteButton.onclick = async () => {
            hideMessage();
            messageCloseButton.textContent = 'Close';
            messageCloseButton.onclick = originalCloseHandler;
            confirmDeleteButton.remove();
            showMessage(`Processing deletion for "${fileName}"...`, false);
            console.log(`Attempting to delete file from Stratus (Name: ${fileName}) and update DB (ROWID: ${rowId})`);
            let dbUpdateSuccess = false;
            let stratusDeleteSuccess = false;
            try {
                const bucket = catalyst.stratus.bucket(bucket_name);
                await bucket.deleteObject(fileName);
                stratusDeleteSuccess = true;
            } catch (error) {
                console.error('Error deleting file from Stratus:', error);
                showMessage(`Failed to remove "${fileName}" from storage.`, true);
            }
            try {
                const table = catalyst.table.tableId('FileVault');
                const updateDetails = [
                    {
                        "ROWID": rowId,
                        "StratusUpload": "Deleted"
                    }
                ];
                await table.updateRow(updateDetails);
                dbUpdateSuccess = true;
            } catch (error) {
                console.error('Error updating Data Store row to "Deleted":', error);
            if (stratusDeleteSuccess) {
                showMessage(`File "${fileName}" removed from storage, but failed to mark as deleted in database.`, true);
            } else {
                showMessage(`Failed to mark "${fileName}" as deleted in database.`, true);
            }
        }
        if (dbUpdateSuccess && stratusDeleteSuccess) {
            showMessage(`"${fileName}" removed from storage!`, false);
            fetchFiles();
            setTimeout(hideMessage, 1500);
        } else {
            fetchFiles();
            setTimeout(hideMessage, 3000);
        }
    };
}
fileTableBody.addEventListener('click', (event) => {
    const targetButton = event.target.closest('.download-btn, .delete-btn');
    if (!targetButton || targetButton.disabled) return;

    const row = targetButton.closest('tr');
    if (!row) return;

    const rowId = row.dataset.rowId;
    const fileName = row.dataset.fileName;

    if (targetButton.classList.contains('download-btn')) {
        downloadFile(fileName);
    } else if (targetButton.classList.contains('delete-btn')) {
        deleteFile(rowId, fileName);
    }
});
await checkSignInStatus();
fetchFiles();

});

View more

A continuación, actualice su client-package.json con el código siguiente.

client-package.json
copy
{
    "name": "WorkDriveSyncApp",
    "version": "0.0.1",
    "homepage" : "/__catalyst/auth/login", 
    "login_redirect" : "index.html"
}
View more

El directorio del client está ahora configurado.

Repasemos rápidamente el funcionamiento de las funciones y el client de la aplicación:

  • index.html contiene el código para el front-end del client. El CSS está definido en main.css.

  • main.js: La función JavaScript en el componente del client que maneja las diversas acciones realizadas en el client a través de estas funciones:

    • checkSignInStatus(): Verifica si el usuario está autenticado o no; si está autenticado, permite al usuario acceder a la aplicación; de lo contrario, muestra un error y redirige al usuario a la página de inicio de sesión.

    • fetchFiles(): Obtiene todos los registros de archivos de la tabla de Data Store de Catalyst.

    • uploadFile (triggered via file input): Maneja la carga de archivos a Catalyst Stratus y actualiza los metadatos en Data Store.

    • downloadFile(fileName): Descarga un archivo del Stratus Bucket.

    • deleteFile(rowId, fileName): Elimina un archivo tanto de Stratus como de Data Store.

    • logout(): Cierra la sesión del usuario en la aplicación

  • La Event function se activa cada vez que el event listener se ejecuta. Esta función realiza las siguientes acciones:

    • Descarga el archivo que fue enviado como datos del evento por el event listener usando un flujo de archivo
    • Carga el archivo y sus metadatos a WorkDrive a través de una API con el método POST. Las credenciales OAuth se pasan para autorizar esta acción.
    • Si la acción es exitosa, el estado de sincronización de WorkDrive se establece en “Completed”. El registro en Data Store se actualiza con el ID de archivo de WorkDrive y el estado de sincronización.

Última actualización 2026-03-20 21:51:56 +0530 IST