Files
windmill/frontend/src/lib/components/ArgInput.svelte
Ruben Fiszel f6f829b9f0 remove tooltip
2022-09-15 22:15:26 +02:00

363 lines
10 KiB
Svelte

<script lang="ts">
import { slide } from 'svelte/transition'
import {
faArrowRotateLeft,
faChevronDown,
faChevronUp,
faMinus,
faPlus
} from '@fortawesome/free-solid-svg-icons'
import { setInputCat as computeInputCat, type InputCat } from '$lib/utils'
import { Button, Tooltip } from 'flowbite-svelte'
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
import FieldHeader from './FieldHeader.svelte'
import ObjectResourceInput from './ObjectResourceInput.svelte'
import ObjectTypeNarrowing from './ObjectTypeNarrowing.svelte'
import ResourcePicker from './ResourcePicker.svelte'
import StringTypeNarrowing from './StringTypeNarrowing.svelte'
import SchemaForm from './SchemaForm.svelte'
import type { SchemaProperty } from '$lib/common'
import SimpleEditor from './SimpleEditor.svelte'
export let label: string = ''
export let value: any
export let defaultValue: any = undefined
export let description: string = ''
export let format: string = ''
export let contentEncoding: 'base64' | 'binary' | undefined = undefined
export let type: string | undefined = undefined
export let required = false
export let pattern: undefined | string = undefined
export let valid = required ? false : true
export let minRows = 1
export let maxRows = 10
export let enum_: string[] | undefined = undefined
export let disabled = false
export let editableSchema = false
export let itemsType:
| { type?: 'string' | 'number' | 'bytes'; contentEncoding?: 'base64' }
| undefined = undefined
export let displayHeader = true
export let properties: { [name: string]: SchemaProperty } | undefined = undefined
let seeEditable: boolean = enum_ != undefined || pattern != undefined
const dispatch = createEventDispatcher()
$: minHeight = `${1 + minRows * 1.2}em`
$: maxHeight = maxRows ? `${1 + maxRows * 1.2}em` : `auto`
$: validateInput(pattern, value)
let error: string = ''
export let editor: SimpleEditor | undefined = undefined
let rawValue: string | undefined = undefined
$: {
if (rawValue) {
try {
value = JSON.parse(rawValue)
} catch (err) {
error = err.toString()
}
}
}
$: {
if (inputCat === 'object') {
evalValueToRaw()
validateInput(pattern, value)
}
}
$: {
defaultValue && recomputeRowSize(JSON.stringify(defaultValue, null, 4))
}
function recomputeRowSize(str: string) {
if (type == 'string') {
minRows = str.split('\n').length
} else if (type != 'string') {
minRows = Math.max(minRows, Math.min(str.split(/\r\n|\r|\n/).length + 1, maxRows))
}
}
export function evalValueToRaw() {
if (value) {
rawValue = JSON.stringify(value, null, 4)
}
}
function fileChanged(e: any, cb: (v: string | undefined) => void) {
let t = e.target
if (t && 'files' in t && t.files.length > 0) {
let reader = new FileReader()
reader.onload = (e: any) => {
cb(e.target.result.split('base64,')[1])
}
reader.readAsDataURL(t.files[0])
} else {
cb(undefined)
}
}
function validateInput(pattern: string | undefined, v: any): void {
if (required && (v == undefined || v == null)) {
error = 'This field is required'
valid = false
} else {
if (pattern && !testRegex(pattern, v)) {
error = `Should match ${pattern}`
valid = false
} else {
error = ''
valid = true
}
}
}
function testRegex(pattern: string, value: any): boolean {
try {
const regex = new RegExp(pattern)
return regex.test(value)
} catch (err) {
return false
}
}
$: {
if (value == undefined) {
value = defaultValue
}
}
export let inputCat: InputCat = 'string'
$: inputCat = computeInputCat(type, format, itemsType?.type, enum_, contentEncoding)
</script>
<div class="flex flex-col w-full mb-2">
<div>
{#if displayHeader}
<FieldHeader {label} {required} {type} {contentEncoding} {format} {itemsType} />
{/if}
{#if editableSchema}
<div class="my-1 text-xs border-solid border border-gray-400 rounded p-2">
<span
class="underline"
on:click={() => {
seeEditable = !seeEditable
}}
>
Customize argument
<Icon class="ml-2" data={seeEditable ? faChevronUp : faChevronDown} scale={0.7} />
</span>
{#if seeEditable}
<div transition:slide class="mt-2">
<label class="text-gray-700">
Description
<textarea rows="1" bind:value={description} placeholder="Edit description" />
{#if type == 'string' && !contentEncoding && format != 'date-time'}
<StringTypeNarrowing bind:format bind:pattern bind:enum_ bind:contentEncoding />
{:else if type == 'object'}
<ObjectTypeNarrowing bind:format />
{:else if type == 'array'}
<select bind:value={itemsType}>
<option value={undefined}>No specific item type</option>
<option value={{ type: 'string' }}> Items are strings</option>
<option value={{ type: 'number' }}>Items are numbers</option>
<option value={{ type: 'string', contentEncoding: 'base64' }}
>Items are bytes</option
>
</select>
{/if}
</label>
</div>
{/if}
</div>
<span class="text-2xs">Preview:</span>
{/if}
<div class="grid grid-cols-2">
{#if description || error}
<div class="text-sm italic pb-1">
{description}
</div>
<div class="text-right text-xs {error === '' ? 'text-white' : 'font-bold text-red-600'}">
{error === '' ? '...' : error}
</div>
{:else}
<div class="mt-1" />
{/if}
</div>
<div class="flex space-x-1">
{#if inputCat == 'number'}
<input
{disabled}
type="number"
class={valid
? ''
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'}
placeholder={defaultValue ?? ''}
bind:value
on:input={() => dispatch('input', { value, isRaw: true })}
/>
{:else if inputCat == 'boolean'}
<input
{disabled}
type="checkbox"
class={valid
? ''
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'}
bind:checked={value}
/>
{#if type == 'boolean' && value == undefined}
<span>&nbsp; Not set</span>
{/if}
{:else if inputCat == 'list'}
<div>
<div>
{#each value ?? [] as v, i}
<div class="flex flex-row max-w-md mt-1">
{#if itemsType?.type == 'number'}
<input type="number" bind:value={v} />
{:else if itemsType?.type == 'string' && itemsType?.contentEncoding == 'base64'}
<input
type="file"
class="my-6"
on:change={(x) => fileChanged(x, (val) => (value[i] = val))}
multiple={false}
/>
{:else}
<input type="text" bind:value={v} />
{/if}
<button
class="default-button-secondary mx-6"
on:click={() => {
value = value.filter((el) => el != v)
if (value.length == 0) {
value = undefined
}
}}
>
<Icon data={faMinus} class="mb-1" />
</button>
</div>
{/each}
</div>
<button
class="default-button-secondary mt-1"
on:click={() => {
if (value == undefined) {
value = []
}
value = value.concat('')
}}
>
<Icon data={faPlus} class="mr-2" />
Add item
</button>
<span class="ml-2">
{(value ?? []).length} item{(value ?? []).length > 1 ? 's' : ''}
</span>
</div>
{:else if inputCat == 'resource-object'}
<ObjectResourceInput {format} bind:value />
{:else if inputCat == 'object'}
{#if properties && Object.keys(properties).length > 0}
<div class="p-4 pl-8 border rounded w-full">
<SchemaForm
schema={{ properties, $schema: '', required: [], type: 'object' }}
bind:args={value}
/>
</div>
{:else}
<textarea
{disabled}
style="min-height: {minHeight}; max-height: {maxHeight}"
on:input={async () => recomputeRowSize(rawValue ?? '')}
class="col-span-10 {valid
? ''
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'}"
placeholder={defaultValue ? JSON.stringify(defaultValue, null, 4) : ''}
bind:value={rawValue}
/>
{/if}
{:else if inputCat == 'enum'}
<select {disabled} class="px-6" bind:value>
{#each enum_ ?? [] as e}
<option>{e}</option>
{/each}
</select>
{:else if inputCat == 'date'}
<input class="inline-block" type="datetime-local" bind:value />
{:else if inputCat == 'sql'}
<div class="border rounded mb-4 w-full border-gray-700">
<SimpleEditor
on:focus={() => dispatch('focus')}
on:blur={() => dispatch('blur')}
bind:this={editor}
lang="sql"
bind:code={value}
class="few-lines-editor"
on:change={async () => {
dispatch('input', { rawValue: value, isRaw: false })
}}
/>
</div>
{:else if inputCat == 'base64'}
<input
type="file"
class="my-6"
on:change={(x) => fileChanged(x, (val) => (value = val))}
multiple={false}
/>
{:else if inputCat == 'resource-string'}
<ResourcePicker
bind:value
resourceType={format.split('-').length > 1
? format.substring('resource-'.length)
: undefined}
/>
{:else if inputCat == 'string'}
<textarea
on:focus={() => dispatch('focus')}
on:blur={() => dispatch('blur')}
{disabled}
style="height: {minHeight}; max-height: {maxHeight}"
class="col-span-10 {valid
? ''
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'}"
placeholder={defaultValue ?? ''}
bind:value
on:input={async () => {
recomputeRowSize(value)
dispatch('input', { rawValue: value, isRaw: false })
}}
/>
{/if}
{#if !required && inputCat != 'resource-object'}
<!-- <Tooltip placement="bottom" content="Reset to default value">
<Button
on:click={() => (value = undefined)}
{disabled}
color="alternative"
size="sm"
class="h-8"
>
<Icon data={faArrowRotateLeft} />
</Button>
</Tooltip> -->
{/if}
<slot name="actions" />
</div>
</div>
</div>