From a23d4f015aa32e00fd96b8f49bccd37efa1200b2 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Tue, 2 Dec 2025 13:53:35 +0100 Subject: [PATCH] feat: add S3 support to download button and PDF preview components (#7271) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add S3 support to download button and PDF preview components Add S3 object and s3:// URL support to AppDownload and AppPdf components, following the same pattern used in AppImage component. Both components now: - Handle partial S3 objects with storage and presigned URL support - Handle s3:// URL format - Construct proper API endpoints for S3 file downloads Fixes #7240 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Co-authored-by: windmill-internal-app[bot] * handle policy + fix s3 picker --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Co-authored-by: windmill-internal-app[bot] --- frontend/src/lib/components/EditorBar.svelte | 4 +- .../src/lib/components/S3FilePicker.svelte | 17 +++--- .../src/lib/components/S3ObjectPicker.svelte | 2 +- .../components/display/AppDownload.svelte | 48 ++++++++++++++- .../apps/components/display/AppImage.svelte | 61 +++++++------------ .../apps/components/display/AppPdf.svelte | 45 +++++++++++++- .../lib/components/apps/editor/appPolicy.ts | 11 +++- .../lib/components/apps/editor/appUtilsS3.ts | 57 ++++++++++++++++- .../apps/editor/component/components.ts | 9 ++- .../settingsPanel/InputsSpecEditor.svelte | 8 ++- .../inputEditor/StaticInputEditor.svelte | 18 +++--- .../common/fileUpload/S3ArgInput.svelte | 10 +-- .../triggers/http/RouteEditorInner.svelte | 3 +- 13 files changed, 215 insertions(+), 78 deletions(-) diff --git a/frontend/src/lib/components/EditorBar.svelte b/frontend/src/lib/components/EditorBar.svelte index 1b3d6ee8a1..315bfeba75 100644 --- a/frontend/src/lib/components/EditorBar.svelte +++ b/frontend/src/lib/components/EditorBar.svelte @@ -757,8 +757,8 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS { - let s = `'${formatS3Object(s3obj.detail)}'` + onSelectAndClose={(s3obj) => { + let s = `'${formatS3Object(s3obj)}'` if (lang === 'duckdb') { editor?.insertAtCursor(`SELECT * FROM ${s}`) } else if (lang === 'python3') { diff --git a/frontend/src/lib/components/S3FilePicker.svelte b/frontend/src/lib/components/S3FilePicker.svelte index cc061dc18c..16eddb9f1a 100644 --- a/frontend/src/lib/components/S3FilePicker.svelte +++ b/frontend/src/lib/components/S3FilePicker.svelte @@ -2,7 +2,7 @@ import { emptyString, type S3Object } from '$lib/utils' import { Button, Drawer } from './common' import DrawerContent from './common/drawer/DrawerContent.svelte' - import { createEventDispatcher, tick, untrack } from 'svelte' + import { tick, untrack } from 'svelte' import S3FilePickerInner from './S3FilePickerInner.svelte' import Select from './select/Select.svelte' import { FileUp } from 'lucide-svelte' @@ -10,11 +10,6 @@ import { SettingService } from '$lib/gen' import { workspaceStore } from '$lib/stores' - let dispatch = createEventDispatcher<{ - close: { s3: string; storage: string | undefined } | undefined - selectAndClose: { s3: string; storage: string | undefined } - }>() - interface Props { fromWorkspaceSettings?: boolean readOnlyMode: boolean @@ -22,6 +17,8 @@ selectedFileKey?: { s3: string; storage?: string } | undefined folderOnly?: boolean regexFilter?: RegExp | undefined + onClose?: () => void + onSelectAndClose?: (selected: { s3: string; storage: string | undefined }) => void } let { @@ -30,7 +27,9 @@ initialFileKey = $bindable(undefined), selectedFileKey = $bindable(undefined), folderOnly = false, - regexFilter = undefined + regexFilter = undefined, + onClose, + onSelectAndClose }: Props = $props() let drawer: Drawer | undefined = $state() @@ -76,7 +75,7 @@ { - dispatch('close') + onClose?.() s3FilePickerInner?.close?.() }} size="1200px" @@ -93,7 +92,7 @@ { - dispatch('selectAndClose', e.detail) + onSelectAndClose?.(e.detail) drawer?.closeDrawer?.() }} {fromWorkspaceSettings} diff --git a/frontend/src/lib/components/S3ObjectPicker.svelte b/frontend/src/lib/components/S3ObjectPicker.svelte index 6a9d1c9a86..c70983ee83 100644 --- a/frontend/src/lib/components/S3ObjectPicker.svelte +++ b/frontend/src/lib/components/S3ObjectPicker.svelte @@ -39,7 +39,7 @@ { + onClose={() => { rawValue = JSON.stringify(value, null, 2) editor?.setCode(rawValue) }} diff --git a/frontend/src/lib/components/apps/components/display/AppDownload.svelte b/frontend/src/lib/components/apps/components/display/AppDownload.svelte index dbc732bd06..0f38afe1db 100644 --- a/frontend/src/lib/components/apps/components/display/AppDownload.svelte +++ b/frontend/src/lib/components/apps/components/display/AppDownload.svelte @@ -12,6 +12,8 @@ import ComponentErrorHandler from '../helpers/ComponentErrorHandler.svelte' import ResolveStyle from '../helpers/ResolveStyle.svelte' import AlignWrapper from '../helpers/AlignWrapper.svelte' + import { userStore } from '$lib/stores' + import { isPartialS3Object, getS3File } from '../../editor/appUtilsS3' interface Props { id: string @@ -37,7 +39,8 @@ initConfig(components['downloadcomponent'].initialData.configuration, configuration) ) - const { app, worldStore } = getContext('AppViewerContext') + const { app, worldStore, appPath, workspace, isEditor } = + getContext('AppViewerContext') //used so that we can count number of outputs setup for first refresh initOutput($worldStore, id, {}) @@ -45,6 +48,10 @@ let beforeIconComponent: any = $state() let afterIconComponent: any = $state() + let downloadUrl: string | undefined = $state(undefined) + + let token = getContext<{ token?: string }>('AuthToken') + async function handleBeforeIcon() { if (resolvedConfig.beforeIcon) { beforeIconComponent = await loadIcon( @@ -69,6 +76,36 @@ } } + async function loadSource() { + if (isPartialS3Object(resolvedConfig.source)) { + downloadUrl = await getS3File({ + source: resolvedConfig.source.s3, + storage: resolvedConfig.source.storage, + presigned: resolvedConfig.source.presigned, + appPath: $appPath, + username: $userStore?.username, + workspace, + token: token?.token, + isEditor, + configuration + }) + } else if (resolvedConfig.source && typeof resolvedConfig.source !== 'string') { + throw new Error('Invalid source object' + typeof resolvedConfig.source) + } else if (resolvedConfig.source?.startsWith('s3://')) { + downloadUrl = await getS3File({ + source: resolvedConfig.source?.replace('s3://', ''), + appPath: $appPath, + username: $userStore?.username, + workspace, + token: token?.token, + isEditor, + configuration + }) + } else { + downloadUrl = transformBareBase64IfNecessary(resolvedConfig.source) + } + } + let css = $state(initCss($app.css?.downloadcomponent, customCss)) $effect(() => { resolvedConfig.beforeIcon && beforeIconComponent && untrack(() => handleBeforeIcon()) @@ -76,6 +113,9 @@ $effect(() => { resolvedConfig.afterIcon && afterIconComponent && untrack(() => handleAfterIcon()) }) + $effect(() => { + resolvedConfig && loadSource() + }) @@ -102,7 +142,9 @@ {#if render}