From 3c80e071b65dda4671483aa21e5b2063523cbff8 Mon Sep 17 00:00:00 2001 From: rcarvajalp <166424809+rcarvajalp@users.noreply.github.com> Date: Sat, 9 May 2026 17:28:06 -0600 Subject: [PATCH 01/19] Modernize Frauden UI --- index.html | 4 +- src/App.css | 266 +++++++++++++++++++++++++++++++++++- src/App.tsx | 266 ++++++++++++++++++++++++++++++++++-- src/assets/frauden-logo.svg | 24 ++++ 4 files changed, 545 insertions(+), 15 deletions(-) create mode 100644 src/assets/frauden-logo.svg diff --git a/index.html b/index.html index 1a96360..a929ef2 100644 --- a/index.html +++ b/index.html @@ -1,9 +1,9 @@ - + - Amplify Storage Browser Sample + Frauden | Gestor documental
diff --git a/src/App.css b/src/App.css index 5bbce99..3834858 100644 --- a/src/App.css +++ b/src/App.css @@ -1,11 +1,269 @@ +:root { + color-scheme: light; + font-family: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --frauden-navy: #101936; + --frauden-navy-soft: #1f2d5f; + --frauden-blue: #26366f; + --frauden-silver: #c9ccd1; + --frauden-silver-dark: #8e939b; + --frauden-ink: #171b26; + --frauden-muted: #667085; + --frauden-surface: rgba(255, 255, 255, 0.9); + --frauden-border: rgba(16, 25, 54, 0.12); + --frauden-shadow: 0 24px 70px rgba(16, 25, 54, 0.14); + + --amplify-colors-brand-primary-10: #eef1f8; + --amplify-colors-brand-primary-20: #d8ddeb; + --amplify-colors-brand-primary-40: #9fa9cd; + --amplify-colors-brand-primary-60: #53639b; + --amplify-colors-brand-primary-80: var(--frauden-blue); + --amplify-colors-brand-primary-90: var(--frauden-navy-soft); + --amplify-colors-brand-primary-100: var(--frauden-navy); + --amplify-colors-font-primary: var(--frauden-ink); + --amplify-colors-font-secondary: var(--frauden-muted); + --amplify-colors-border-primary: var(--frauden-border); + --amplify-radii-small: 0.65rem; + --amplify-radii-medium: 0.95rem; + --amplify-radii-large: 1.4rem; +} + +* { + box-sizing: border-box; +} + +body { + min-width: 320px; + min-height: 100vh; + margin: 0; + color: var(--frauden-ink); + background: + radial-gradient(circle at top left, rgba(201, 204, 209, 0.48), transparent 34rem), + linear-gradient(135deg, #f8fafc 0%, #eef1f6 47%, #fdfdfd 100%); +} + +body::before { + position: fixed; + inset: 0; + z-index: -1; + pointer-events: none; + content: ""; + background: + linear-gradient(115deg, transparent 0 62%, rgba(16, 25, 54, 0.08) 62% 100%), + radial-gradient(circle at 86% 10%, rgba(38, 54, 111, 0.14), transparent 20rem); +} + #root { + width: 100%; + min-height: 100vh; +} + +.app-shell { + width: min(1440px, 100%); + min-height: 100vh; margin: 0 auto; - padding: 2rem; - text-align: center; + padding: 1.5rem; } -.header { +.site-header { display: flex; + gap: 1.5rem; + align-items: center; justify-content: space-between; + padding: 1rem 1.25rem; + background: rgba(255, 255, 255, 0.82); + border: 1px solid var(--frauden-border); + border-radius: 1.5rem; + box-shadow: 0 18px 50px rgba(16, 25, 54, 0.1); + backdrop-filter: blur(18px); +} + +.brand { + display: inline-flex; + align-items: center; + min-width: 0; +} + +.brand-logo { + display: block; + width: clamp(14rem, 34vw, 25rem); + max-height: 5.75rem; + object-fit: contain; +} + +.header-actions { + display: flex; + flex-wrap: wrap; + gap: 0.85rem; align-items: center; -} \ No newline at end of file + justify-content: flex-end; +} + +.user-card { + min-width: min(15rem, 100%); + padding: 0.75rem 1rem; + text-align: right; + background: linear-gradient(135deg, rgba(16, 25, 54, 0.06), rgba(201, 204, 209, 0.18)); + border: 1px solid rgba(16, 25, 54, 0.08); + border-radius: 1rem; +} + +.user-card strong { + display: block; + overflow: hidden; + color: var(--frauden-navy); + text-overflow: ellipsis; + white-space: nowrap; +} + +.eyebrow { + display: inline-flex; + margin-bottom: 0.35rem; + color: var(--frauden-silver-dark); + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.16em; + text-transform: uppercase; +} + +.sign-out-button.amplify-button { + min-height: 3rem; + padding-inline: 1.25rem; + font-weight: 700; + background: linear-gradient(135deg, var(--frauden-navy), var(--frauden-blue)); + border: 0; + box-shadow: 0 14px 30px rgba(16, 25, 54, 0.25); +} + +.hero-card { + position: relative; + overflow: hidden; + margin: 1.25rem 0; + padding: clamp(1.5rem, 4vw, 3.75rem); + color: white; + background: + linear-gradient(135deg, rgba(16, 25, 54, 0.94), rgba(31, 45, 95, 0.88)), + linear-gradient(90deg, rgba(201, 204, 209, 0.22), transparent); + border-radius: 2rem; + box-shadow: var(--frauden-shadow); +} + +.hero-card::after { + position: absolute; + top: -38%; + right: -10%; + width: 27rem; + height: 27rem; + content: ""; + background: radial-gradient(circle, rgba(255, 255, 255, 0.2), transparent 62%); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 999px; +} + +.hero-card > div { + position: relative; + z-index: 1; + max-width: 50rem; +} + +.hero-card .eyebrow { + color: var(--frauden-silver); +} + +.hero-card h1 { + max-width: 48rem; + margin: 0; + font-size: clamp(2rem, 5vw, 4.75rem); + line-height: 0.96; + letter-spacing: -0.06em; +} + +.hero-card p { + max-width: 43rem; + margin: 1.1rem 0 0; + color: rgba(255, 255, 255, 0.78); + font-size: clamp(1rem, 2vw, 1.2rem); + line-height: 1.7; +} + +.storage-card { + padding: clamp(0.85rem, 2vw, 1.4rem); + background: var(--frauden-surface); + border: 1px solid var(--frauden-border); + border-radius: 2rem; + box-shadow: var(--frauden-shadow); + backdrop-filter: blur(18px); +} + +.storage-card :is(.amplify-button, button) { + border-radius: 0.85rem; +} + +.storage-card :is(.amplify-button--primary, [data-variation="primary"]) { + background: linear-gradient(135deg, var(--frauden-navy), var(--frauden-blue)); + border-color: transparent; +} + +.storage-card :is(table, .amplify-table) { + overflow: hidden; + border-radius: 1rem; +} + +.storage-card :is(th, .amplify-table__th) { + color: var(--frauden-navy); + background: #f4f6f9; +} + +.amplify-authenticator { + min-height: 100vh; + background: + radial-gradient(circle at 18% 10%, rgba(201, 204, 209, 0.5), transparent 22rem), + linear-gradient(135deg, #f8fafc, #eef1f6); +} + +.amplify-authenticator [data-amplify-router] { + overflow: hidden; + border: 1px solid var(--frauden-border); + border-radius: 1.5rem; + box-shadow: var(--frauden-shadow); +} + +@media (max-width: 760px) { + .app-shell { + padding: 0.85rem; + } + + .site-header, + .header-actions { + align-items: stretch; + } + + .site-header { + flex-direction: column; + } + + .brand-logo { + width: min(100%, 22rem); + } + + .header-actions, + .sign-out-button.amplify-button { + width: 100%; + } + + .user-card { + flex: 1; + text-align: left; + } +} + +.auth-brand { + display: flex; + justify-content: center; + padding: 2rem 2rem 0.5rem; +} + +.auth-brand-logo { + width: min(20rem, 82vw); + height: auto; +} diff --git a/src/App.tsx b/src/App.tsx index e2f45be..f4c9d4e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,29 +2,277 @@ import { createAmplifyAuthAdapter, createStorageBrowser, } from '@aws-amplify/ui-react-storage/browser'; +import '@aws-amplify/ui-react/styles.css'; import '@aws-amplify/ui-react-storage/styles.css'; import './App.css'; import config from '../amplify_outputs.json'; import { Amplify } from 'aws-amplify'; -import { Authenticator, Button } from '@aws-amplify/ui-react'; +import { I18n } from 'aws-amplify/utils'; +import { Authenticator, Button, translations } from '@aws-amplify/ui-react'; +import fraudenLogo from './assets/frauden-logo.svg'; + Amplify.configure(config); +I18n.putVocabularies(translations); +I18n.setLanguage('es'); const { StorageBrowser } = createStorageBrowser({ config: createAmplifyAuthAdapter(), }); +type StorageBrowserDisplayText = NonNullable[0]['displayText']>; + +const authenticatorComponents = { + Header() { + return ( +
+ Frauden +
+ ); + }, +}; + +const storageBrowserDisplayText: StorageBrowserDisplayText = { + LocationsView: { + title: 'Inicio', + searchPlaceholder: 'Filtrar carpetas y archivos', + searchSubmitLabel: 'Buscar', + searchClearLabel: 'Limpiar búsqueda', + loadingIndicatorLabel: 'Cargando', + tableColumnBucketHeader: 'Bucket', + tableColumnFolderHeader: 'Carpeta', + tableColumnPermissionsHeader: 'Permisos', + tableColumnActionsHeader: 'Acciones', + getPermissionName: (permissions) => { + const canRead = permissions.includes('get') || permissions.includes('list'); + const canWrite = permissions.includes('write') || permissions.includes('delete'); + + if (canRead && canWrite) return 'Lectura/Escritura'; + if (canRead) return 'Lectura'; + if (canWrite) return 'Escritura'; + + return permissions.join('/'); + }, + getDownloadLabel: (fileName) => `Descargar ${fileName}`, + getListLocationsResultMessage: (data) => { + const { isLoading, items, hasExhaustedSearch, hasError = false, message } = data ?? {}; + + if (isLoading) return undefined; + if (hasError) { + return { + type: 'error', + content: message ?? 'Ocurrió un error al cargar las ubicaciones.', + }; + } + if (items?.length === 0 && !hasExhaustedSearch) { + return { type: 'info', content: 'No hay carpetas ni archivos.' }; + } + if (hasExhaustedSearch) { + return { + type: 'info', + content: 'Se muestran resultados de hasta los primeros 10,000 elementos.', + }; + } + + return undefined; + }, + }, + LocationDetailView: { + loadingIndicatorLabel: 'Cargando', + searchPlaceholder: 'Buscar en la carpeta actual', + searchSubmitLabel: 'Buscar', + searchClearLabel: 'Limpiar búsqueda', + searchSubfoldersToggleLabel: 'Incluir subcarpetas', + selectFileLabel: 'Seleccionar archivo', + selectAllFilesLabel: 'Seleccionar todos los archivos', + tableColumnLastModifiedHeader: 'Última modificación', + tableColumnNameHeader: 'Nombre', + tableColumnSizeHeader: 'Tamaño', + tableColumnTypeHeader: 'Tipo', + getActionListItemLabel: (key = '') => { + const labels: Record = { + Copy: 'Copiar', + Delete: 'Eliminar', + 'Create folder': 'Crear carpeta', + Upload: 'Subir', + }; + + return labels[key] ?? key; + }, + getListItemsResultMessage: (data) => { + const { items, hasExhaustedSearch, hasError = false, message, isLoading } = data ?? {}; + + if (isLoading) return undefined; + if (hasError) { + return { + type: 'error', + content: message ?? 'Ocurrió un error al cargar los elementos.', + }; + } + if (!items?.length && hasExhaustedSearch) { + return { + type: 'info', + content: 'No se encontraron resultados en los primeros 10,000 elementos.', + }; + } + if (!items?.length) return { type: 'info', content: 'No hay archivos.' }; + if (hasExhaustedSearch) { + return { + type: 'info', + content: 'Se muestran resultados de hasta los primeros 10,000 elementos.', + }; + } + + return undefined; + }, + }, + UploadView: { + title: 'Subir', + actionStartLabel: 'Subir', + actionCancelLabel: 'Cancelar', + actionExitLabel: 'Salir', + addFilesLabel: 'Agregar archivos', + addFolderLabel: 'Agregar carpeta', + overwriteToggleLabel: 'Sobrescribir archivos existentes', + statusDisplayCanceledLabel: 'Cancelado', + statusDisplayCompletedLabel: 'Completado', + statusDisplayFailedLabel: 'Fallido', + statusDisplayInProgressLabel: 'En progreso', + statusDisplayOverwritePreventedLabel: 'Sobrescritura evitada', + statusDisplayQueuedLabel: 'Sin iniciar', + statusDisplayTotalLabel: 'Total', + tableColumnFolderHeader: 'Carpeta', + tableColumnNameHeader: 'Nombre', + tableColumnTypeHeader: 'Tipo', + tableColumnSizeHeader: 'Tamaño', + tableColumnStatusHeader: 'Estado', + tableColumnProgressHeader: 'Progreso', + getActionCompleteMessage: () => ({ + content: 'Proceso de carga finalizado.', + type: 'success', + }), + }, + DeleteView: { + title: 'Eliminar', + actionStartLabel: 'Eliminar', + actionCancelLabel: 'Cancelar', + actionExitLabel: 'Salir', + statusDisplayCanceledLabel: 'Cancelado', + statusDisplayCompletedLabel: 'Completado', + statusDisplayFailedLabel: 'Fallido', + statusDisplayInProgressLabel: 'En progreso', + statusDisplayQueuedLabel: 'Sin iniciar', + statusDisplayTotalLabel: 'Total', + tableColumnFolderHeader: 'Carpeta', + tableColumnNameHeader: 'Nombre', + tableColumnTypeHeader: 'Tipo', + tableColumnSizeHeader: 'Tamaño', + tableColumnStatusHeader: 'Estado', + getActionCompleteMessage: () => ({ + content: 'Proceso de eliminación finalizado.', + type: 'success', + }), + }, + CopyView: { + title: 'Copiar', + actionStartLabel: 'Copiar', + actionCancelLabel: 'Cancelar', + actionExitLabel: 'Salir', + actionDestinationLabel: 'Destino de la copia', + loadingIndicatorLabel: 'Cargando' as 'Loading', + overwriteWarningMessage: + 'Los archivos copiados sobrescribirán archivos existentes en el destino seleccionado.', + searchPlaceholder: 'Buscar carpetas', + searchSubmitLabel: 'Buscar', + searchClearLabel: 'Limpiar búsqueda', + statusDisplayCanceledLabel: 'Cancelado', + statusDisplayCompletedLabel: 'Completado', + statusDisplayFailedLabel: 'Fallido', + statusDisplayInProgressLabel: 'En progreso', + statusDisplayQueuedLabel: 'Sin iniciar', + statusDisplayTotalLabel: 'Total', + tableColumnFolderHeader: 'Carpeta', + tableColumnNameHeader: 'Nombre', + tableColumnTypeHeader: 'Tipo', + tableColumnSizeHeader: 'Tamaño', + tableColumnStatusHeader: 'Estado', + tableColumnProgressHeader: 'Progreso', + getListFoldersResultsMessage: ({ folders, query, hasError, message, hasExhaustedSearch }) => { + if (!folders?.length) { + return { + content: query + ? `No se encontraron carpetas que coincidan con "${query}".` + : 'No se encontraron subcarpetas en la carpeta seleccionada.', + type: 'info', + }; + } + if ((message && query) || hasError) + return { content: 'Error al cargar carpetas.', type: 'error' }; + if (hasExhaustedSearch) { + return { + content: 'Se muestran resultados de hasta los primeros 10,000 elementos.', + type: 'info', + }; + } + + return undefined; + }, + getActionCompleteMessage: () => ({ + content: 'Proceso de copia finalizado.', + type: 'success', + }), + }, + CreateFolderView: { + title: 'Crear carpeta', + actionStartLabel: 'Crear carpeta', + actionCancelLabel: 'Cancelar', + actionExitLabel: 'Salir', + actionDestinationLabel: 'Destino', + folderNameLabel: 'Nombre de la carpeta', + folderNamePlaceholder: 'No puede contener "/" ni empezar o terminar con "."', + getValidationMessage: () => 'El nombre no puede contener "/" ni empezar o terminar con "."', + getActionCompleteMessage: () => ({ + content: 'Carpeta creada.', + type: 'success', + }), + }, +}; + function App() { return ( - + {({ signOut, user }) => ( - <> -
-

{`Hello ${user?.username}`}

- -
- - +
+
+ + Frauden + +
+
+ Sesión activa + {user?.username ?? 'Usuario'} +
+ +
+
+ +
+
+ Panel seguro +

Gestor documental antifraude

+

+ Organiza, consulta y administra archivos críticos con una experiencia clara, + profesional y alineada a la identidad de Frauden. +

+
+
+ +
+ +
+
)}
); diff --git a/src/assets/frauden-logo.svg b/src/assets/frauden-logo.svg new file mode 100644 index 0000000..1a7d2c9 --- /dev/null +++ b/src/assets/frauden-logo.svg @@ -0,0 +1,24 @@ + + Frauden + Logo de Frauden con texto plateado y azul marino + + + + + + + + + + + + + + + + + FRAUD + EN + + Fraude empresarial y en los negocios + From 8f2f915457714a4db6ebfecb6d2ee8ac6c5866bb Mon Sep 17 00:00:00 2001 From: rcarvajalp <166424809+rcarvajalp@users.noreply.github.com> Date: Sat, 9 May 2026 20:39:29 -0600 Subject: [PATCH 02/19] Update hero subtitle --- amplify/backend.ts | 14 ++++++++++---- src/App.tsx | 8 ++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/amplify/backend.ts b/amplify/backend.ts index 032938d..3f569c7 100644 --- a/amplify/backend.ts +++ b/amplify/backend.ts @@ -2,12 +2,18 @@ import { defineBackend } from '@aws-amplify/backend'; import { auth } from './auth/resource'; import { storage, secondaryStorage } from './storage/resource'; - /** * @see https://docs.amplify.aws/react/build-a-backend/ to add storage, functions, and more */ -defineBackend({ +const backend = defineBackend({ auth, - storage, - secondaryStorage + storage, + secondaryStorage, }); + +const { cfnUserPool } = backend.auth.resources.cfnResources; + +cfnUserPool.adminCreateUserConfig = { + ...cfnUserPool.adminCreateUserConfig, + allowAdminCreateUserOnly: true, +}; diff --git a/src/App.tsx b/src/App.tsx index f4c9d4e..437f94b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -240,7 +240,7 @@ const storageBrowserDisplayText: StorageBrowserDisplayText = { function App() { return ( - + {({ signOut, user }) => (
@@ -261,10 +261,10 @@ function App() {
Panel seguro -

Gestor documental antifraude

+

Gestor documental de Frauden

- Organiza, consulta y administra archivos críticos con una experiencia clara, - profesional y alineada a la identidad de Frauden. + carga archivos y organiza de forma segura y sencilla el archivo documental de + Frauden.

From 0fbda646a74690250241db0b3fbb9e5e50e86ad6 Mon Sep 17 00:00:00 2001 From: rcarvajalp <166424809+rcarvajalp@users.noreply.github.com> Date: Mon, 11 May 2026 11:38:39 -0600 Subject: [PATCH 03/19] Grant delete where storage write is allowed --- README.md | 4 +-- amplify/storage/resource.ts | 52 ++++++++++++++++--------------------- 2 files changed, 25 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 3bfe9f0..c797c37 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ This template equips you with a foundational React application integrated with A - **Authentication**: Setup with Amazon Cognito for secure user authentication with email login. - More info on how to setup and configuration option: https://docs.amplify.aws/react/build-a-backend/auth/set-up-auth/ - **Storage**: Configured with multiple S3 buckets and granular access controls. The sample is configured with - - Default storage bucket with public, admin, and private access paths - - Secondary storage bucket with separate backup paths. + - Default `frauden-bucket` storage bucket with `doctrina`, `medios`, `jurisprudencia`, and `legislacion` access paths for authenticated readers and admin read/write/delete access. + - Secondary `frauden-expedientes` storage bucket with `publico`, `confidencial`, and owner-scoped `privado/{entity_id}` access paths, including delete permissions wherever write access is granted. - More info on how to setup : https://docs.amplify.aws/react/build-a-backend/storage/set-up-storage/#building-your-storage-backend - **UI Components**: Pre-integrated Amplify UI React components including: - Authenticator for sign-in/sign-up flows diff --git a/amplify/storage/resource.ts b/amplify/storage/resource.ts index 62d6340..8dd2ab6 100644 --- a/amplify/storage/resource.ts +++ b/amplify/storage/resource.ts @@ -1,39 +1,33 @@ import { defineStorage } from '@aws-amplify/backend'; export const storage = defineStorage({ - name: 'myStorageBucket', + name: 'frauden-bucket', isDefault: true, - access: (allow) => ({ - 'public/*': [ - allow.guest.to(['read', 'write']), - allow.authenticated.to(['read', 'write', 'delete']), + access: (allow) => ({ + 'doctrina/*': [ + allow.authenticated.to(['read']), + allow.groups(['admin']).to(['read', 'write', 'delete']), ], - 'admin/*': [ - allow.groups(['admin']).to(['read', 'write', 'delete']), - allow.authenticated.to(['read']) + 'medios/*': [ + allow.authenticated.to(['read']), + allow.groups(['admin']).to(['read', 'write', 'delete']), ], - 'private/{entity_id}/*': [ - allow.entity('identity').to(['read', 'write', 'delete']) - ] - }) -}); - -export const secondaryStorage = defineStorage({ - name: 'mySecondaryStorageBucket', - access: (allow) => ({ - 'backup_public/*': [ - allow.guest.to(['read', 'write']), - allow.authenticated.to(['read', 'write', 'delete']), + 'jurisprudencia/*': [ + allow.authenticated.to(['read']), + allow.groups(['admin']).to(['read', 'write', 'delete']), ], - 'backup_admin/*': [ - allow.groups(['admin']).to(['read', 'write', 'delete']), - allow.authenticated.to(['read']) + 'legislacion/*': [ + allow.authenticated.to(['read']), + allow.groups(['admin']).to(['read', 'write', 'delete']), ], - 'backup_private/{entity_id}/*': [ - allow.entity('identity').to(['read', 'write', 'delete']) - ] - }) + }), }); - - +export const secondaryStorage = defineStorage({ + name: 'frauden-expedientes', + access: (allow) => ({ + 'publico/*': [allow.authenticated.to(['read', 'write', 'delete'])], + 'confidencial/*': [allow.groups(['admin']).to(['read', 'write', 'delete'])], + 'privado/{entity_id}/*': [allow.entity('identity').to(['read', 'write', 'delete'])], + }), +}); From 23dca19decd6212a1ff53cff642067e6e68c62f5 Mon Sep 17 00:00:00 2001 From: rcarvajalp <166424809+rcarvajalp@users.noreply.github.com> Date: Mon, 11 May 2026 13:36:37 -0600 Subject: [PATCH 04/19] Use resource-defined S3 bucket names --- amplify/backend.ts | 10 +++++++++- amplify/storage/resource.ts | 7 +++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/amplify/backend.ts b/amplify/backend.ts index 3f569c7..0d0e6a6 100644 --- a/amplify/backend.ts +++ b/amplify/backend.ts @@ -1,6 +1,11 @@ import { defineBackend } from '@aws-amplify/backend'; import { auth } from './auth/resource'; -import { storage, secondaryStorage } from './storage/resource'; +import { + secondaryStorage, + secondaryStorageBucketName, + storage, + storageBucketName, +} from './storage/resource'; /** * @see https://docs.amplify.aws/react/build-a-backend/ to add storage, functions, and more @@ -11,6 +16,9 @@ const backend = defineBackend({ secondaryStorage, }); +backend.storage.resources.cfnResources.cfnBucket.bucketName = storageBucketName; +backend.secondaryStorage.resources.cfnResources.cfnBucket.bucketName = secondaryStorageBucketName; + const { cfnUserPool } = backend.auth.resources.cfnResources; cfnUserPool.adminCreateUserConfig = { diff --git a/amplify/storage/resource.ts b/amplify/storage/resource.ts index 8dd2ab6..fa1eef0 100644 --- a/amplify/storage/resource.ts +++ b/amplify/storage/resource.ts @@ -1,7 +1,10 @@ import { defineStorage } from '@aws-amplify/backend'; +export const storageBucketName = 'frauden-bucket'; +export const secondaryStorageBucketName = 'frauden-expedientes'; + export const storage = defineStorage({ - name: 'frauden-bucket', + name: storageBucketName, isDefault: true, access: (allow) => ({ 'doctrina/*': [ @@ -24,7 +27,7 @@ export const storage = defineStorage({ }); export const secondaryStorage = defineStorage({ - name: 'frauden-expedientes', + name: secondaryStorageBucketName, access: (allow) => ({ 'publico/*': [allow.authenticated.to(['read', 'write', 'delete'])], 'confidencial/*': [allow.groups(['admin']).to(['read', 'write', 'delete'])], From b5f6ebec472d67761b01bf422830e1e2bf080b30 Mon Sep 17 00:00:00 2001 From: rcarvajalp <166424809+rcarvajalp@users.noreply.github.com> Date: Mon, 11 May 2026 13:56:14 -0600 Subject: [PATCH 05/19] Avoid physical bucket rename collision --- amplify/storage/resource.ts | 7 +++-- src/App.tsx | 54 +++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/amplify/storage/resource.ts b/amplify/storage/resource.ts index 8dd2ab6..fa1eef0 100644 --- a/amplify/storage/resource.ts +++ b/amplify/storage/resource.ts @@ -1,7 +1,10 @@ import { defineStorage } from '@aws-amplify/backend'; +export const storageBucketName = 'frauden-bucket'; +export const secondaryStorageBucketName = 'frauden-expedientes'; + export const storage = defineStorage({ - name: 'frauden-bucket', + name: storageBucketName, isDefault: true, access: (allow) => ({ 'doctrina/*': [ @@ -24,7 +27,7 @@ export const storage = defineStorage({ }); export const secondaryStorage = defineStorage({ - name: 'frauden-expedientes', + name: secondaryStorageBucketName, access: (allow) => ({ 'publico/*': [allow.authenticated.to(['read', 'write', 'delete'])], 'confidencial/*': [allow.groups(['admin']).to(['read', 'write', 'delete'])], diff --git a/src/App.tsx b/src/App.tsx index 437f94b..50c7f44 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ import { + componentsDefault, createAmplifyAuthAdapter, createStorageBrowser, } from '@aws-amplify/ui-react-storage/browser'; @@ -11,12 +12,64 @@ import { Amplify } from 'aws-amplify'; import { I18n } from 'aws-amplify/utils'; import { Authenticator, Button, translations } from '@aws-amplify/ui-react'; import fraudenLogo from './assets/frauden-logo.svg'; +import type { ComponentProps } from 'react'; Amplify.configure(config); I18n.putVocabularies(translations); I18n.setLanguage('es'); +const resourceBucketNames = ['frauden-bucket', 'frauden-expedientes'] as const; + +const getResourceBucketName = (bucketName: string) => { + const comparableBucketName = bucketName.toLowerCase().replace(/[^a-z0-9]/g, ''); + + return ( + resourceBucketNames.find((resourceBucketName) => { + const comparableResourceBucketName = resourceBucketName + .toLowerCase() + .replace(/[^a-z0-9]/g, ''); + + return ( + bucketName === resourceBucketName || + comparableBucketName.includes(comparableResourceBucketName) + ); + }) ?? bucketName + ); +}; + +const DefaultDataTable = componentsDefault.DataTable; + +type DataTableProps = ComponentProps>; + +const ResourceBucketDataTable = ({ headers, rows, ...props }: DataTableProps) => { + const bucketColumnIndex = headers.findIndex(({ key }) => key === 'bucket'); + const displayRows = + bucketColumnIndex === -1 + ? rows + : rows.map((row) => ({ + ...row, + content: row.content.map((cell, index) => + index === bucketColumnIndex && cell.type === 'text' + ? { + ...cell, + content: { + ...cell.content, + text: getResourceBucketName(cell.content.text ?? ''), + }, + } + : cell + ), + })); + + return DefaultDataTable ? ( + + ) : null; +}; + const { StorageBrowser } = createStorageBrowser({ + components: { + DataTable: ResourceBucketDataTable, + }, config: createAmplifyAuthAdapter(), }); @@ -89,6 +142,7 @@ const storageBrowserDisplayText: StorageBrowserDisplayText = { tableColumnNameHeader: 'Nombre', tableColumnSizeHeader: 'Tamaño', tableColumnTypeHeader: 'Tipo', + getTitle: ({ current, key }) => key || getResourceBucketName(current?.bucket ?? ''), getActionListItemLabel: (key = '') => { const labels: Record = { Copy: 'Copiar', From 3ff587f05d51faf8c68fb1d909994ab4a5b82f7d Mon Sep 17 00:00:00 2001 From: rcarvajalp <166424809+rcarvajalp@users.noreply.github.com> Date: Mon, 11 May 2026 14:39:57 -0600 Subject: [PATCH 06/19] Remove reusable bucket name constants --- amplify/backend.ts | 3 +++ src/App.tsx | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/amplify/backend.ts b/amplify/backend.ts index 3f569c7..0c49596 100644 --- a/amplify/backend.ts +++ b/amplify/backend.ts @@ -11,6 +11,9 @@ const backend = defineBackend({ secondaryStorage, }); +// Do not override cfnBucket.bucketName here. S3 bucket names are globally unique, +// and Amplify's generated physical names avoid collisions during deployments. + const { cfnUserPool } = backend.auth.resources.cfnResources; cfnUserPool.adminCreateUserConfig = { diff --git a/src/App.tsx b/src/App.tsx index 437f94b..50c7f44 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ import { + componentsDefault, createAmplifyAuthAdapter, createStorageBrowser, } from '@aws-amplify/ui-react-storage/browser'; @@ -11,12 +12,64 @@ import { Amplify } from 'aws-amplify'; import { I18n } from 'aws-amplify/utils'; import { Authenticator, Button, translations } from '@aws-amplify/ui-react'; import fraudenLogo from './assets/frauden-logo.svg'; +import type { ComponentProps } from 'react'; Amplify.configure(config); I18n.putVocabularies(translations); I18n.setLanguage('es'); +const resourceBucketNames = ['frauden-bucket', 'frauden-expedientes'] as const; + +const getResourceBucketName = (bucketName: string) => { + const comparableBucketName = bucketName.toLowerCase().replace(/[^a-z0-9]/g, ''); + + return ( + resourceBucketNames.find((resourceBucketName) => { + const comparableResourceBucketName = resourceBucketName + .toLowerCase() + .replace(/[^a-z0-9]/g, ''); + + return ( + bucketName === resourceBucketName || + comparableBucketName.includes(comparableResourceBucketName) + ); + }) ?? bucketName + ); +}; + +const DefaultDataTable = componentsDefault.DataTable; + +type DataTableProps = ComponentProps>; + +const ResourceBucketDataTable = ({ headers, rows, ...props }: DataTableProps) => { + const bucketColumnIndex = headers.findIndex(({ key }) => key === 'bucket'); + const displayRows = + bucketColumnIndex === -1 + ? rows + : rows.map((row) => ({ + ...row, + content: row.content.map((cell, index) => + index === bucketColumnIndex && cell.type === 'text' + ? { + ...cell, + content: { + ...cell.content, + text: getResourceBucketName(cell.content.text ?? ''), + }, + } + : cell + ), + })); + + return DefaultDataTable ? ( + + ) : null; +}; + const { StorageBrowser } = createStorageBrowser({ + components: { + DataTable: ResourceBucketDataTable, + }, config: createAmplifyAuthAdapter(), }); @@ -89,6 +142,7 @@ const storageBrowserDisplayText: StorageBrowserDisplayText = { tableColumnNameHeader: 'Nombre', tableColumnSizeHeader: 'Tamaño', tableColumnTypeHeader: 'Tipo', + getTitle: ({ current, key }) => key || getResourceBucketName(current?.bucket ?? ''), getActionListItemLabel: (key = '') => { const labels: Record = { Copy: 'Copiar', From d3098d1207caabde3c86e75f65a4bdede08517cd Mon Sep 17 00:00:00 2001 From: rcarvajalp Date: Mon, 11 May 2026 20:05:32 -0600 Subject: [PATCH 07/19] Revert "Merge pull request #5 from rcarvajalp/codex/locate-buckets-without-prefixes-or-suffixes-r8f952" This reverts commit 094eac468a9548964f52455cf36ca062ac304b57, reversing changes made to 2b5b6eb3af63cba188a2c9021730ffe313625bf5. --- src/App.tsx | 54 ----------------------------------------------------- 1 file changed, 54 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 50c7f44..437f94b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,4 @@ import { - componentsDefault, createAmplifyAuthAdapter, createStorageBrowser, } from '@aws-amplify/ui-react-storage/browser'; @@ -12,64 +11,12 @@ import { Amplify } from 'aws-amplify'; import { I18n } from 'aws-amplify/utils'; import { Authenticator, Button, translations } from '@aws-amplify/ui-react'; import fraudenLogo from './assets/frauden-logo.svg'; -import type { ComponentProps } from 'react'; Amplify.configure(config); I18n.putVocabularies(translations); I18n.setLanguage('es'); -const resourceBucketNames = ['frauden-bucket', 'frauden-expedientes'] as const; - -const getResourceBucketName = (bucketName: string) => { - const comparableBucketName = bucketName.toLowerCase().replace(/[^a-z0-9]/g, ''); - - return ( - resourceBucketNames.find((resourceBucketName) => { - const comparableResourceBucketName = resourceBucketName - .toLowerCase() - .replace(/[^a-z0-9]/g, ''); - - return ( - bucketName === resourceBucketName || - comparableBucketName.includes(comparableResourceBucketName) - ); - }) ?? bucketName - ); -}; - -const DefaultDataTable = componentsDefault.DataTable; - -type DataTableProps = ComponentProps>; - -const ResourceBucketDataTable = ({ headers, rows, ...props }: DataTableProps) => { - const bucketColumnIndex = headers.findIndex(({ key }) => key === 'bucket'); - const displayRows = - bucketColumnIndex === -1 - ? rows - : rows.map((row) => ({ - ...row, - content: row.content.map((cell, index) => - index === bucketColumnIndex && cell.type === 'text' - ? { - ...cell, - content: { - ...cell.content, - text: getResourceBucketName(cell.content.text ?? ''), - }, - } - : cell - ), - })); - - return DefaultDataTable ? ( - - ) : null; -}; - const { StorageBrowser } = createStorageBrowser({ - components: { - DataTable: ResourceBucketDataTable, - }, config: createAmplifyAuthAdapter(), }); @@ -142,7 +89,6 @@ const storageBrowserDisplayText: StorageBrowserDisplayText = { tableColumnNameHeader: 'Nombre', tableColumnSizeHeader: 'Tamaño', tableColumnTypeHeader: 'Tipo', - getTitle: ({ current, key }) => key || getResourceBucketName(current?.bucket ?? ''), getActionListItemLabel: (key = '') => { const labels: Record = { Copy: 'Copiar', From 47fef5c40b7bb22146717d17b44b6ff740fcecc5 Mon Sep 17 00:00:00 2001 From: rcarvajalp Date: Mon, 11 May 2026 20:14:45 -0600 Subject: [PATCH 08/19] Revert "Merge pull request #4 from rcarvajalp/codex/locate-buckets-without-prefixes-or-suffixes" This reverts commit 2b5b6eb3af63cba188a2c9021730ffe313625bf5, reversing changes made to 512a08da91d5b9c6c787fc4ebb3bb71ba97d94bd. --- amplify/backend.ts | 7 +------ amplify/storage/resource.ts | 7 ++----- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/amplify/backend.ts b/amplify/backend.ts index 6e9e694..0c49596 100644 --- a/amplify/backend.ts +++ b/amplify/backend.ts @@ -1,11 +1,6 @@ import { defineBackend } from '@aws-amplify/backend'; import { auth } from './auth/resource'; -import { - secondaryStorage, - secondaryStorageBucketName, - storage, - storageBucketName, -} from './storage/resource'; +import { storage, secondaryStorage } from './storage/resource'; /** * @see https://docs.amplify.aws/react/build-a-backend/ to add storage, functions, and more diff --git a/amplify/storage/resource.ts b/amplify/storage/resource.ts index fa1eef0..8dd2ab6 100644 --- a/amplify/storage/resource.ts +++ b/amplify/storage/resource.ts @@ -1,10 +1,7 @@ import { defineStorage } from '@aws-amplify/backend'; -export const storageBucketName = 'frauden-bucket'; -export const secondaryStorageBucketName = 'frauden-expedientes'; - export const storage = defineStorage({ - name: storageBucketName, + name: 'frauden-bucket', isDefault: true, access: (allow) => ({ 'doctrina/*': [ @@ -27,7 +24,7 @@ export const storage = defineStorage({ }); export const secondaryStorage = defineStorage({ - name: secondaryStorageBucketName, + name: 'frauden-expedientes', access: (allow) => ({ 'publico/*': [allow.authenticated.to(['read', 'write', 'delete'])], 'confidencial/*': [allow.groups(['admin']).to(['read', 'write', 'delete'])], From 7fa26ae5567371ab55dc8bee63af7a81b89a107e Mon Sep 17 00:00:00 2001 From: rcarvajalp Date: Mon, 11 May 2026 20:19:38 -0600 Subject: [PATCH 09/19] quitar la palabra clave bucket del nombre del storage --- amplify/storage/resource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amplify/storage/resource.ts b/amplify/storage/resource.ts index 8dd2ab6..5ad5951 100644 --- a/amplify/storage/resource.ts +++ b/amplify/storage/resource.ts @@ -1,7 +1,7 @@ import { defineStorage } from '@aws-amplify/backend'; export const storage = defineStorage({ - name: 'frauden-bucket', + name: 'frauden', isDefault: true, access: (allow) => ({ 'doctrina/*': [ From dd647df551322a7c8ff0aa25388b2a35bb290c1b Mon Sep 17 00:00:00 2001 From: rcarvajalp <166424809+rcarvajalp@users.noreply.github.com> Date: Mon, 11 May 2026 20:41:50 -0600 Subject: [PATCH 10/19] Tighten hero title layout --- src/App.css | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/App.css b/src/App.css index 3834858..82fec7c 100644 --- a/src/App.css +++ b/src/App.css @@ -138,8 +138,8 @@ body::before { .hero-card { position: relative; overflow: hidden; - margin: 1.25rem 0; - padding: clamp(1.5rem, 4vw, 3.75rem); + margin: 1rem 0; + padding: clamp(1rem, 2.5vw, 2.25rem) clamp(1.25rem, 4vw, 3.75rem); color: white; background: linear-gradient(135deg, rgba(16, 25, 54, 0.94), rgba(31, 45, 95, 0.88)), @@ -163,7 +163,7 @@ body::before { .hero-card > div { position: relative; z-index: 1; - max-width: 50rem; + width: 100%; } .hero-card .eyebrow { @@ -171,7 +171,8 @@ body::before { } .hero-card h1 { - max-width: 48rem; + width: 100%; + max-width: none; margin: 0; font-size: clamp(2rem, 5vw, 4.75rem); line-height: 0.96; From 648c17a13a2e6bed91f645834c07b0ced30c01d5 Mon Sep 17 00:00:00 2001 From: rcarvajalp <166424809+rcarvajalp@users.noreply.github.com> Date: Mon, 11 May 2026 22:08:35 -0600 Subject: [PATCH 11/19] Show storage bucket friendly names --- amplify/backend.ts | 7 +---- src/App.css | 23 ++++++++++------ src/App.tsx | 66 ++++++++++++++++++++++++++-------------------- 3 files changed, 53 insertions(+), 43 deletions(-) diff --git a/amplify/backend.ts b/amplify/backend.ts index 6e9e694..4b1250e 100644 --- a/amplify/backend.ts +++ b/amplify/backend.ts @@ -1,11 +1,6 @@ import { defineBackend } from '@aws-amplify/backend'; import { auth } from './auth/resource'; -import { - secondaryStorage, - secondaryStorageBucketName, - storage, - storageBucketName, -} from './storage/resource'; +import { secondaryStorage, storage } from './storage/resource'; /** * @see https://docs.amplify.aws/react/build-a-backend/ to add storage, functions, and more diff --git a/src/App.css b/src/App.css index 3834858..0564c8e 100644 --- a/src/App.css +++ b/src/App.css @@ -1,7 +1,13 @@ :root { color-scheme: light; font-family: - Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; --frauden-navy: #101936; --frauden-navy-soft: #1f2d5f; --frauden-blue: #26366f; @@ -47,7 +53,7 @@ body::before { inset: 0; z-index: -1; pointer-events: none; - content: ""; + content: ''; background: linear-gradient(115deg, transparent 0 62%, rgba(16, 25, 54, 0.08) 62% 100%), radial-gradient(circle at 86% 10%, rgba(38, 54, 111, 0.14), transparent 20rem); @@ -138,8 +144,8 @@ body::before { .hero-card { position: relative; overflow: hidden; - margin: 1.25rem 0; - padding: clamp(1.5rem, 4vw, 3.75rem); + margin: 1rem 0; + padding: clamp(1rem, 2.5vw, 2.25rem) clamp(1.25rem, 4vw, 3.75rem); color: white; background: linear-gradient(135deg, rgba(16, 25, 54, 0.94), rgba(31, 45, 95, 0.88)), @@ -154,7 +160,7 @@ body::before { right: -10%; width: 27rem; height: 27rem; - content: ""; + content: ''; background: radial-gradient(circle, rgba(255, 255, 255, 0.2), transparent 62%); border: 1px solid rgba(255, 255, 255, 0.12); border-radius: 999px; @@ -163,7 +169,7 @@ body::before { .hero-card > div { position: relative; z-index: 1; - max-width: 50rem; + width: 100%; } .hero-card .eyebrow { @@ -171,7 +177,8 @@ body::before { } .hero-card h1 { - max-width: 48rem; + width: 100%; + max-width: none; margin: 0; font-size: clamp(2rem, 5vw, 4.75rem); line-height: 0.96; @@ -199,7 +206,7 @@ body::before { border-radius: 0.85rem; } -.storage-card :is(.amplify-button--primary, [data-variation="primary"]) { +.storage-card :is(.amplify-button--primary, [data-variation='primary']) { background: linear-gradient(135deg, var(--frauden-navy), var(--frauden-blue)); border-color: transparent; } diff --git a/src/App.tsx b/src/App.tsx index 50c7f44..a8739c0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,23 +18,13 @@ Amplify.configure(config); I18n.putVocabularies(translations); I18n.setLanguage('es'); -const resourceBucketNames = ['frauden-bucket', 'frauden-expedientes'] as const; - -const getResourceBucketName = (bucketName: string) => { - const comparableBucketName = bucketName.toLowerCase().replace(/[^a-z0-9]/g, ''); - - return ( - resourceBucketNames.find((resourceBucketName) => { - const comparableResourceBucketName = resourceBucketName - .toLowerCase() - .replace(/[^a-z0-9]/g, ''); - - return ( - bucketName === resourceBucketName || - comparableBucketName.includes(comparableResourceBucketName) - ); - }) ?? bucketName +const getBucketFriendlyName = (bucketName: string) => { + const buckets = Amplify.getConfig().Storage?.S3?.buckets; + const bucketEntry = Object.entries(buckets ?? {}).find( + ([, { bucketName: configuredBucketName }]) => configuredBucketName === bucketName ); + + return bucketEntry?.[0] ?? bucketName; }; const DefaultDataTable = componentsDefault.DataTable; @@ -43,22 +33,40 @@ type DataTableProps = ComponentProps>; const ResourceBucketDataTable = ({ headers, rows, ...props }: DataTableProps) => { const bucketColumnIndex = headers.findIndex(({ key }) => key === 'bucket'); + const folderColumnIndex = headers.findIndex(({ key }) => key === 'folder'); const displayRows = - bucketColumnIndex === -1 + bucketColumnIndex === -1 && folderColumnIndex === -1 ? rows : rows.map((row) => ({ ...row, - content: row.content.map((cell, index) => - index === bucketColumnIndex && cell.type === 'text' - ? { - ...cell, - content: { - ...cell.content, - text: getResourceBucketName(cell.content.text ?? ''), - }, - } - : cell - ), + content: row.content.map((cell, index) => { + const isBucketColumn = index === bucketColumnIndex; + const isFolderColumn = index === folderColumnIndex; + + if (!isBucketColumn && !isFolderColumn) return cell; + + if (cell.type === 'text') { + return { + ...cell, + content: { + ...cell.content, + text: getBucketFriendlyName(cell.content.text ?? ''), + }, + }; + } + + if (cell.type === 'button') { + return { + ...cell, + content: { + ...cell.content, + label: getBucketFriendlyName(cell.content.label ?? ''), + }, + }; + } + + return cell; + }), })); return DefaultDataTable ? ( @@ -142,7 +150,7 @@ const storageBrowserDisplayText: StorageBrowserDisplayText = { tableColumnNameHeader: 'Nombre', tableColumnSizeHeader: 'Tamaño', tableColumnTypeHeader: 'Tipo', - getTitle: ({ current, key }) => key || getResourceBucketName(current?.bucket ?? ''), + getTitle: ({ current, key }) => key || getBucketFriendlyName(current?.bucket ?? ''), getActionListItemLabel: (key = '') => { const labels: Record = { Copy: 'Copiar', From 1db3610757386831a35a1e2b685e15b1c1970693 Mon Sep 17 00:00:00 2001 From: rcarvajalp <166424809+rcarvajalp@users.noreply.github.com> Date: Mon, 11 May 2026 22:25:56 -0600 Subject: [PATCH 12/19] Fix storage browser data table override --- src/App.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/App.tsx b/src/App.tsx index 0ecafd6..48218ee 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,12 @@ import { + componentsDefault, createAmplifyAuthAdapter, createStorageBrowser, } from '@aws-amplify/ui-react-storage/browser'; import '@aws-amplify/ui-react/styles.css'; import '@aws-amplify/ui-react-storage/styles.css'; import './App.css'; +import type { ComponentProps } from 'react'; import config from '../amplify_outputs.json'; import { Amplify } from 'aws-amplify'; @@ -74,6 +76,10 @@ const ResourceBucketDataTable = ({ headers, rows, ...props }: DataTableProps) => const { StorageBrowser } = createStorageBrowser({ config: createAmplifyAuthAdapter(), + components: { + ...componentsDefault, + DataTable: ResourceBucketDataTable, + }, }); type StorageBrowserDisplayText = NonNullable[0]['displayText']>; From a3884c767525c461d9f768b0a4e24d238a17f651 Mon Sep 17 00:00:00 2001 From: rcarvajalp <166424809+rcarvajalp@users.noreply.github.com> Date: Mon, 11 May 2026 23:00:55 -0600 Subject: [PATCH 13/19] Fix storage browser friendly bucket labels --- src/App.tsx | 101 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 72 insertions(+), 29 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 48218ee..cc97e0a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,56 +18,99 @@ Amplify.configure(config); I18n.putVocabularies(translations); I18n.setLanguage('es'); +type AmplifyBucketInfo = { + bucketName?: string; +}; + const getBucketFriendlyName = (bucketName: string) => { - const buckets = Amplify.getConfig().Storage?.S3?.buckets; - const bucketEntry = Object.entries(buckets ?? {}).find( - ([, { bucketName: configuredBucketName }]) => configuredBucketName === bucketName + if (!bucketName) return bucketName; + + const storageConfig = Amplify.getConfig().Storage?.S3; + const buckets = storageConfig?.buckets ?? {}; + const bucketEntry = Object.entries(buckets).find( + ([, bucketInfo]: [string, AmplifyBucketInfo]) => bucketInfo?.bucketName === bucketName ); + // Amplify Gen 2 stores the user-facing storage resource name in the + // `amplify:friendly-name` tag and emits that same value as the key in + // `Storage.S3.buckets` inside amplify_outputs.json. Fall back to the real + // bucket name if the bucket is not present in the generated outputs. return bucketEntry?.[0] ?? bucketName; }; const DefaultDataTable = componentsDefault.DataTable; type DataTableProps = ComponentProps>; +type DataTableRow = DataTableProps['rows'][number]; +type DataTableCell = DataTableRow['content'][number]; + +const getTextCellValue = (cell: DataTableCell | undefined) => { + if (cell?.type === 'text') return cell.content.text; + if (cell?.type === 'button') return cell.content.label; + + return undefined; +}; + +const withCellDisplayText = (cell: DataTableCell, displayText: string): DataTableCell => { + if (cell.type === 'text') { + return { + ...cell, + content: { + ...cell.content, + text: displayText, + }, + }; + } + + if (cell.type === 'button') { + return { + ...cell, + content: { + ...cell.content, + label: displayText, + }, + }; + } + + return cell; +}; const ResourceBucketDataTable = ({ headers, rows, ...props }: DataTableProps) => { const bucketColumnIndex = headers.findIndex(({ key }) => key === 'bucket'); const folderColumnIndex = headers.findIndex(({ key }) => key === 'folder'); - const displayRows = - bucketColumnIndex === -1 && folderColumnIndex === -1 - ? rows - : rows.map((row) => ({ + const isLocationsTable = + bucketColumnIndex !== -1 && + folderColumnIndex !== -1 && + headers.some(({ key }) => key === 'permission'); + + const displayRows = !isLocationsTable + ? rows + : rows.map((row) => { + const bucketName = getTextCellValue(row.content[bucketColumnIndex]); + const friendlyBucketName = getBucketFriendlyName(bucketName ?? ''); + + return { ...row, content: row.content.map((cell, index) => { - const isBucketColumn = index === bucketColumnIndex; - const isFolderColumn = index === folderColumnIndex; - - if (!isBucketColumn && !isFolderColumn) return cell; - - if (cell.type === 'text') { - return { - ...cell, - content: { - ...cell.content, - text: getBucketFriendlyName(cell.content.text ?? ''), - }, - }; + if (index === bucketColumnIndex) { + return withCellDisplayText(cell, friendlyBucketName); } - if (cell.type === 'button') { - return { - ...cell, - content: { - ...cell.content, - label: getBucketFriendlyName(cell.content.label ?? ''), - }, - }; + const folderDisplayValue = getTextCellValue(cell); + + // In the Locations view, Amplify uses the real bucket name as the + // folder button label only for bucket-level locations where there is + // no prefix. Prefix rows such as `doctrina/` must stay untouched; + // otherwise the destination/location data can become visually + // misleading and hard to navigate. + if (index === folderColumnIndex && folderDisplayValue === bucketName) { + return withCellDisplayText(cell, friendlyBucketName); } return cell; }), - })); + }; + }); return DefaultDataTable ? ( From b9ff508cf462b26cd8d32c9f58059327dc024ce7 Mon Sep 17 00:00:00 2001 From: rcarvajalp <166424809+rcarvajalp@users.noreply.github.com> Date: Mon, 11 May 2026 23:18:09 -0600 Subject: [PATCH 14/19] Show real bucket names in storage browser --- src/App.tsx | 108 +--------------------------------------------------- 1 file changed, 2 insertions(+), 106 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index cc97e0a..fe9f635 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,8 +6,6 @@ import { import '@aws-amplify/ui-react/styles.css'; import '@aws-amplify/ui-react-storage/styles.css'; import './App.css'; -import type { ComponentProps } from 'react'; - import config from '../amplify_outputs.json'; import { Amplify } from 'aws-amplify'; import { I18n } from 'aws-amplify/utils'; @@ -18,111 +16,9 @@ Amplify.configure(config); I18n.putVocabularies(translations); I18n.setLanguage('es'); -type AmplifyBucketInfo = { - bucketName?: string; -}; - -const getBucketFriendlyName = (bucketName: string) => { - if (!bucketName) return bucketName; - - const storageConfig = Amplify.getConfig().Storage?.S3; - const buckets = storageConfig?.buckets ?? {}; - const bucketEntry = Object.entries(buckets).find( - ([, bucketInfo]: [string, AmplifyBucketInfo]) => bucketInfo?.bucketName === bucketName - ); - - // Amplify Gen 2 stores the user-facing storage resource name in the - // `amplify:friendly-name` tag and emits that same value as the key in - // `Storage.S3.buckets` inside amplify_outputs.json. Fall back to the real - // bucket name if the bucket is not present in the generated outputs. - return bucketEntry?.[0] ?? bucketName; -}; - -const DefaultDataTable = componentsDefault.DataTable; - -type DataTableProps = ComponentProps>; -type DataTableRow = DataTableProps['rows'][number]; -type DataTableCell = DataTableRow['content'][number]; - -const getTextCellValue = (cell: DataTableCell | undefined) => { - if (cell?.type === 'text') return cell.content.text; - if (cell?.type === 'button') return cell.content.label; - - return undefined; -}; - -const withCellDisplayText = (cell: DataTableCell, displayText: string): DataTableCell => { - if (cell.type === 'text') { - return { - ...cell, - content: { - ...cell.content, - text: displayText, - }, - }; - } - - if (cell.type === 'button') { - return { - ...cell, - content: { - ...cell.content, - label: displayText, - }, - }; - } - - return cell; -}; - -const ResourceBucketDataTable = ({ headers, rows, ...props }: DataTableProps) => { - const bucketColumnIndex = headers.findIndex(({ key }) => key === 'bucket'); - const folderColumnIndex = headers.findIndex(({ key }) => key === 'folder'); - const isLocationsTable = - bucketColumnIndex !== -1 && - folderColumnIndex !== -1 && - headers.some(({ key }) => key === 'permission'); - - const displayRows = !isLocationsTable - ? rows - : rows.map((row) => { - const bucketName = getTextCellValue(row.content[bucketColumnIndex]); - const friendlyBucketName = getBucketFriendlyName(bucketName ?? ''); - - return { - ...row, - content: row.content.map((cell, index) => { - if (index === bucketColumnIndex) { - return withCellDisplayText(cell, friendlyBucketName); - } - - const folderDisplayValue = getTextCellValue(cell); - - // In the Locations view, Amplify uses the real bucket name as the - // folder button label only for bucket-level locations where there is - // no prefix. Prefix rows such as `doctrina/` must stay untouched; - // otherwise the destination/location data can become visually - // misleading and hard to navigate. - if (index === folderColumnIndex && folderDisplayValue === bucketName) { - return withCellDisplayText(cell, friendlyBucketName); - } - - return cell; - }), - }; - }); - - return DefaultDataTable ? ( - - ) : null; -}; - const { StorageBrowser } = createStorageBrowser({ config: createAmplifyAuthAdapter(), - components: { - ...componentsDefault, - DataTable: ResourceBucketDataTable, - }, + components: componentsDefault, }); type StorageBrowserDisplayText = NonNullable[0]['displayText']>; @@ -194,7 +90,7 @@ const storageBrowserDisplayText: StorageBrowserDisplayText = { tableColumnNameHeader: 'Nombre', tableColumnSizeHeader: 'Tamaño', tableColumnTypeHeader: 'Tipo', - getTitle: ({ current, key }) => key || getBucketFriendlyName(current?.bucket ?? ''), + getTitle: ({ current, key }) => key || current?.bucket || '', getActionListItemLabel: (key = '') => { const labels: Record = { Copy: 'Copiar', From fa516b89e59d74f61ccbfb6a4eeafbb6f7f63f96 Mon Sep 17 00:00:00 2001 From: rcarvajalp <166424809+rcarvajalp@users.noreply.github.com> Date: Tue, 12 May 2026 10:00:30 -0600 Subject: [PATCH 15/19] Hide bucket column in locations table --- src/App.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/App.css b/src/App.css index 0564c8e..fd8fec8 100644 --- a/src/App.css +++ b/src/App.css @@ -221,6 +221,10 @@ body::before { background: #f4f6f9; } +.storage-card [data-testid='LOCATIONS_VIEW'] .storage-browser__table :is(th, td):nth-child(2) { + display: none; +} + .amplify-authenticator { min-height: 100vh; background: From bed39c18a1f2e861a64a4c37c4afa72d78f3c5cc Mon Sep 17 00:00:00 2001 From: rcarvajalp <166424809+rcarvajalp@users.noreply.github.com> Date: Tue, 12 May 2026 10:26:46 -0600 Subject: [PATCH 16/19] Hide bucket column in folders table --- src/App.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/App.css b/src/App.css index fd8fec8..9ec5d70 100644 --- a/src/App.css +++ b/src/App.css @@ -221,7 +221,8 @@ body::before { background: #f4f6f9; } -.storage-card [data-testid='LOCATIONS_VIEW'] .storage-browser__table :is(th, td):nth-child(2) { +.storage-card [data-testid='LOCATIONS_VIEW'] :is(.amplify-storage-browser__table, .storage-browser__table) + :is(th, td):nth-child(2) { display: none; } From a2a9d2aec6f89d6a884a99417f4afc5b7ee27de6 Mon Sep 17 00:00:00 2001 From: rcarvajalp <166424809+rcarvajalp@users.noreply.github.com> Date: Tue, 12 May 2026 10:53:02 -0600 Subject: [PATCH 17/19] Add knowledge sync button --- src/App.css | 60 +++++++++++++++++++++++++++++++++ src/App.tsx | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) diff --git a/src/App.css b/src/App.css index 9ec5d70..605fc59 100644 --- a/src/App.css +++ b/src/App.css @@ -193,6 +193,66 @@ body::before { line-height: 1.7; } +.knowledge-sync-panel { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: center; + justify-content: space-between; + margin-bottom: 1rem; + padding: 1rem 1.1rem; + background: linear-gradient(135deg, rgba(16, 25, 54, 0.06), rgba(201, 204, 209, 0.18)); + border: 1px solid rgba(16, 25, 54, 0.08); + border-radius: 1.25rem; +} + +.knowledge-sync-panel p { + margin: 0; + color: var(--frauden-silver-dark); + line-height: 1.5; +} + +.knowledge-sync-actions { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + align-items: center; + justify-content: flex-end; +} + +.knowledge-sync-button.amplify-button { + min-height: 3rem; + padding-inline: 1.25rem; + font-weight: 700; + box-shadow: 0 14px 30px rgba(16, 25, 54, 0.18); +} + +.knowledge-sync-status { + max-width: 28rem; + padding: 0.7rem 0.85rem; + font-size: 0.92rem; + font-weight: 700; + border-radius: 0.85rem; +} + +.knowledge-sync-status--error { + color: #8a1f11; + background: #fff0ed; + border: 1px solid #f3b3a8; +} + +.knowledge-sync-status--info { + color: var(--frauden-navy); + background: #eef4ff; + border: 1px solid #c7d8f8; +} + +.knowledge-sync-status--success { + color: #115f35; + background: #edfff5; + border: 1px solid #a8e2c0; +} + .storage-card { padding: clamp(0.85rem, 2vw, 1.4rem); background: var(--frauden-surface); diff --git a/src/App.tsx b/src/App.tsx index fe9f635..c0c776f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,8 +6,10 @@ import { import '@aws-amplify/ui-react/styles.css'; import '@aws-amplify/ui-react-storage/styles.css'; import './App.css'; +import { useState } from 'react'; import config from '../amplify_outputs.json'; import { Amplify } from 'aws-amplify'; +import { fetchAuthSession } from 'aws-amplify/auth'; import { I18n } from 'aws-amplify/utils'; import { Authenticator, Button, translations } from '@aws-amplify/ui-react'; import fraudenLogo from './assets/frauden-logo.svg'; @@ -23,6 +25,13 @@ const { StorageBrowser } = createStorageBrowser({ type StorageBrowserDisplayText = NonNullable[0]['displayText']>; +type KnowledgeSyncStatus = { + message: string; + type: 'error' | 'info' | 'success'; +} | null; + +const syncKnowledgeEndpoint = import.meta.env.VITE_SYNC_KNOWLEDGE_LAMBDA_URL?.trim(); + const authenticatorComponents = { Header() { return ( @@ -33,6 +42,92 @@ const authenticatorComponents = { }, }; +function KnowledgeSyncButton() { + const [isSyncing, setIsSyncing] = useState(false); + const [syncStatus, setSyncStatus] = useState(null); + + const handleKnowledgeSync = async () => { + if (!syncKnowledgeEndpoint) { + setSyncStatus({ + type: 'error', + message: + 'Configura VITE_SYNC_KNOWLEDGE_LAMBDA_URL con la URL de la Lambda para sincronizar.', + }); + return; + } + + setIsSyncing(true); + setSyncStatus({ type: 'info', message: 'Solicitando sincronización de conocimiento...' }); + + try { + const session = await fetchAuthSession(); + const idToken = session.tokens?.idToken?.toString(); + + if (!idToken) { + throw new Error('No se encontró una sesión autenticada para invocar la Lambda.'); + } + + const response = await fetch(syncKnowledgeEndpoint, { + method: 'POST', + headers: { + Authorization: `Bearer ${idToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ action: 'syncKnowledgeBase' }), + }); + + if (!response.ok) { + const errorMessage = await response.text(); + throw new Error( + errorMessage || `La Lambda respondió con estado ${response.status}. Intenta nuevamente.` + ); + } + + setSyncStatus({ + type: 'success', + message: 'Sincronización de conocimiento iniciada correctamente.', + }); + } catch (error) { + setSyncStatus({ + type: 'error', + message: + error instanceof Error + ? error.message + : 'No se pudo iniciar la sincronización de conocimiento.', + }); + } finally { + setIsSyncing(false); + } + }; + + return ( +
+
+ Base de conocimiento +

Invoca la Lambda de sincronización usando tu sesión autenticada.

+
+
+ + {syncStatus ? ( +
+ {syncStatus.message} +
+ ) : null} +
+
+ ); +} + const storageBrowserDisplayText: StorageBrowserDisplayText = { LocationsView: { title: 'Inicio', @@ -272,6 +367,7 @@ function App() {
+
From 7d66a4d4cc349cdc7d3917c5d52246719aaafe90 Mon Sep 17 00:00:00 2001 From: rcarvajalp <166424809+rcarvajalp@users.noreply.github.com> Date: Sat, 16 May 2026 18:55:57 -0600 Subject: [PATCH 18/19] Add knowledge sync button tooltip --- src/App.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index c0c776f..5873c0d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -31,6 +31,8 @@ type KnowledgeSyncStatus = { } | null; const syncKnowledgeEndpoint = import.meta.env.VITE_SYNC_KNOWLEDGE_LAMBDA_URL?.trim(); +const syncKnowledgeTooltip = + 'Cuando agregues nuevos documentos o elimines debes sincronizar la base de conocimiento para cargar la nueva información'; const authenticatorComponents = { Header() { @@ -104,12 +106,12 @@ function KnowledgeSyncButton() {
Base de conocimiento -

Invoca la Lambda de sincronización usando tu sesión autenticada.