mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-10 00:05:27 +00:00
@@ -9,7 +9,7 @@
|
||||
emptyString,
|
||||
getSchemaFromProperties
|
||||
} from '$lib/utils'
|
||||
import { DollarSign, Pipette, Plus, X, Check, Loader2 } from 'lucide-svelte'
|
||||
import { DollarSign, Plus, X, Check, Loader2 } from 'lucide-svelte'
|
||||
import { createEventDispatcher, onDestroy, onMount, tick, untrack } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { Button, SecondsInput } from './common'
|
||||
@@ -25,7 +25,6 @@
|
||||
import ArgEnum from './ArgEnum.svelte'
|
||||
import DateTimeInput from './DateTimeInput.svelte'
|
||||
import DateInput from './DateInput.svelte'
|
||||
import S3FilePicker from './S3FilePicker.svelte'
|
||||
import CurrencyInput from './apps/components/inputs/currency/CurrencyInput.svelte'
|
||||
import FileUpload from './common/fileUpload/FileUpload.svelte'
|
||||
import autosize from '$lib/autosize'
|
||||
@@ -42,6 +41,7 @@
|
||||
import type { ComponentCustomCSS } from './apps/types'
|
||||
import MultiSelect from './select/MultiSelect.svelte'
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
import S3ArgInput from './common/fileUpload/S3ArgInput.svelte'
|
||||
|
||||
interface Props {
|
||||
label?: string
|
||||
@@ -227,8 +227,6 @@
|
||||
|
||||
let ignoreValueUndefined = $state(false)
|
||||
let error: string = $state('')
|
||||
let s3FilePicker: S3FilePicker | undefined = $state()
|
||||
let s3FileUploadRawMode: boolean = $state(false)
|
||||
let isListJson = $state(false)
|
||||
let hasIsListJsonChanged = $state(false)
|
||||
|
||||
@@ -499,16 +497,6 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<S3FilePicker
|
||||
bind:this={s3FilePicker}
|
||||
bind:selectedFileKey={value}
|
||||
on:close={() => {
|
||||
rawValue = JSON.stringify(value, null, 2)
|
||||
editor?.setCode(rawValue)
|
||||
}}
|
||||
readOnlyMode={false}
|
||||
/>
|
||||
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<div
|
||||
class={twMerge(
|
||||
@@ -631,6 +619,22 @@
|
||||
<span> Not set</span>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if (inputCat == 'resource-object' && format && format.split('-').length > 1 && format
|
||||
.replace('resource-', '')
|
||||
.replace('_', '')
|
||||
.toLowerCase() == 's3object') || (inputCat == 'list' && itemsType?.resourceType === 's3_object')}
|
||||
<S3ArgInput
|
||||
multiple={inputCat == 'list'}
|
||||
bind:value
|
||||
{defaultValue}
|
||||
{setNewValueFromCode}
|
||||
{workspace}
|
||||
onFocus={() => dispatch('focus')}
|
||||
onBlur={() => dispatch('blur')}
|
||||
bind:editor
|
||||
{appPath}
|
||||
{computeS3ForceViewerPolicies}
|
||||
/>
|
||||
{:else if inputCat == 'list' && !isListJson}
|
||||
<div class="w-full flex gap-4">
|
||||
<div class="w-full">
|
||||
@@ -851,70 +855,6 @@
|
||||
}}
|
||||
{showSchemaExplorer}
|
||||
/>
|
||||
{:else if inputCat == 'resource-object' && format && format.split('-').length > 1 && format
|
||||
.replace('resource-', '')
|
||||
.replace('_', '')
|
||||
.toLowerCase() == 's3object'}
|
||||
<div class="flex flex-col w-full gap-1">
|
||||
<Toggle
|
||||
class="flex justify-end"
|
||||
bind:checked={s3FileUploadRawMode}
|
||||
size="xs"
|
||||
options={{ left: 'Raw S3 object input' }}
|
||||
/>
|
||||
{#if s3FileUploadRawMode}
|
||||
{#await import('$lib/components/JsonEditor.svelte')}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:then Module}
|
||||
<Module.default
|
||||
bind:editor
|
||||
on:focus={(e) => {
|
||||
dispatch('focus')
|
||||
}}
|
||||
on:blur={(e) => {
|
||||
dispatch('blur')
|
||||
}}
|
||||
code={JSON.stringify(value ?? defaultValue ?? { s3: '' }, null, 2)}
|
||||
on:changeValue={(e) => {
|
||||
setNewValueFromCode(e.detail)
|
||||
}}
|
||||
/>
|
||||
{/await}
|
||||
{:else}
|
||||
<FileUpload
|
||||
{appPath}
|
||||
computeForceViewerPolicies={computeS3ForceViewerPolicies}
|
||||
{workspace}
|
||||
allowMultiple={false}
|
||||
randomFileKey={true}
|
||||
on:addition={(evt) => {
|
||||
value = {
|
||||
s3: evt.detail?.path ?? '',
|
||||
filename: evt.detail?.filename ?? ''
|
||||
}
|
||||
}}
|
||||
on:deletion={(evt) => {
|
||||
value = {
|
||||
s3: ''
|
||||
}
|
||||
}}
|
||||
defaultValue={defaultValue?.s3}
|
||||
initialValue={value}
|
||||
/>
|
||||
{/if}
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
btnClasses="mt-1"
|
||||
on:click={() => {
|
||||
s3FilePicker?.open?.(value)
|
||||
}}
|
||||
startIcon={{ icon: Pipette }}
|
||||
>
|
||||
Choose an object from the catalog
|
||||
</Button>
|
||||
</div>
|
||||
{:else if inputCat == 'object' || inputCat == 'resource-object' || isListJson}
|
||||
{#if oneOf && oneOf.length >= 2}
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { AppService, HelpersService } from '$lib/gen'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import { Ban, CheckCheck, FileWarning, Files, RefreshCcw, Trash } from 'lucide-svelte'
|
||||
import { Ban, CheckCheck, FileWarning, Files, RefreshCcw, Trash, XIcon } from 'lucide-svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import { emptyString } from '$lib/utils'
|
||||
@@ -94,16 +94,15 @@
|
||||
)
|
||||
init()
|
||||
|
||||
function transform(initialValue: { s3: string; filename?: string }) {
|
||||
return {
|
||||
name: initialValue?.filename ?? initialValue?.s3 ?? 'Unknown file',
|
||||
path: initialValue?.s3,
|
||||
fromBucket: true
|
||||
}
|
||||
}
|
||||
function init() {
|
||||
if (initialS3) {
|
||||
function transform(initialValue: { s3: string; filename: string }) {
|
||||
return {
|
||||
name: initialValue.filename,
|
||||
size: 0,
|
||||
progress: 100,
|
||||
path: initialValue?.s3
|
||||
}
|
||||
}
|
||||
for (const s3 of initialS3) {
|
||||
if (!$fileUploads.find((fileUpload) => fileUpload.path === s3)) {
|
||||
let initialFileUploads = initialValue
|
||||
@@ -117,15 +116,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
export function addUpload(upload: { s3: string; filename?: string }) {
|
||||
$fileUploads = [...$fileUploads, transform(upload)]
|
||||
}
|
||||
|
||||
export function setUpload(upload: { s3: string; filename?: string }) {
|
||||
$fileUploads = [transform(upload)]
|
||||
}
|
||||
|
||||
type FileUploadData = {
|
||||
name: string
|
||||
size: number
|
||||
progress: number
|
||||
size?: number
|
||||
progress?: number
|
||||
cancelled?: boolean
|
||||
errorMessage?: string
|
||||
path?: string
|
||||
file?: File
|
||||
deleteToken?: string
|
||||
fromBucket?: boolean
|
||||
}
|
||||
|
||||
async function handleChange(files: File[] | undefined) {
|
||||
@@ -394,8 +402,28 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
{#snippet fileInput()}
|
||||
<FileInput
|
||||
{folderOnly}
|
||||
{disabled}
|
||||
accept={acceptedFileTypes?.join(',')}
|
||||
multiple={allowMultiple}
|
||||
returnFileNames
|
||||
{iconSize}
|
||||
on:change={({ detail }) => {
|
||||
forceDisplayUploads = false
|
||||
handleChange(detail)
|
||||
}}
|
||||
class={twMerge('w-full h-full', customClass, 'wm-file-input')}
|
||||
style={customStyle}
|
||||
defaultFile={defaultValue}
|
||||
>
|
||||
{containerText}{#if disabled}<br />(Disabled){/if}
|
||||
</FileInput>
|
||||
{/snippet}
|
||||
|
||||
<div class="w-full h-full p-0 flex flex-col">
|
||||
{#if $fileUploads.length > 0 && !forceDisplayUploads}
|
||||
{#if $fileUploads.length > 0}
|
||||
<div class="border rounded-md flex flex-col gap-1 divide-y h-full w-full p-1">
|
||||
<div class="flex h-full overflow-y-auto flex-col">
|
||||
{#each $fileUploads as fileUpload}
|
||||
@@ -468,7 +496,7 @@
|
||||
Remove from list
|
||||
</Button>
|
||||
{/if}
|
||||
{#if fileUpload.progress < 100 && !fileUpload.cancelled && !fileUpload.errorMessage}
|
||||
{#if fileUpload.progress !== undefined && fileUpload.progress < 100 && !fileUpload.cancelled && !fileUpload.errorMessage}
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
@@ -506,100 +534,115 @@
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
{:else if fileUpload.fromBucket}
|
||||
<Button
|
||||
size="xs2"
|
||||
color="red"
|
||||
variant="border"
|
||||
on:click={() => {
|
||||
$fileUploads = $fileUploads.filter(
|
||||
(_fileUpload) => _fileUpload.name !== fileUpload.name
|
||||
)
|
||||
|
||||
if (fileUpload.path) {
|
||||
dispatch('deletion', { path: fileUpload.path })
|
||||
}
|
||||
}}
|
||||
startIcon={{
|
||||
icon: XIcon
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<FileProgressBar
|
||||
progress={fileUpload.progress}
|
||||
color={fileUpload.errorMessage
|
||||
? '#ef4444'
|
||||
: fileUpload.cancelled
|
||||
? '#eab308'
|
||||
: fileUpload.progress === 100
|
||||
? '#22c55e'
|
||||
: '#3b82f6'}
|
||||
ended={fileUpload.cancelled || fileUpload.errorMessage !== undefined}
|
||||
>
|
||||
{#if fileUpload.errorMessage}
|
||||
<span class="text-xs text-red-600">{fileUpload.errorMessage}</span>
|
||||
{:else if fileUpload.cancelled}
|
||||
<span class="text-xs text-yellow-600">Upload cancelled</span>
|
||||
{/if}
|
||||
</FileProgressBar>
|
||||
{#if !fileUpload.fromBucket}
|
||||
<FileProgressBar
|
||||
progress={fileUpload.progress}
|
||||
color={fileUpload.errorMessage
|
||||
? '#ef4444'
|
||||
: fileUpload.cancelled
|
||||
? '#eab308'
|
||||
: fileUpload.progress === 100
|
||||
? '#22c55e'
|
||||
: '#3b82f6'}
|
||||
ended={fileUpload.cancelled || fileUpload.errorMessage !== undefined}
|
||||
>
|
||||
{#if fileUpload.errorMessage}
|
||||
<span class="text-xs text-red-600">{fileUpload.errorMessage}</span>
|
||||
{:else if fileUpload.cancelled}
|
||||
<span class="text-xs text-yellow-600">Upload cancelled</span>
|
||||
{/if}
|
||||
</FileProgressBar>
|
||||
{/if}
|
||||
{#if !(fileUpload.cancelled || fileUpload.errorMessage !== undefined)}
|
||||
<span class="text-xs text-gray-500 dark:text-gray-200">
|
||||
{fileUpload.progress === 100 ? 'Upload finished' : `Uploading`} to path: {fileUpload.path ??
|
||||
'N/A'}
|
||||
{#if fileUpload.fromBucket}
|
||||
{fileUpload.path ?? 'N/A'}
|
||||
{:else}
|
||||
{fileUpload.progress === 100 ? 'Upload finished' : `Uploading`} to path: {fileUpload.path ??
|
||||
'N/A'}
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if allowMultiple}
|
||||
<div class="flex flex-row gap-1 items-center justify-end p-1">
|
||||
{#if !$fileUploads.every((fileUpload) => fileUpload.progress === 100 || fileUpload.cancelled)}
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
$fileUploads = $fileUploads.map((fileUpload) => {
|
||||
if (fileUpload.progress === 100 || fileUpload.cancelled) {
|
||||
return fileUpload
|
||||
}
|
||||
|
||||
fileUpload.cancelled = true
|
||||
fileUpload.progress = 0
|
||||
return fileUpload
|
||||
})
|
||||
}}
|
||||
startIcon={{
|
||||
icon: Ban
|
||||
}}
|
||||
>
|
||||
Cancel all uploads
|
||||
</Button>
|
||||
{/if}
|
||||
<div class="flex flex-row gap-1 items-center justify-end p-1">
|
||||
{#if !forceDisplayUploads && (allowMultiple || folderOnly) && !$fileUploads.every((fileUpload) => fileUpload.progress === 100 || fileUpload.cancelled || fileUpload.fromBucket)}
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
forceDisplayUploads = true
|
||||
$fileUploads = $fileUploads.map((fileUpload) => {
|
||||
if (fileUpload.progress === 100 || fileUpload.cancelled) {
|
||||
return fileUpload
|
||||
}
|
||||
|
||||
fileUpload.cancelled = true
|
||||
fileUpload.progress = 0
|
||||
return fileUpload
|
||||
})
|
||||
}}
|
||||
startIcon={{
|
||||
icon: Files
|
||||
icon: Ban
|
||||
}}
|
||||
disabled={$fileUploads.some(
|
||||
(fileUpload) => fileUpload.progress !== 100 && !fileUpload.cancelled
|
||||
)}
|
||||
>
|
||||
Upload more files
|
||||
Cancel all uploads
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if allowMultiple}
|
||||
{#if forceDisplayUploads}
|
||||
{@render fileInput()}
|
||||
{:else}
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
forceDisplayUploads = true
|
||||
}}
|
||||
startIcon={{
|
||||
icon: Files
|
||||
}}
|
||||
disabled={$fileUploads.some(
|
||||
(fileUpload) =>
|
||||
fileUpload.progress !== 100 && !fileUpload.cancelled && !fileUpload.fromBucket
|
||||
)}
|
||||
>
|
||||
Upload more files
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<FileInput
|
||||
{folderOnly}
|
||||
{disabled}
|
||||
accept={acceptedFileTypes?.join(',')}
|
||||
multiple={allowMultiple}
|
||||
returnFileNames
|
||||
{iconSize}
|
||||
on:change={({ detail }) => {
|
||||
forceDisplayUploads = false
|
||||
handleChange(detail)
|
||||
}}
|
||||
class={twMerge('w-full h-full', customClass, 'wm-file-input')}
|
||||
style={customStyle}
|
||||
defaultFile={defaultValue}
|
||||
>
|
||||
{containerText}{#if disabled}<br />(Disabled){/if}
|
||||
</FileInput>
|
||||
{@render fileInput()}
|
||||
{/if}
|
||||
{#if initialS3 && initialS3.length > 0 && $fileUploads.length == 0}
|
||||
<div class="flex flex-row gap-1 items-center p-1">
|
||||
<span class="text-sm">
|
||||
File currently selected: {initialS3?.join(', ')}
|
||||
File{initialS3.length > 1 ? 's' : ''} currently selected: {initialS3?.join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
<script lang="ts">
|
||||
import S3FilePicker from '$lib/components/S3FilePicker.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { Loader2, Pipette } from 'lucide-svelte'
|
||||
import FileUpload from './FileUpload.svelte'
|
||||
import type SimpleEditor from '$lib/components/SimpleEditor.svelte'
|
||||
import Button from '../button/Button.svelte'
|
||||
|
||||
let {
|
||||
multiple,
|
||||
value = $bindable(),
|
||||
defaultValue,
|
||||
setNewValueFromCode,
|
||||
onFocus,
|
||||
onBlur,
|
||||
computeS3ForceViewerPolicies,
|
||||
workspace,
|
||||
editor = $bindable(),
|
||||
appPath
|
||||
}: {
|
||||
multiple: boolean
|
||||
value: any
|
||||
defaultValue: any
|
||||
setNewValueFromCode: (v: any) => void
|
||||
onFocus: () => void
|
||||
onBlur: () => void
|
||||
computeS3ForceViewerPolicies:
|
||||
| (() =>
|
||||
| {
|
||||
allowed_resources: string[]
|
||||
allow_user_resources: boolean
|
||||
allow_workspace_resource: boolean
|
||||
file_key_regex: string
|
||||
}
|
||||
| undefined)
|
||||
| undefined
|
||||
workspace: string | undefined
|
||||
editor: SimpleEditor | undefined
|
||||
appPath: string | undefined
|
||||
} = $props()
|
||||
|
||||
let s3FileUploadRawMode: boolean = $state(false)
|
||||
|
||||
let s3FilePicker: S3FilePicker | undefined = $state(undefined)
|
||||
let fileUpload: FileUpload | undefined = $state(undefined)
|
||||
</script>
|
||||
|
||||
<S3FilePicker
|
||||
bind:this={s3FilePicker}
|
||||
on:selectAndClose={(ev) => {
|
||||
if (multiple) {
|
||||
if (Array.isArray(value)) {
|
||||
value.push(ev.detail)
|
||||
} else {
|
||||
value = [ev.detail]
|
||||
}
|
||||
fileUpload?.addUpload(ev.detail)
|
||||
} else {
|
||||
value = ev.detail
|
||||
fileUpload?.setUpload(value)
|
||||
}
|
||||
editor?.setCode(JSON.stringify(value))
|
||||
}}
|
||||
readOnlyMode={false}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col w-full gap-1">
|
||||
<Toggle
|
||||
class="flex justify-end"
|
||||
bind:checked={s3FileUploadRawMode}
|
||||
size="xs"
|
||||
options={{ left: `Raw S3 object${multiple ? 's' : ''} input` }}
|
||||
/>
|
||||
{#if s3FileUploadRawMode}
|
||||
{#await import('$lib/components/JsonEditor.svelte')}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:then Module}
|
||||
<Module.default
|
||||
bind:editor
|
||||
on:focus={(e) => {
|
||||
onFocus()
|
||||
}}
|
||||
on:blur={(e) => {
|
||||
onBlur()
|
||||
}}
|
||||
code={JSON.stringify(value ?? defaultValue ?? (multiple ? [] : { s3: '' }), null, 2)}
|
||||
on:changeValue={(e) => {
|
||||
setNewValueFromCode(e.detail)
|
||||
}}
|
||||
/>
|
||||
{/await}
|
||||
{:else}
|
||||
<FileUpload
|
||||
bind:this={fileUpload}
|
||||
{appPath}
|
||||
computeForceViewerPolicies={computeS3ForceViewerPolicies}
|
||||
{workspace}
|
||||
allowMultiple={multiple}
|
||||
randomFileKey={true}
|
||||
on:addition={(evt) => {
|
||||
const s3Object = {
|
||||
s3: evt.detail?.path ?? '',
|
||||
filename: evt.detail?.filename ?? ''
|
||||
}
|
||||
if (multiple) {
|
||||
if (Array.isArray(value)) {
|
||||
value.push(s3Object)
|
||||
} else {
|
||||
value = [s3Object]
|
||||
}
|
||||
} else {
|
||||
value = s3Object
|
||||
}
|
||||
}}
|
||||
on:deletion={(evt) => {
|
||||
if (multiple) {
|
||||
if (Array.isArray(value)) {
|
||||
value = value.filter((v) => v.s3 !== evt.detail?.path)
|
||||
}
|
||||
} else {
|
||||
value = {
|
||||
s3: ''
|
||||
}
|
||||
}
|
||||
}}
|
||||
defaultValue={defaultValue?.s3}
|
||||
initialValue={value}
|
||||
/>
|
||||
{/if}
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
btnClasses="mt-1"
|
||||
on:click={() => {
|
||||
s3FilePicker?.open?.(value)
|
||||
}}
|
||||
startIcon={{ icon: Pipette }}
|
||||
>
|
||||
{multiple ? 'Add' : 'Choose'} an object from the catalog
|
||||
</Button>
|
||||
</div>
|
||||
@@ -502,7 +502,6 @@
|
||||
disabled={!can_write}
|
||||
/>
|
||||
{/if}
|
||||
{s3FileUploadRawMode}
|
||||
{#if s3FileUploadRawMode}
|
||||
{#if can_write}
|
||||
<JsonEditor
|
||||
|
||||
Reference in New Issue
Block a user