From 8dbe0fa6446a34bd60484f1b5ac828ff9f892735 Mon Sep 17 00:00:00 2001 From: Alexander Petric Date: Thu, 6 Mar 2025 11:53:53 +0100 Subject: [PATCH] feat(frontend): pick image from workspace storage bucket (#5382) * feat(frontend): pick image from workspace storage bucket * also upload * update policy for unauthed s3 download * sqlx prep * sqlx prep * force policy * no need for ee * image picker * Update frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * field not needed * feature flag * filter for image files --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- ...f2a5f75c195c7bf916a9b55be99e5cacdf44f.json | 24 ++++ backend/windmill-api/openapi.yaml | 11 +- backend/windmill-api/src/apps.rs | 108 ++++++++++++++---- .../apps/components/display/AppImage.svelte | 84 +++++++++++--- .../apps/components/helpers/InputValue.svelte | 5 +- .../components/inputs/AppS3FileInput.svelte | 11 +- .../apps/editor/AppEditorHeader.svelte | 17 ++- .../lib/components/apps/editor/appUtilsS3.ts | 11 ++ .../apps/editor/component/components.ts | 5 + .../settingsPanel/InputsSpecEditor.svelte | 68 ++++++++++- .../settingsPanel/InputsSpecsEditor.svelte | 1 + .../inputEditor/UploadInputEditor.svelte | 74 ++++++++---- frontend/src/lib/components/apps/inputType.ts | 21 ++++ frontend/src/lib/components/apps/types.ts | 4 +- .../common/fileUpload/FileUpload.svelte | 2 + 15 files changed, 367 insertions(+), 79 deletions(-) create mode 100644 backend/.sqlx/query-a06d8e9ac6e6a8d6b4b61bdf89ef2a5f75c195c7bf916a9b55be99e5cacdf44f.json diff --git a/backend/.sqlx/query-a06d8e9ac6e6a8d6b4b61bdf89ef2a5f75c195c7bf916a9b55be99e5cacdf44f.json b/backend/.sqlx/query-a06d8e9ac6e6a8d6b4b61bdf89ef2a5f75c195c7bf916a9b55be99e5cacdf44f.json new file mode 100644 index 0000000000..52069fd432 --- /dev/null +++ b/backend/.sqlx/query-a06d8e9ac6e6a8d6b4b61bdf89ef2a5f75c195c7bf916a9b55be99e5cacdf44f.json @@ -0,0 +1,24 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS (\n SELECT 1 FROM app\n WHERE path = $1\n AND workspace_id = $2\n AND policy @> jsonb_build_object('allowed_s3_keys', jsonb_build_array(jsonb_build_object('s3_path', $3::text)))::jsonb\n )", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "a06d8e9ac6e6a8d6b4b61bdf89ef2a5f75c195c7bf916a9b55be99e5cacdf44f" +} diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index fba7bce91d..e2da676ce4 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -11398,7 +11398,7 @@ paths: /w/{workspace}/job_helpers/download_s3_file: get: - summary: Download file to S3 bucket + summary: Download file from S3 bucket operationId: fileDownload tags: - helpers @@ -14919,6 +14919,15 @@ components: type: array items: type: object + allowed_s3_keys: + type: array + items: + type: object + properties: + s3_path: + type: string + resource: + type: string execution_mode: type: string enum: [viewer, publisher, anonymous] diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index 58323580b2..d5ac116531 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -225,6 +225,12 @@ pub struct S3Input { file_key_regex: String, } +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct S3Key { + s3_path: String, + resource: String, +} + #[derive(Serialize, Deserialize, Debug, Clone, Default)] pub struct Policy { pub on_behalf_of: Option, @@ -239,6 +245,7 @@ pub struct Policy { pub triggerables_v2: Option>, pub execution_mode: ExecutionMode, pub s3_inputs: Option>, + pub allowed_s3_keys: Option>, } #[derive(Deserialize)] @@ -1563,6 +1570,7 @@ async fn upload_s3_file_from_app( .map(|s| s.split(',').map(|s| s.to_string()).collect()) .unwrap_or_default(), }]), + allowed_s3_keys: None, }) } else { let policy_o = sqlx::query_scalar!( @@ -1873,26 +1881,41 @@ async fn get_on_behalf_authed_from_app( path: &str, w_id: &str, opt_authed: &Option, -) -> Result { - let policy_o = sqlx::query_scalar!( - "SELECT policy from app WHERE path = $1 AND workspace_id = $2", - path, - w_id - ) - .fetch_optional(db) - .await?; - - let policy = policy_o - .map(|p| serde_json::from_value::(p).map_err(to_anyhow)) - .transpose()? - .unwrap_or_else(|| Policy { + force_allowed_s3_keys: Option>, +) -> Result<(ApiAuthed, Policy)> { + let policy = if let Some(force_allowed_s3_keys) = force_allowed_s3_keys { + Policy { execution_mode: ExecutionMode::Viewer, triggerables: None, triggerables_v2: None, on_behalf_of: None, on_behalf_of_email: None, s3_inputs: None, - }); + allowed_s3_keys: Some(force_allowed_s3_keys), + } + } else { + // TODO: improve db query to not return uneeded fields + let policy_o = sqlx::query_scalar!( + "SELECT policy from app WHERE path = $1 AND workspace_id = $2", + path, + w_id + ) + .fetch_optional(db) + .await?; + + policy_o + .map(|p| serde_json::from_value::(p).map_err(to_anyhow)) + .transpose()? + .unwrap_or_else(|| Policy { + execution_mode: ExecutionMode::Viewer, + triggerables: None, + triggerables_v2: None, + on_behalf_of: None, + on_behalf_of_email: None, + s3_inputs: None, + allowed_s3_keys: None, + }) + }; let (username, permissioned_as, email) = get_on_behalf_details_from_policy_and_authed(&policy, &opt_authed).await?; @@ -1901,7 +1924,7 @@ async fn get_on_behalf_authed_from_app( fetch_api_authed_from_permissioned_as(permissioned_as, email, &w_id, &db, Some(username)) .await?; - Ok(on_behalf_authed) + Ok((on_behalf_authed, policy)) } #[cfg(feature = "parquet")] @@ -1911,6 +1934,7 @@ async fn check_if_allowed_to_access_s3_file_from_app( file_key: &str, w_id: &str, path: &str, + policy: &Policy, ) -> Result<()> { // if anonymous, check that the file was the result of an app script ran by an anonymous user in the last 3 hours // otherwise, if logged in, allow any file (TODO: change that when we implement better s3 policy) @@ -1932,7 +1956,10 @@ async fn check_if_allowed_to_access_s3_file_from_app( ) .fetch_one(db) .await? - .unwrap_or(false); + .unwrap_or(false) + + // check if the file is allowed by the allowed_s3_keys policy + || policy.allowed_s3_keys.as_ref().unwrap().iter().any(|key| key.s3_path == file_key); if !allowed { Err(Error::BadRequest("File restricted".to_string())) @@ -1941,21 +1968,46 @@ async fn check_if_allowed_to_access_s3_file_from_app( } } +#[cfg(feature = "parquet")] +#[derive(Deserialize)] +pub struct DownloadFileQueryWithForceViewerAllowedS3Keys { + #[serde(flatten)] + pub file_query: DownloadFileQuery, + pub force_viewer_allowed_s3_keys: Option, +} + #[cfg(feature = "parquet")] async fn download_s3_file_from_app( OptAuthed(opt_authed): OptAuthed, Extension(db): Extension, Path((w_id, path)): Path<(String, StripPath)>, - Query(query): Query, + Query(query): Query, ) -> Result { let path = path.to_path(); - let on_behalf_authed = get_on_behalf_authed_from_app(&db, &path, &w_id, &opt_authed).await?; + let force_viewer_allowed_s3_keys = if let Some(force_viewer_allowed_s3_keys) = + query.force_viewer_allowed_s3_keys.clone() + { + Some(serde_json::from_str::>(&force_viewer_allowed_s3_keys).unwrap_or_default()) + } else { + None + }; - check_if_allowed_to_access_s3_file_from_app(&db, &opt_authed, &query.file_key, &w_id, &path) - .await?; + let (on_behalf_authed, policy) = + get_on_behalf_authed_from_app(&db, &path, &w_id, &opt_authed, force_viewer_allowed_s3_keys) + .await?; - download_s3_file_internal(on_behalf_authed, &db, None, "", &w_id, query).await + check_if_allowed_to_access_s3_file_from_app( + &db, + &opt_authed, + &query.file_query.file_key, + &w_id, + &path, + &policy, + ) + .await?; + + download_s3_file_internal(on_behalf_authed, &db, None, "", &w_id, query.file_query).await } #[cfg(not(feature = "parquet"))] @@ -1974,10 +2026,18 @@ async fn load_s3_file_image_preview_from_app( ) -> Result { let path = path.to_path(); - let on_behalf_authed = get_on_behalf_authed_from_app(&db, &path, &w_id, &opt_authed).await?; + let (on_behalf_authed, policy) = + get_on_behalf_authed_from_app(&db, &path, &w_id, &opt_authed, None).await?; - check_if_allowed_to_access_s3_file_from_app(&db, &opt_authed, &query.file_key, &w_id, &path) - .await?; + check_if_allowed_to_access_s3_file_from_app( + &db, + &opt_authed, + &query.file_key, + &w_id, + &path, + &policy, + ) + .await?; load_image_preview_internal(on_behalf_authed, &db, "", &w_id, query).await } diff --git a/frontend/src/lib/components/apps/components/display/AppImage.svelte b/frontend/src/lib/components/apps/components/display/AppImage.svelte index c61e355074..a7be34ca3a 100644 --- a/frontend/src/lib/components/apps/components/display/AppImage.svelte +++ b/frontend/src/lib/components/apps/components/display/AppImage.svelte @@ -9,18 +9,30 @@ import ResolveConfig from '../helpers/ResolveConfig.svelte' import InitializeComponent from '../helpers/InitializeComponent.svelte' import ResolveStyle from '../helpers/ResolveStyle.svelte' + import { defaultIfEmptyString } from '$lib/utils' + import { userStore } from '$lib/stores' + import { computeS3ImageViewerPolicy } from '../../editor/appUtilsS3' export let id: string export let configuration: RichConfigurations export let customCss: ComponentCustomCSS<'imagecomponent'> | undefined = undefined export let render: boolean + function computeForceViewerPolicies() { + if (!isEditor) { + return undefined + } + const policy = computeS3ImageViewerPolicy(configuration, $app) + return policy + } + const resolvedConfig = initConfig( components['imagecomponent'].initialData.configuration, configuration ) - const { app, worldStore } = getContext('AppViewerContext') + const { app, appPath, worldStore, workspace, isEditor } = + getContext('AppViewerContext') const fit: Record = { cover: 'object-cover', contain: 'object-contain', @@ -31,6 +43,44 @@ initOutput($worldStore, id, {}) let css = initCss($app.css?.imagecomponent, customCss) + + let imageUrl: string | undefined = undefined + + async function getS3Image(source: string | undefined) { + if (!source) return '' + const appPathOrUser = defaultIfEmptyString( + $appPath, + `u/${$userStore?.username ?? 'unknown'}/newapp` + ) + const params = new URLSearchParams() + params.append('file_key', source) + + const forceViewerPolicies = computeForceViewerPolicies() + if (forceViewerPolicies) { + params.append('force_viewer_allowed_s3_keys', JSON.stringify([forceViewerPolicies])) + } + + return `/api/w/${workspace}/apps_u/download_s3_file/${appPathOrUser}?${params.toString()}` + } + + async function loadImage() { + if ( + resolvedConfig.sourceKind === 's3 (workspace storage)' || + resolvedConfig.source?.startsWith('s3://') + ) { + imageUrl = await getS3Image(resolvedConfig.source?.replace('s3://', '')) + } else if (resolvedConfig.sourceKind === 'png encoded as base64') { + imageUrl = 'data:image/png;base64,' + resolvedConfig.source + } else if (resolvedConfig.sourceKind === 'jpeg encoded as base64') { + imageUrl = 'data:image/jpeg;base64,' + resolvedConfig.source + } else if (resolvedConfig.sourceKind === 'svg encoded as base64') { + imageUrl = 'data:image/svg+xml;base64,' + resolvedConfig.source + } else { + imageUrl = resolvedConfig.source + } + } + + $: resolvedConfig && loadImage() @@ -55,23 +105,19 @@ {/each} {#if render} - - {resolvedConfig.altText} + + {#if imageUrl} + {resolvedConfig.altText} + {/if} {/if} diff --git a/frontend/src/lib/components/apps/components/helpers/InputValue.svelte b/frontend/src/lib/components/apps/components/helpers/InputValue.svelte index 4f619f3dd3..4da3b8babd 100644 --- a/frontend/src/lib/components/apps/components/helpers/InputValue.svelte +++ b/frontend/src/lib/components/apps/components/helpers/InputValue.svelte @@ -6,7 +6,8 @@ EvalAppInput, EvalV2AppInput, TemplateV2Input, - UploadAppInput + UploadAppInput, + UploadS3AppInput } from '../../inputType' import type { AppEditorContext, @@ -235,6 +236,8 @@ } } else if (lastInput?.type == 'upload') { value = (lastInput as UploadAppInput).value + } else if (lastInput?.type == 'uploadS3') { + value = (lastInput as UploadS3AppInput).value } else { value = undefined } diff --git a/frontend/src/lib/components/apps/components/inputs/AppS3FileInput.svelte b/frontend/src/lib/components/apps/components/inputs/AppS3FileInput.svelte index f2b6537737..efd46a1126 100644 --- a/frontend/src/lib/components/apps/components/inputs/AppS3FileInput.svelte +++ b/frontend/src/lib/components/apps/components/inputs/AppS3FileInput.svelte @@ -3,6 +3,7 @@ import { initConfig, initOutput } from '../../editor/appUtils' import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types' + import type { FileUploadData } from '../../inputType' import { initCss } from '../../utils' import ResolveStyle from '../helpers/ResolveStyle.svelte' import ResolveConfig from '../helpers/ResolveConfig.svelte' @@ -26,16 +27,6 @@ configuration ) - type FileUploadData = { - name: string - size: number - progress: number - cancelled?: boolean - errorMessage?: string - path?: string - file?: File - } - let fileUploads: Writable = writable([]) const { app, worldStore, componentControl, runnableComponents, workspace } = getContext('AppViewerContext') diff --git a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte index e5db6c1f34..660d3afe78 100644 --- a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte @@ -85,7 +85,11 @@ import Summary from '$lib/components/Summary.svelte' import HideButton from './settingsPanel/HideButton.svelte' import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte' - import { computeS3FileInputPolicy, computeWorkspaceS3FileInputPolicy } from './appUtilsS3' + import { + computeS3FileInputPolicy, + computeWorkspaceS3FileInputPolicy, + computeS3ImageViewerPolicy + } from './appUtilsS3' import { isCloudHosted } from '$lib/cloud' import { base } from '$lib/base' import ClipboardPanel from '$lib/components/details/ClipboardPanel.svelte' @@ -342,6 +346,17 @@ } policy.s3_inputs = s3_inputs + + const s3FileKeys = items + .filter((x) => (x.data as AppComponent).type === 'imagecomponent') + .map((x) => { + const c = x.data as AppComponent + const config = c.configuration as any + return computeS3ImageViewerPolicy(config, $app) + }) + .filter(Boolean) as { s3_path?: string | undefined; resource?: string | undefined }[] + + policy.allowed_s3_keys = s3FileKeys } async function processRunnable( diff --git a/frontend/src/lib/components/apps/editor/appUtilsS3.ts b/frontend/src/lib/components/apps/editor/appUtilsS3.ts index 2256331cc4..5938db0435 100644 --- a/frontend/src/lib/components/apps/editor/appUtilsS3.ts +++ b/frontend/src/lib/components/apps/editor/appUtilsS3.ts @@ -80,3 +80,14 @@ export function computeS3FileInputPolicy(s3Config: any, app: App) { file_key_regex } } + +export function computeS3ImageViewerPolicy(config: any, app: App) { + if ( + config.sourceKind.value === 's3 (workspace storage)' || + config.source.value.startsWith('s3://') + ) { + return { s3_path: config.source.value.replace('s3://', ''), resource: 'default' } + } else { + return undefined + } +} diff --git a/frontend/src/lib/components/apps/editor/component/components.ts b/frontend/src/lib/components/apps/editor/component/components.ts index 21e288b0a8..c073ca3691 100644 --- a/frontend/src/lib/components/apps/editor/component/components.ts +++ b/frontend/src/lib/components/apps/editor/component/components.ts @@ -485,6 +485,7 @@ export const selectOptions = { prose: ['sm', 'Default', 'lg', 'xl', '2xl'], imageSourceKind: [ 'url', + 's3 (workspace storage)', 'png encoded as base64', 'jpeg encoded as base64', 'svg encoded as base64' @@ -3073,6 +3074,10 @@ See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm' fileUpload: { accept: 'image/*', convertTo: 'base64' + }, + fileUploadS3: { + accept: 'image/*', + convertTo: 'base64' } }, sourceKind: { diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte index abd055fd7e..d73763c98f 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte @@ -5,13 +5,24 @@ import EvalInputEditor from './inputEditor/EvalInputEditor.svelte' import RowInputEditor from './inputEditor/RowInputEditor.svelte' import StaticInputEditor from './inputEditor/StaticInputEditor.svelte' + import { Button } from '$lib/components/common' import UploadInputEditor from './inputEditor/UploadInputEditor.svelte' import { getContext, createEventDispatcher } from 'svelte' import type { AppViewerContext, RichConfiguration } from '../../types' import type { InputConnection, InputType, UploadAppInput } from '../../inputType' import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte' + import S3FilePicker from '$lib/components/S3FilePicker.svelte' import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte' - import { FunctionSquare, Loader2, Pen, Plug2, Upload, User } from 'lucide-svelte' + import { + FunctionSquare, + Loader2, + Pen, + Plug2, + Upload, + UploadCloud, + User, + Pipette + } from 'lucide-svelte' import { fieldTypeToTsType } from '../../utils' import EvalV2InputEditor from './inputEditor/EvalV2InputEditor.svelte' import ConnectionButton from '$lib/components/common/button/ConnectionButton.svelte' @@ -30,6 +41,7 @@ export let format: string | undefined export let selectOptions: string[] | undefined export let fileUpload: UploadAppInput['fileUpload'] | undefined = undefined + export let fileUploadS3: UploadAppInput['fileUploadS3'] | undefined = undefined export let placeholder: string | undefined export let customTitle: string | undefined = undefined export let displayType: boolean = false @@ -44,11 +56,22 @@ export let markdownTooltip: string | undefined = undefined export let securedContext = false - const { connectingInput, app } = getContext('AppViewerContext') + const { connectingInput, app, workspace } = getContext('AppViewerContext') const dispatch = createEventDispatcher() let evalV2editor: EvalV2InputEditor + let s3FilePicker: S3FilePicker | undefined + let s3PickerSelection: { s3: string; storage?: string } | undefined = undefined + let s3FolderPrefix: string = '' + $: s3PickerSelection && updateSelectedS3File() + + function updateSelectedS3File() { + if (s3PickerSelection) { + componentInput['value'] = `s3://${s3PickerSelection.s3}` + } + } + function applyConnection(connection: InputConnection) { const expr = `${connection.componentId}.${connection.path}` //@ts-ignore @@ -150,6 +173,15 @@ {#if fileUpload} {/if} + {#if fileUploadS3} + + {/if} {#if componentInput?.type === 'connected'} {/if} @@ -201,6 +233,38 @@ /> {:else if componentInput?.type === 'upload'} + {:else if componentInput?.type === 'uploadS3'} +
+ +
+ + + {:else if componentInput?.type === 'user'} Field's value is set by the user {/if} diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte index 9b76c3c759..d802eacfd8 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte @@ -71,6 +71,7 @@ selectOptions={meta?.['selectOptions']} tooltip={meta?.['tooltip']} fileUpload={meta?.['fileUpload']} + fileUploadS3={meta?.['fileUploadS3']} placeholder={meta?.['placeholder']} customTitle={meta?.['customTitle']} loading={meta?.['loading']} diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/UploadInputEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/UploadInputEditor.svelte index 1598f39341..a5303e146e 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/UploadInputEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/UploadInputEditor.svelte @@ -1,25 +1,59 @@ - { - if (componentInput) { - componentInput.value = fileUpload?.multiple ? detail : detail?.[0] - } - }} -> - - - - - +{#if s3} + { + const cleanPrefix = prefix ? `${prefix.replace(/^\/+|\/+$/g, '')}/` : '' + return `${cleanPrefix}${file.name}` + }} + on:addition={({ detail }) => { + if (componentInput) { + componentInput.value = `s3://${detail.path}` + } + }} + /> +{:else} + { + if (componentInput) { + componentInput.value = fileUpload?.multiple ? detail : detail?.[0] + } + }} + > + + + + + +{/if} diff --git a/frontend/src/lib/components/apps/inputType.ts b/frontend/src/lib/components/apps/inputType.ts index 237d7bbd61..4bc2fc8532 100644 --- a/frontend/src/lib/components/apps/inputType.ts +++ b/frontend/src/lib/components/apps/inputType.ts @@ -76,6 +76,21 @@ export type UploadInput = { value: string } +export type UploadS3Input = { + type: 'uploadS3' + value: string +} + +export type FileUploadData = { + name: string + size: number + progress: number + cancelled?: boolean + errorMessage?: string + path?: string + file?: File +} + export type EvalInput = { type: 'eval' expr: string @@ -149,6 +164,7 @@ export type AppInputSpec = | EvalInput | EvalInputV2 | UploadInput + | UploadS3Input | ResultInput | TemplateInput | TemplateV2Input @@ -174,6 +190,10 @@ type InputConfiguration = { */ convertTo?: ReadFileAs } + fileUploadS3?: { + accept: string + multiple?: boolean + } noStatic?: boolean onDemandOnly?: boolean hideRefreshButton?: boolean @@ -243,6 +263,7 @@ export type StaticAppInputOnDemand = Extract export type UploadAppInput = Extract +export type UploadS3AppInput = Extract export type RichAppInput = | AppInput diff --git a/frontend/src/lib/components/apps/types.ts b/frontend/src/lib/components/apps/types.ts index bf7beb6b7c..3317709ce0 100644 --- a/frontend/src/lib/components/apps/types.ts +++ b/frontend/src/lib/components/apps/types.ts @@ -22,7 +22,8 @@ import type { StaticAppInput, TemplateV2AppInput, UploadAppInput, - UserAppInput + UploadS3AppInput, + UserAppInput, } from './inputType' import type { World } from './rx' import type { FilledItem } from './svelte-grid/types' @@ -59,6 +60,7 @@ export type Configuration = | EvalAppInput | EvalV2AppInput | UploadAppInput + | UploadS3AppInput | ResultAppInput | TemplateV2AppInput diff --git a/frontend/src/lib/components/common/fileUpload/FileUpload.svelte b/frontend/src/lib/components/common/fileUpload/FileUpload.svelte index 13e2b7b4ed..5bf6373670 100644 --- a/frontend/src/lib/components/common/fileUpload/FileUpload.svelte +++ b/frontend/src/lib/components/common/fileUpload/FileUpload.svelte @@ -33,6 +33,7 @@ export let fileUploads: Writable = writable([]) export let appPath: string | undefined = undefined export let disabled = false + export let iconSize: number | undefined = undefined export let initialValue: | { s3: string @@ -534,6 +535,7 @@ accept={acceptedFileTypes?.join(',')} multiple={allowMultiple} returnFileNames + iconSize={iconSize} on:change={({ detail }) => { forceDisplayUploads = false handleChange(detail)