mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 08:03:50 +00:00
* improve flow detail page * Do not display seconds for last edit * expand graph when possible * nit * make flowGraph min height reactive * Add flow graph tab when chat mode enabled * improve script detail layout * nit * nit * Update frontend/src/lib/components/TimeAgo.svelte Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> * Update frontend/src/lib/components/TimeAgo.svelte Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> * Update frontend/src/lib/components/TimeAgo.svelte Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> * Update frontend/src/lib/components/TimeAgo.svelte Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> * nit --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
357 lines
9.3 KiB
Svelte
357 lines
9.3 KiB
Svelte
<script lang="ts">
|
|
import { Button } from '$lib/components/common'
|
|
import { InputService, type Input, type RunnableType } from '$lib/gen/index.js'
|
|
import { userStore, workspaceStore } from '$lib/stores.js'
|
|
import { sendUserToast } from '$lib/utils.js'
|
|
import { createEventDispatcher, onDestroy, untrack } from 'svelte'
|
|
import { Trash2, Save, Pencil } from 'lucide-svelte'
|
|
import Toggle from './Toggle.svelte'
|
|
import { Cell } from './table/index'
|
|
import SaveInputsButton from '$lib/components/SaveInputsButton.svelte'
|
|
import Popover from '$lib/components/Popover.svelte'
|
|
import InfiniteList from './InfiniteList.svelte'
|
|
import { twMerge } from 'tailwind-merge'
|
|
import SavedInputsPickerViewer from './SavedInputsPickerViewer.svelte'
|
|
|
|
interface Props {
|
|
previewArgs?: any
|
|
runnableId?: string | undefined
|
|
runnableType?: RunnableType | undefined
|
|
isValid?: boolean
|
|
noButton?: boolean
|
|
jsonView?: boolean
|
|
limitPayloadSize?: boolean
|
|
}
|
|
|
|
let {
|
|
previewArgs = undefined,
|
|
runnableId = undefined,
|
|
runnableType = undefined,
|
|
isValid = false,
|
|
noButton = false,
|
|
jsonView = false,
|
|
limitPayloadSize = false
|
|
}: Props = $props()
|
|
|
|
interface EditableInput extends Input {
|
|
isEditing?: boolean
|
|
isSaving?: boolean
|
|
isNew?: boolean
|
|
isDeleting?: boolean
|
|
payloadData?: any
|
|
getFullPayload?: () => Promise<any>
|
|
}
|
|
|
|
let infiniteList: InfiniteList | null = $state(null)
|
|
let draft = $state(true)
|
|
let selectedInput: string | null = $state(null)
|
|
let isEditing: EditableInput | null = $state(null)
|
|
let viewerOpen = $state(false)
|
|
let openStates: Record<string, boolean> = $state({})
|
|
let clientWidth: number = $state(0)
|
|
|
|
const dispatch = createEventDispatcher()
|
|
|
|
async function updateInput(input: EditableInput | null) {
|
|
if (!input) return
|
|
input.isSaving = true
|
|
|
|
try {
|
|
await InputService.updateInput({
|
|
workspace: $workspaceStore!,
|
|
requestBody: {
|
|
id: input.id,
|
|
name: input.name,
|
|
is_public: input.is_public
|
|
}
|
|
})
|
|
} catch (err) {
|
|
console.error(err)
|
|
sendUserToast(`Failed to update Input: ${err}`, true)
|
|
}
|
|
|
|
input.isSaving = false
|
|
}
|
|
|
|
function initLoadInputs() {
|
|
const loadInputsPageFn = async (page: number, perPage: number) => {
|
|
const inputs = await InputService.listInputs({
|
|
workspace: $workspaceStore!,
|
|
runnableId,
|
|
runnableType,
|
|
page,
|
|
perPage
|
|
})
|
|
const inputsWithPayload = await Promise.all(
|
|
inputs.map(async (input) => {
|
|
const payloadData = await loadLargeArgs(input.id, undefined, false)
|
|
if (payloadData === 'WINDMILL_TOO_BIG') {
|
|
return {
|
|
...input,
|
|
payloadData: 'WINDMILL_TOO_BIG',
|
|
getFullPayload: () => loadLargeArgs(input.id, undefined, true)
|
|
}
|
|
}
|
|
return {
|
|
...input,
|
|
payloadData
|
|
}
|
|
})
|
|
)
|
|
return inputsWithPayload
|
|
}
|
|
infiniteList?.setLoader(loadInputsPageFn)
|
|
|
|
const deleteInputFn = async (id: string) => {
|
|
await InputService.deleteInput({
|
|
workspace: $workspaceStore!,
|
|
input: id
|
|
})
|
|
}
|
|
infiniteList?.setDeleteItemFn(deleteInputFn)
|
|
}
|
|
|
|
async function loadLargeArgs(
|
|
id: string | undefined,
|
|
input: boolean | undefined,
|
|
allowLarge: boolean
|
|
): Promise<any> {
|
|
if (!id) return
|
|
return await InputService.getArgsFromHistoryOrSavedInput({
|
|
jobOrInputId: id,
|
|
workspace: $workspaceStore!,
|
|
input,
|
|
allowLarge
|
|
})
|
|
}
|
|
|
|
let selectedArgs: any = undefined
|
|
async function handleSelect(input: EditableInput) {
|
|
if (input.isEditing) return
|
|
|
|
if (selectedInput === input.id) {
|
|
resetSelected(true)
|
|
} else {
|
|
selectedInput = input.id
|
|
if (input.payloadData === 'WINDMILL_TOO_BIG') {
|
|
const fullPayload = await input.getFullPayload?.()
|
|
dispatch('select', fullPayload)
|
|
} else {
|
|
selectedArgs = structuredClone($state.snapshot(input.payloadData) ?? {})
|
|
dispatch('select', selectedArgs)
|
|
}
|
|
}
|
|
}
|
|
|
|
onDestroy(() => {
|
|
resetSelected(true)
|
|
})
|
|
|
|
function handleKeydown(event: KeyboardEvent) {
|
|
if (event.key === 'Escape' && isEditing) {
|
|
setEditing(null)
|
|
event.stopPropagation()
|
|
event.preventDefault()
|
|
} else if (event.key === 'Escape' && selectedInput) {
|
|
resetSelected(true)
|
|
event.stopPropagation()
|
|
event.preventDefault()
|
|
}
|
|
|
|
if (event.key === 'Enter' && isEditing) {
|
|
updateInput(isEditing).then(() => {
|
|
setEditing(null)
|
|
})
|
|
event.stopPropagation()
|
|
event.preventDefault()
|
|
}
|
|
}
|
|
|
|
function setEditing(input: EditableInput | null) {
|
|
isEditing = input
|
|
dispatch('isEditing', !!input)
|
|
}
|
|
|
|
function handleError(error: { type: string; error: any }) {
|
|
if (error.type === 'delete') {
|
|
sendUserToast(`Failed to delete saved input: ${error.error}`, true)
|
|
} else if (error.type === 'load') {
|
|
sendUserToast(`Failed to load saved inputs: ${error.error}`, true)
|
|
}
|
|
}
|
|
|
|
export function refresh() {
|
|
infiniteList?.loadData('refresh')
|
|
}
|
|
|
|
export function resetSelected(dispatchEvent?: boolean) {
|
|
selectedInput = null
|
|
selectedArgs = undefined
|
|
if (dispatchEvent) {
|
|
dispatch('select', undefined)
|
|
}
|
|
}
|
|
|
|
function updateViewerOpenState(itemId: string, isOpen: boolean) {
|
|
openStates[itemId] = isOpen
|
|
viewerOpen = Object.values(openStates).some((state) => state)
|
|
}
|
|
|
|
$effect(() => {
|
|
$workspaceStore &&
|
|
runnableId &&
|
|
runnableType &&
|
|
(infiniteList && untrack(() => initLoadInputs()), (draft = false))
|
|
})
|
|
</script>
|
|
|
|
<svelte:window onkeydown={handleKeydown} />
|
|
|
|
<div class="w-full flex flex-col gap-1 h-full overflow-y-auto pb-4">
|
|
{#if !noButton}
|
|
<div>
|
|
<Popover class="w-full" placement="bottom" disablePopup={runnableId && previewArgs}>
|
|
{#snippet text()}
|
|
{#if !runnableId}
|
|
Save draft first before you can save inputs
|
|
{:else if !previewArgs}
|
|
Add inputs before saving
|
|
{/if}
|
|
{/snippet}
|
|
<SaveInputsButton
|
|
{runnableId}
|
|
{runnableType}
|
|
args={previewArgs ?? {}}
|
|
disabled={!previewArgs || !runnableId || !isValid || jsonView}
|
|
on:update={() => {
|
|
refresh()
|
|
}}
|
|
showTooltip={true}
|
|
/>
|
|
</Popover>
|
|
</div>
|
|
{/if}
|
|
<div class="grow min-h-0" data-schema-picker>
|
|
{#if !draft}
|
|
<InfiniteList
|
|
bind:this={infiniteList}
|
|
selectedItemId={selectedInput}
|
|
on:error={(e) => handleError(e.detail)}
|
|
on:select={(e) => handleSelect(e.detail)}
|
|
>
|
|
{#snippet columns()}
|
|
<colgroup>
|
|
<col class="w-8" />
|
|
<col />
|
|
</colgroup>
|
|
{/snippet}
|
|
{#snippet children({ item, hover })}
|
|
{@const editOptions =
|
|
item.created_by == $userStore?.username ||
|
|
$userStore?.is_admin ||
|
|
$userStore?.is_super_admin}
|
|
<Cell>
|
|
<div class="center-center">
|
|
<Save size={12} />
|
|
</div>
|
|
</Cell>
|
|
<Cell>
|
|
<div
|
|
class="w-full flex items-center text-sm justify-between gap-4 py-1 text-left transition-all"
|
|
bind:clientWidth
|
|
>
|
|
<div class="w-full h-full items-center justify-between flex gap-1 min-w-0">
|
|
{#if isEditing && isEditing.id === item.id}
|
|
<form
|
|
onsubmit={() => {
|
|
updateInput(isEditing)
|
|
setEditing(null)
|
|
}}
|
|
class="w-full"
|
|
>
|
|
<input type="text" bind:value={isEditing.name} class="text-secondary" />
|
|
</form>
|
|
{:else}
|
|
<small
|
|
class="whitespace-nowrap overflow-hidden text-ellipsis flex-shrink text-left"
|
|
>
|
|
{item.name}
|
|
</small>
|
|
{/if}
|
|
|
|
<div class="items-center flex gap-2">
|
|
<SavedInputsPickerViewer
|
|
payloadData={item.payloadData}
|
|
{limitPayloadSize}
|
|
{hover}
|
|
{viewerOpen}
|
|
on:openChange={(e) => {
|
|
updateViewerOpenState(item.id, e.detail)
|
|
}}
|
|
maxWidth={clientWidth}
|
|
{editOptions}
|
|
/>
|
|
{#if editOptions}
|
|
{#if !isEditing || isEditing?.id !== item.id}
|
|
<div class={hover || openStates[item.id] ? 'block -my-2' : 'hidden'}>
|
|
<Toggle
|
|
size="xs"
|
|
options={{ right: 'shared' }}
|
|
checked={item.is_public}
|
|
on:change={(e) => {
|
|
updateInput({ ...item, is_public: e.detail })
|
|
}}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
|
|
<Button
|
|
loading={isEditing?.id === item.id && isEditing?.isSaving}
|
|
unifiedSize="sm"
|
|
variant="subtle"
|
|
spacingSize="xs2"
|
|
btnClasses={hover || openStates[item.id] ? 'block -my-2 ' : 'hidden'}
|
|
on:click={(e) => {
|
|
e.stopPropagation()
|
|
if (isEditing?.id === item.id) {
|
|
updateInput(isEditing)
|
|
setEditing(null)
|
|
} else {
|
|
setEditing(item)
|
|
}
|
|
}}
|
|
startIcon={{ icon: Pencil }}
|
|
></Button>
|
|
<Button
|
|
unifiedSize="sm"
|
|
variant="subtle"
|
|
btnClasses={twMerge(
|
|
isEditing?.id === item.id || hover || openStates[item.id]
|
|
? 'block -my-2'
|
|
: 'hidden'
|
|
)}
|
|
destructive
|
|
on:click={() => {
|
|
infiniteList?.deleteItem(item.id)
|
|
}}
|
|
startIcon={{ icon: Trash2 }}
|
|
iconOnly
|
|
></Button>
|
|
{:else}
|
|
<span class="text-xs text-primary px-2 w-28 truncate" title={item.created_by}
|
|
>{item.created_by}</span
|
|
>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Cell>
|
|
{/snippet}
|
|
{#snippet empty()}
|
|
<div class="text-center text-xs text-primary mt-2">No saved Inputs</div>
|
|
{/snippet}
|
|
</InfiniteList>
|
|
{/if}
|
|
</div>
|
|
</div>
|