mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-30 16:17:25 +00:00
* feat (frontend): improve capture UI * show schema diff * add accept reject form * add diff for first node input * move update schema button * change modified properties display * propagate change through nested components * allow modification of nested object in schema * reject nested changes * clean logic * clean code * clean * nit * fix first strep input * fix argument preview * clean * restore indentation for nested objects * ignore undefined field in diff computation * fix run button disabled update * nit * fix update JSON * fix deletion of nested components * dark mode * clean * replace dropdown with sidebar * only check type for compatibility * add shadow * disbale dnd on edit * auto-scroll within schema * fix nested not dnd object diff viewer * fix captures drawer * open edit tab on add new arg * change button label when schema is the same * ajust padding * fix arg update * fix oneof display * propagate change event through nested schema * handle oneOf * fix preview arg sync * handle s3 object * fix arg sync * update schema input compatibility * clean compatible * accept empty array items * clean * increase of schema args gap * fix nested schema update * allow number and int compatibility * reset args when modifying schema * open fields on add with addPropertyV2 * feat(frontend): use new saved inputs picker * add object viewer to saved inputs row * Display alert info on select saved or historic input * Allow args edit when using saved/historic inputs * replace input library drawers with panels * add captures and saved inputs to arg picker * polish ui * nit * clean * add json arg editor * fix escape for deselect input * bind schema form valid * nit * handle big payloads * nit * nit * validity check disabled in json view * improve infinit list request * fix json schema inferrence * nit * nit * nit * nit * fix tipo * handle large captures * fix cancel button shift * use sideTabBar has a trigger * nit * fix everything * fix --------- Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
364 lines
10 KiB
Svelte
364 lines
10 KiB
Svelte
<script lang="ts">
|
|
import Label from '../Label.svelte'
|
|
import { Info, Trash2 } from 'lucide-svelte'
|
|
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
|
|
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
|
import Button from '../common/button/Button.svelte'
|
|
import CustomPopover from '../CustomPopover.svelte'
|
|
import { Webhook, Route, Unplug, Mail, Play } from 'lucide-svelte'
|
|
import KafkaIcon from '$lib/components/icons/KafkaIcon.svelte'
|
|
import { createEventDispatcher, onDestroy } from 'svelte'
|
|
import { type TriggerKind } from '../triggers'
|
|
import { CaptureService } from '$lib/gen'
|
|
import { workspaceStore } from '$lib/stores'
|
|
import { type CaptureTriggerKind } from '$lib/gen'
|
|
import CaptureButton from '$lib/components/triggers/CaptureButton.svelte'
|
|
import InfiniteList from '../InfiniteList.svelte'
|
|
import { isObject, sendUserToast } from '$lib/utils'
|
|
import SchemaPickerRow from '$lib/components/schema/SchemaPickerRow.svelte'
|
|
import { clickOutside } from '$lib/utils'
|
|
import type { Capture } from '$lib/gen'
|
|
|
|
export let path: string
|
|
export let hasPreprocessor = false
|
|
export let canHavePreprocessor = false
|
|
export let isFlow = false
|
|
export let captureType: CaptureTriggerKind | undefined = undefined
|
|
export let headless = false
|
|
export let addButton = false
|
|
export let canEdit = false
|
|
export let fullHeight = true
|
|
|
|
let selected: number | undefined = undefined
|
|
let testKind: 'preprocessor' | 'main' = 'main'
|
|
let isEmpty: boolean = true
|
|
let infiniteList: InfiniteList | null = null
|
|
let firstClick = true
|
|
let capturesLength = 0
|
|
|
|
$: hasPreprocessor && (testKind = 'preprocessor')
|
|
|
|
const dispatch = createEventDispatcher<{
|
|
openTriggers: {
|
|
kind: TriggerKind
|
|
config: Record<string, any>
|
|
}
|
|
applyArgs: {
|
|
kind: 'main' | 'preprocessor'
|
|
args: Record<string, any> | undefined
|
|
}
|
|
addPreprocessor: null
|
|
updateSchema: {
|
|
payloadData: any
|
|
redirect: boolean
|
|
args?: boolean
|
|
}
|
|
select: any
|
|
testWithArgs: any
|
|
}>()
|
|
|
|
interface CaptureWithPayload extends Capture {
|
|
getFullCapture?: () => Promise<any>
|
|
payloadData?: any
|
|
}
|
|
|
|
export async function loadCaptures(refresh: boolean = false) {
|
|
await infiniteList?.loadData(refresh ? 'refresh' : 'loadMore')
|
|
}
|
|
|
|
function initLoadCaptures(testKind: 'preprocessor' | 'main' = 'main') {
|
|
const loadInputsPageFn = async (page: number, perPage: number) => {
|
|
const captures = await CaptureService.listCaptures({
|
|
workspace: $workspaceStore!,
|
|
runnableKind: isFlow ? 'flow' : 'script',
|
|
path: path ?? '',
|
|
triggerKind: captureType,
|
|
page,
|
|
perPage
|
|
})
|
|
|
|
let capturesWithPayload: CaptureWithPayload[] = captures.map((capture) => {
|
|
let newCapture: CaptureWithPayload = { ...capture }
|
|
if (capture.payload === 'WINDMILL_TOO_BIG') {
|
|
newCapture = {
|
|
...capture,
|
|
getFullCapture: () =>
|
|
CaptureService.getCapture({
|
|
workspace: $workspaceStore!,
|
|
id: capture.id
|
|
})
|
|
}
|
|
}
|
|
const trigger_extra = isObject(capture.trigger_extra) ? capture.trigger_extra : {}
|
|
newCapture.payloadData =
|
|
testKind === 'preprocessor'
|
|
? capture.payload === 'WINDMILL_TOO_BIG'
|
|
? {
|
|
payload: capture.payload,
|
|
...trigger_extra
|
|
}
|
|
: typeof capture.payload === 'object'
|
|
? {
|
|
...capture.payload,
|
|
...trigger_extra
|
|
}
|
|
: trigger_extra
|
|
: capture.payload
|
|
|
|
return newCapture
|
|
})
|
|
return capturesWithPayload
|
|
}
|
|
infiniteList?.setLoader(loadInputsPageFn)
|
|
|
|
const deleteInputFn = async (id: any) => {
|
|
await CaptureService.deleteCapture({
|
|
workspace: $workspaceStore!,
|
|
id
|
|
})
|
|
}
|
|
infiniteList?.setDeleteItemFn(deleteInputFn)
|
|
}
|
|
|
|
async function handleSelect(capture: any) {
|
|
if (selected === capture.id) {
|
|
deselect()
|
|
} else {
|
|
const payloadData = await getPayload(capture)
|
|
selected = capture.id
|
|
dispatch('select', structuredClone(payloadData))
|
|
}
|
|
}
|
|
|
|
async function getPayload(capture: any) {
|
|
let payloadData = {}
|
|
if (capture.getFullCapture) {
|
|
const fullCapture = await capture.getFullCapture()
|
|
payloadData =
|
|
testKind === 'preprocessor'
|
|
? {
|
|
...(typeof fullCapture.payload === 'object' ? fullCapture.payload : {}),
|
|
...(typeof fullCapture.trigger_extra === 'object' ? fullCapture.trigger_extra : {})
|
|
}
|
|
: fullCapture.payload
|
|
} else {
|
|
payloadData = structuredClone(capture.payloadData)
|
|
}
|
|
return payloadData
|
|
}
|
|
|
|
function deselect() {
|
|
selected = undefined
|
|
dispatch('select', undefined)
|
|
}
|
|
|
|
onDestroy(() => {
|
|
deselect()
|
|
})
|
|
|
|
const captureKindToIcon: Record<string, any> = {
|
|
webhook: Webhook,
|
|
http: Route,
|
|
email: Mail,
|
|
websocket: Unplug,
|
|
kafka: KafkaIcon
|
|
}
|
|
|
|
async function getPropPickerElements(): Promise<HTMLElement[]> {
|
|
return Array.from(
|
|
document.querySelectorAll('[data-schema-picker], [data-schema-picker] *')
|
|
) as HTMLElement[]
|
|
}
|
|
|
|
function handleKeydown(event: KeyboardEvent) {
|
|
if (event.key === 'Escape' && selected) {
|
|
deselect()
|
|
event.stopPropagation()
|
|
event.preventDefault()
|
|
}
|
|
}
|
|
|
|
function handleError(error: { type: string; error: any }) {
|
|
if (error.type === 'delete') {
|
|
sendUserToast(`Failed to delete capture: ${error.error}`, true)
|
|
} else if (error.type === 'load') {
|
|
sendUserToast(`Failed to load captures: ${error.error}`, true)
|
|
}
|
|
}
|
|
|
|
$: path && infiniteList && initLoadCaptures()
|
|
</script>
|
|
|
|
<svelte:window on:keydown={handleKeydown} />
|
|
|
|
<Label label="Trigger captures" {headless} class="h-full {headless ? '' : 'flex flex-col gap-1'}">
|
|
<svelte:fragment slot="header">
|
|
{#if addButton}
|
|
<div class="inline-block">
|
|
<CaptureButton small={true} on:openTriggers />
|
|
</div>
|
|
{/if}
|
|
</svelte:fragment>
|
|
<svelte:fragment slot="action">
|
|
{#if canHavePreprocessor && !isEmpty}
|
|
<div>
|
|
<ToggleButtonGroup
|
|
bind:selected={testKind}
|
|
class="h-full"
|
|
on:selected={(e) => {
|
|
initLoadCaptures(e.detail)
|
|
}}
|
|
>
|
|
<ToggleButton value="main" label={isFlow ? 'Flow' : 'Main'} small />
|
|
<ToggleButton
|
|
value="preprocessor"
|
|
label="Preprocessor"
|
|
small
|
|
tooltip="When the runnable has a preprocessor, it receives additional information about the request"
|
|
/>
|
|
</ToggleButtonGroup>
|
|
</div>
|
|
{/if}
|
|
</svelte:fragment>
|
|
|
|
<div
|
|
class={fullHeight
|
|
? headless
|
|
? 'h-full'
|
|
: 'min-h-0 grow'
|
|
: capturesLength > 7
|
|
? 'h-[300px]'
|
|
: 'h-fit'}
|
|
use:clickOutside={{ capture: false, exclude: getPropPickerElements }}
|
|
on:click_outside={() => {
|
|
if (firstClick) {
|
|
firstClick = false
|
|
return
|
|
}
|
|
deselect()
|
|
}}
|
|
>
|
|
<InfiniteList
|
|
bind:this={infiniteList}
|
|
selectedItemId={selected}
|
|
bind:isEmpty
|
|
on:error={(e) => handleError(e.detail)}
|
|
on:select={(e) => handleSelect(e.detail)}
|
|
bind:length={capturesLength}
|
|
>
|
|
<svelte:fragment slot="columns">
|
|
<colgroup>
|
|
<col class="w-8" />
|
|
<col class="w-16" />
|
|
<col />
|
|
</colgroup>
|
|
</svelte:fragment>
|
|
<svelte:fragment let:item let:hover>
|
|
{@const captureIcon = captureKindToIcon[item.trigger_kind]}
|
|
<SchemaPickerRow
|
|
date={item.created_at}
|
|
payloadData={item.payloadData}
|
|
selected={selected === item.id}
|
|
hovering={hover}
|
|
>
|
|
<svelte:fragment slot="start">
|
|
<div class="center-center">
|
|
<svelte:component this={captureIcon} size={12} />
|
|
</div>
|
|
</svelte:fragment>
|
|
|
|
<svelte:fragment slot="extra">
|
|
{#if canEdit}
|
|
<div class="flex flex-row items-center gap-2 px-2">
|
|
{#if testKind === 'preprocessor' && !hasPreprocessor}
|
|
<CustomPopover noPadding>
|
|
<Button
|
|
size="xs2"
|
|
color="dark"
|
|
disabled
|
|
endIcon={{
|
|
icon: Info
|
|
}}
|
|
wrapperClasses="h-full"
|
|
>
|
|
Apply args
|
|
</Button>
|
|
<svelte:fragment slot="overlay">
|
|
<div class="text-sm p-2 flex flex-col gap-1 items-start">
|
|
<p> You need to add a preprocessor to use preprocessor captures as args </p>
|
|
<Button
|
|
size="xs2"
|
|
color="dark"
|
|
on:click={() => {
|
|
dispatch('addPreprocessor')
|
|
}}
|
|
>
|
|
Add preprocessor
|
|
</Button>
|
|
</div>
|
|
</svelte:fragment>
|
|
</CustomPopover>
|
|
{:else}
|
|
<Button
|
|
size="xs2"
|
|
color={hover || selected === item.id ? 'dark' : 'light'}
|
|
dropdownItems={[
|
|
{
|
|
label: 'Use as input schema',
|
|
onClick: async () => {
|
|
const payloadData = await getPayload(item)
|
|
dispatch('updateSchema', {
|
|
payloadData,
|
|
redirect: true,
|
|
args: true
|
|
})
|
|
},
|
|
disabled: !isFlow || testKind !== 'main'
|
|
}
|
|
].filter((item) => !item.disabled)}
|
|
on:click={async () => {
|
|
const payloadData = await getPayload(item)
|
|
if (isFlow && testKind === 'main') {
|
|
dispatch('testWithArgs', payloadData)
|
|
} else {
|
|
dispatch('applyArgs', {
|
|
kind: testKind,
|
|
args: payloadData
|
|
})
|
|
}
|
|
}}
|
|
disabled={testKind === 'preprocessor' && !hasPreprocessor}
|
|
title={isFlow && testKind === 'main'
|
|
? 'Test flow with args'
|
|
: testKind === 'preprocessor'
|
|
? 'Apply args to preprocessor'
|
|
: 'Apply args to inputs'}
|
|
startIcon={isFlow && testKind === 'main' ? { icon: Play } : {}}
|
|
>
|
|
{isFlow && testKind === 'main' ? 'Test' : 'Apply args'}
|
|
</Button>
|
|
{/if}
|
|
<Button
|
|
size="xs2"
|
|
color="light"
|
|
variant="contained"
|
|
iconOnly
|
|
startIcon={{ icon: Trash2 }}
|
|
loading={item.isDeleting}
|
|
on:click={() => {
|
|
infiniteList?.deleteItem(item.id)
|
|
}}
|
|
btnClasses="hover:text-white hover:bg-red-500 text-red-500"
|
|
/>
|
|
</div>
|
|
{/if}
|
|
</svelte:fragment>
|
|
</SchemaPickerRow>
|
|
</svelte:fragment>
|
|
<svelte:fragment slot="empty">
|
|
<div class="text-center text-xs text-tertiary">No captures yet</div>
|
|
</svelte:fragment>
|
|
</InfiniteList>
|
|
</div>
|
|
</Label>
|