diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 8b1e52cef6..4c2e4d7211 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -36158ddebc1b77f47380d6bba59d70bc6d18b653 \ No newline at end of file +d3e20c73b06a7c6820868769a49b5fa621591653 \ No newline at end of file diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index d5ac116531..0a8878fad6 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -1858,7 +1858,8 @@ async fn delete_s3_file_from_app( let s3_client = build_object_store_client(&s3_resource).await?; - let path = object_store::path::Path::from(file_key.as_str()); + let path = object_store::path::Path::parse(file_key.as_str()) + .map_err(|e| Error::internal_err(format!("Error parsing file key: {}", e)))?; s3_client.delete(&path).await.map_err(|err| { tracing::error!("Error deleting file: {:?}", err); diff --git a/frontend/src/lib/components/DisplayResult.svelte b/frontend/src/lib/components/DisplayResult.svelte index 999c57bb26..5eac69b8be 100644 --- a/frontend/src/lib/components/DisplayResult.svelte +++ b/frontend/src/lib/components/DisplayResult.svelte @@ -729,7 +729,8 @@ appPath ? 'apps_u/load_image_preview/' + appPath : 'job_helpers/load_image_preview' - }?file_key=${result.s3}` + (result.storage ? `&storage=${result.storage}` : '')} + }?file_key=${encodeURIComponent(result.s3)}` + + (result.storage ? `&storage=${result.storage}` : '')} /> {:else if result?.s3?.endsWith('.pdf')} @@ -740,7 +741,8 @@ appPath ? 'apps_u/load_image_preview/' + appPath : 'job_helpers/load_image_preview' - }?file_key=${result.s3}` + (result.storage ? `&storage=${result.storage}` : '')} + }?file_key=${encodeURIComponent(result.s3)}` + + (result.storage ? `&storage=${result.storage}` : '')} /> {/if} @@ -798,8 +800,9 @@ preview rendered {:else} @@ -815,8 +818,9 @@
{/if} diff --git a/frontend/src/lib/components/ParqetCsvTableRenderer.svelte b/frontend/src/lib/components/ParqetCsvTableRenderer.svelte index b5053b2307..1c50dfba2c 100644 --- a/frontend/src/lib/components/ParqetCsvTableRenderer.svelte +++ b/frontend/src/lib/components/ParqetCsvTableRenderer.svelte @@ -195,9 +195,9 @@ {#if !disable_download && !s3resource.endsWith('.csv')}
CSV
diff --git a/frontend/src/lib/components/S3FilePicker.svelte b/frontend/src/lib/components/S3FilePicker.svelte index f08284e53f..3a8f964062 100644 --- a/frontend/src/lib/components/S3FilePicker.svelte +++ b/frontend/src/lib/components/S3FilePicker.svelte @@ -108,13 +108,7 @@ if (!firstLoad) { timeout && clearTimeout(timeout) timeout = setTimeout(() => { - page = 0 - count = 0 - displayedCount = 0 - allFilesByKey = {} - displayedFileKeys = [] - listMarkers = [] - loadFiles() + clearAndLoadFiles({ keepFilter: true }) }, 500) } else { firstLoad = false @@ -286,7 +280,6 @@ fileInfoLoading = false } - let render = 0 async function deleteFileFromS3(fileKey: string | undefined) { fileDeletionInProgress = true if (fileKey === undefined) { @@ -303,12 +296,48 @@ deletionModalOpen = false } sendUserToast(`${fileKey} deleted from S3 bucket`) - displayedFileKeys = [...displayedFileKeys.filter((key) => key !== fileKey)] - delete allFilesByKey[fileKey] - filePreview = undefined - fileMetadata = undefined selectedFileKey = { s3: '' } - render++ + const currentPage = page + await clearAndLoadFiles() + for (let i = 0; i < currentPage; i++) { + page = i + 1 + await loadFiles() + } + const fileKeyFolders = fileKey.split('/').slice(0, -1) + let current_path: string | undefined = undefined + for (let i = 0; i < fileKeyFolders.length; i++) { + current_path = + current_path === undefined ? fileKeyFolders[i] : current_path + fileKeyFolders[i] + if (i < fileKeyFolders.length) { + current_path += '/' + } + const folder = allFilesByKey[current_path] + if (folder) { + folder.collapsed = false + } + for (let file_key in allFilesByKey) { + let file_info = allFilesByKey[file_key] + if (file_info.parentPath === current_path) { + displayedFileKeys.push(file_key) + } + } + } + displayedFileKeys = displayedFileKeys.sort() + } + + async function clearAndLoadFiles({ keepFilter }: { keepFilter?: boolean } = {}) { + displayedFileKeys = [] + allFilesByKey = {} + count = 0 + displayedCount = 0 + page = 0 + listMarkers = [] + fileMetadata = undefined + filePreview = undefined + if (keepFilter) { + filter = '' + } + await loadFiles() } async function moveS3File(srcFileKey: string | undefined, destFileKey: string | undefined) { @@ -329,7 +358,7 @@ } sendUserToast(`${srcFileKey} moved to ${destFileKey}`) selectedFileKey = { s3: destFileKey! } - await loadFiles() + await clearAndLoadFiles() await loadFileMetadataPlusPreviewAsync(selectedFileKey.s3) } @@ -342,15 +371,6 @@ initialFileKey = { ...preSelectedFileKey } selectedFileKey = { ...preSelectedFileKey } } - displayedFileKeys = [] - allFilesByKey = {} - count = 0 - displayedCount = 0 - page = 0 - filter = '' - listMarkers = [] - fileMetadata = undefined - filePreview = undefined reloadContent() drawer.openDrawer?.() } @@ -372,7 +392,7 @@ workspaceSettingsInitialized = false return } - await loadFiles() // TODO: Potentially load only on the first open and add a refresh button + await clearAndLoadFiles() if (selectedFileKey !== undefined) { if (allFilesByKey[selectedFileKey.s3] === undefined) { selectedFileKey = { s3: '' } @@ -513,62 +533,58 @@ {:else}
- {#key render} - +
-
- {@const file_info = allFilesByKey[displayedFileKeys[index]]} - {#if file_info} + {@const file_info = allFilesByKey[displayedFileKeys[index]]} + {#if file_info} +
selectItem(index)} + class={twMerge( + 'flex flex-row h-full font-semibold text-xs items-center justify-start', + selectedFileKey !== undefined && selectedFileKey.s3 === file_info.full_key + ? 'bg-surface-hover' + : '' + )} + >
selectItem(index)} - class={twMerge( - 'flex flex-row h-full font-semibold text-xs items-center justify-start', - selectedFileKey !== undefined && - selectedFileKey.s3 === file_info.full_key - ? 'bg-surface-hover' - : '' - )} + class={`flex flex-row w-full gap-2 h-full items-center`} + style={`margin-left: ${(2 + file_info.nestingLevel) * 0.25}rem;`} > -
- {#if file_info.type === 'folder'} - {#if file_info.collapsed}{:else}{/if} -
- {file_info.display_name} ({file_info.count}{count % 1000 === 0 && - lastKeyFolders[file_info.nestingLevel / 2] === - file_info.display_name - ? '+' - : ''} item{file_info.count === 1 ? '' : 's'}) -
- {:else} - -
- {file_info.display_name} -
- {/if} -
+ {#if file_info.type === 'folder'} + {#if file_info.collapsed}{:else}{/if} +
+ {file_info.display_name} ({file_info.count}{count % 1000 === 0 && + lastKeyFolders[file_info.nestingLevel / 2] === file_info.display_name + ? '+' + : ''} item{file_info.count === 1 ? '' : 's'}) +
+ {:else} + +
+ {file_info.display_name} +
+ {/if}
- {/if} -
- - {/key} +
+ {/if} +
+
- {#if fileMetadata?.fileKey.endsWith('.png') || fileMetadata?.fileKey.endsWith('.jpg') || fileMetadata?.fileKey.endsWith('.jpeg') || fileMetadata?.fileKey.endsWith('.webp')} -
- S3 preview -
- {:else if fileMetadata?.fileKey.endsWith('.pdf')} -
- -
- {:else if filePreviewLoading} -
- File preview loading -
- {:else if fileMetadata !== undefined && filePreview !== undefined} -
- {#if filePreview.contentType === 'Unknown'} - Type of file not supported for preview. - {:else if filePreview.contentType === 'Csv'} - Previewing a {filePreview.contentType?.toLowerCase()} file. Separator character: -
- -
- Header row: -
- - loadFilePreview( - fileMetadata?.fileKey ?? '', - fileMetadata?.size, - fileMetadata?.mimeType - )} - /> -
- {:else} - Previewing a {filePreview.contentType?.toLowerCase()} file. - {/if} -
-
{#if !emptyString(filePreview.contentPreview)}{filePreview.contentPreview}{:else if filePreview.contentType !== undefined}Preview impossible.{/if}
+						{#if filePreviewLoading || fileMetadata}
+							{#if fileMetadata?.fileKey.endsWith('.png') || fileMetadata?.fileKey.endsWith('.jpg') || fileMetadata?.fileKey.endsWith('.jpeg') || fileMetadata?.fileKey.endsWith('.webp')}
+								
+ S3 preview +
+ {:else if fileMetadata?.fileKey.endsWith('.pdf')} +
+ +
+ {:else if filePreviewLoading} +
+ File preview loading +
+ {:else if fileMetadata !== undefined && filePreview !== undefined} +
+ {#if filePreview.contentType === 'Unknown'} + Type of file not supported for preview. + {:else if filePreview.contentType === 'Csv'} + Previewing a {filePreview.contentType?.toLowerCase()} file. Separator character: +
+ +
+ Header row: +
+ + loadFilePreview( + fileMetadata?.fileKey ?? '', + fileMetadata?.size, + fileMetadata?.mimeType + )} + /> +
+ {:else} + Previewing a {filePreview.contentType?.toLowerCase()} file. + {/if} +
+
{#if !emptyString(filePreview.contentPreview)}{filePreview.contentPreview}{:else if filePreview.contentType !== undefined}Preview impossible.{/if}
 							
+ {/if} {/if}
@@ -812,7 +832,7 @@ uploadModalOpen = false if (evt.detail !== undefined && evt.detail !== null) { selectedFileKey = { s3: evt.detail } - loadFiles() + await clearAndLoadFiles() loadFileMetadataPlusPreviewAsync(evt.detail) } }} diff --git a/frontend/src/lib/components/apps/components/helpers/eval.ts b/frontend/src/lib/components/apps/components/helpers/eval.ts index 3e6b5cc522..bb737040bd 100644 --- a/frontend/src/lib/components/apps/components/helpers/eval.ts +++ b/frontend/src/lib/components/apps/components/helpers/eval.ts @@ -3,7 +3,11 @@ import { sendUserToast } from '$lib/toast' import { waitJob } from '$lib/components/waitJob' import { base } from '$lib/base' -export function computeGlobalContext(world: World | undefined, id: string | undefined, extraContext: any = {}) { +export function computeGlobalContext( + world: World | undefined, + id: string | undefined, + extraContext: any = {} +) { return { ...Object.fromEntries( Object.entries(world?.outputsById ?? {}) @@ -29,15 +33,17 @@ function create_context_function_template( return ` return async function (context, state, createProxy, goto, setTab, recompute, globalRecompute, getAgGrid, setValue, setSelectedIndex, openModal, closeModal, open, close, validate, invalidate, validateAll, clearFiles, showToast, waitJob, askNewResource, downloadFile) { "use strict"; -${contextKeys && contextKeys.length > 0 - ? `let ${contextKeys.map((key) => ` ${key} = createProxy('${key}', context['${key}'])`)};` - : `` - } -${hasReturnAsLastLine - ? eval_string - : ` +${ + contextKeys && contextKeys.length > 0 + ? `let ${contextKeys.map((key) => ` ${key} = createProxy('${key}', context['${key}'])`)};` + : `` +} +${ + hasReturnAsLastLine + ? eval_string + : ` return ${eval_string.startsWith('return ') ? eval_string.substring(7) : eval_string}` - } +} } ` @@ -278,9 +284,9 @@ export async function eval_like( if (typeof input === 'object' && input.s3) { const workspaceId = ((context ?? {}) as any).ctx?.workspace - const s3href = `${base}/api/w/${workspaceId}/job_helpers/download_s3_file?file_key=${ - input?.s3 - }${input?.storage ? `&storage=${input.storage}` : ''}` + const s3href = `${base}/api/w/${workspaceId}/job_helpers/download_s3_file?file_key=${encodeURIComponent( + input?.s3 ?? '' + )}${input?.storage ? `&storage=${input.storage}` : ''}` downloadFile(s3href, filename || input.s3) } else if (typeof input === 'string') { if (input.startsWith('data:')) { diff --git a/frontend/src/lib/components/common/fileDownload/FileDownload.svelte b/frontend/src/lib/components/common/fileDownload/FileDownload.svelte index 2c80eea7ef..519acbb1d9 100644 --- a/frontend/src/lib/components/common/fileDownload/FileDownload.svelte +++ b/frontend/src/lib/components/common/fileDownload/FileDownload.svelte @@ -15,7 +15,9 @@ focus-within:border-blue-500 hover:bg-blue-50 dark:hover:bg-frost-900 focus-with duration-200 rounded-lg p-1 gap-2" href={`${base}/api/w/${workspaceId ?? $workspaceStore}${ appPath ? `/apps_u/download_s3_file/${appPath}` : '/job_helpers/download_s3_file' - }?file_key=${s3object?.s3}${s3object?.storage ? `&storage=${s3object.storage}` : ''}`} + }?file_key=${encodeURIComponent(s3object?.s3 ?? '')}${ + s3object?.storage ? `&storage=${s3object.storage}` : '' + }`} download={s3object?.s3.split('/').pop() ?? 'unnamed_download.file'} > diff --git a/frontend/src/lib/components/propertyPicker/ObjectViewer.svelte b/frontend/src/lib/components/propertyPicker/ObjectViewer.svelte index 79935129a2..a4a67cff73 100644 --- a/frontend/src/lib/components/propertyPicker/ObjectViewer.svelte +++ b/frontend/src/lib/components/propertyPicker/ObjectViewer.svelte @@ -201,9 +201,9 @@ {#if getTypeAsString(json) === 's3object'} download