Files
windmill/frontend/src/lib/components/SavedInputs.svelte
T
310f37a682 feat: migrate to svelte5 + vite6 (#4813)
* runs on svelte 5

* Line component from svelte-chartjs

* Replaced all svelte-chartjs occurrences with custom wrapper

* Fix props mistake

* Fix illegal table structures

* self-closing-tags fix

* aria labels

* Fixed trivial warnings and errors

* @tanstack/svelte-table fix

* upgrade to vite 6

* svelte-kit sync before running svelte-check

* Remove on:clear which is actually on:removeAll and already handled by on:change

* fix worker tags not displaying in Autoscaling

* Try to fix svelte-kit sync not working during CI

* remove warnings

* Fix add flow page crashing

* access worldStore before assignment fix

* fix infinite recursions in App Editor

* Replaced JSON.stringify with proper deepEqual

* component mount api changed (no longer classes)

* fix ci errors

* Fix infinite loops in background runnable panel

* factored effect on deep equal logic in onObjChange

* fix "Add" not working in AgGrid Table

* Replaced legacy component.$set api

* Fix multiselect infinite value reaction

* Fix flow input fields resetting when opening their edit tab

* fix date input resetting when typing year

* Remove !p-0 affecting subgrid dotted borders

* fix missing debounceTemplate causing hundreds of updates

* Fix AgGrid action refreshes and disppearing

* resolve getItems generating random ids every rerun

* fix cannot access items before init

* fix sort lambda arguments being undefined

* Revert "Remove !p-0 affecting subgrid dotted borders"

This reverts commit c62809bb45d682a48376b071680645ed4e1c601b.

* fix input not updating in decision tree editor

* Update frontend/src/lib/components/schema/EditableSchemaWrapper.svelte

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Re-added padding affecting subgrid dotted borders (#5479)

* remove !p-0 in preset components

* removed extra padding on accordion tabs subgrid

* Fix non-reactive SchemaForm

* dirty fix for the oneOf bug

* Fix warnings and update svelte-exmarkdown for svelte 5

* fix dnd not working

* don't mount component like objects

---------

Co-authored-by: Diego Imbert <diegoimbert@protonmail.com>
Co-authored-by: Diego Imbert <70353967+diegoimbert@users.noreply.github.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-04-02 21:33:50 +00:00

455 lines
13 KiB
Svelte

<script lang="ts">
import { Button } from '$lib/components/common'
import {
InputService,
type Input,
type RunnableType,
type Job,
JobService
} from '$lib/gen/index.js'
import { userStore, workspaceStore } from '$lib/stores.js'
import { base } from '$lib/base'
import { classNames, displayDateOnly, sendUserToast } from '$lib/utils.js'
import { createEventDispatcher } from 'svelte'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import ObjectViewer from './propertyPicker/ObjectViewer.svelte'
import { ArrowLeftIcon, Edit, ExternalLink, X } from 'lucide-svelte'
import Toggle from './Toggle.svelte'
import Tooltip from './Tooltip.svelte'
import TimeAgo from './TimeAgo.svelte'
import JobLoader from './runs/JobLoader.svelte'
import Skeleton from './common/skeleton/Skeleton.svelte'
import SaveInputsButton from './SaveInputsButton.svelte'
export let scriptHash: string | null = null
export let scriptPath: string | null = null
export let flowPath: string | null = null
export let canSaveInputs: boolean = true
// Are the current Inputs valid and able to be saved?
export let isValid: boolean
export let args: object
interface EditableInput extends Input {
isEditing?: boolean
isSaving?: boolean
}
let previousInputs: Input[] | undefined = undefined
let savedInputs: EditableInput[] | undefined = undefined
let selectedInput: Input | null
let jobs: Job[] = []
let loading: boolean = false
const dispatch = createEventDispatcher()
$: runnableId = scriptHash || scriptPath || flowPath || undefined
let runnableType: RunnableType | undefined = undefined
$: runnableType = scriptHash
? 'ScriptHash'
: scriptPath
? 'ScriptPath'
: flowPath
? 'FlowPath'
: undefined
let hasAlreadyFailed = false
async function loadInputHistory() {
try {
previousInputs = await InputService.getInputHistory({
workspace: $workspaceStore!,
runnableId,
runnableType,
perPage: 10
})
} catch (e) {
console.error(e)
if (hasAlreadyFailed) return
hasAlreadyFailed = true
sendUserToast(`Failed to load input history: ${e}`, true)
}
}
async function loadSavedInputs() {
savedInputs = await InputService.listInputs({
workspace: $workspaceStore!,
runnableId,
runnableType,
perPage: 10
})
}
async function updateInput(input: EditableInput) {
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
}
async function deleteInput(input: Input) {
try {
await InputService.deleteInput({
workspace: $workspaceStore!,
input: input.id
})
savedInputs = (savedInputs ?? []).filter((i) => i.id !== input.id)
if (selectedInput === input) {
selectedInput = null
}
} catch (err) {
console.error(err)
sendUserToast(`Failed to delete Input: ${err}`, true)
}
}
$: {
if ($workspaceStore && (scriptHash || scriptPath || flowPath)) {
loadInputHistory()
loadSavedInputs()
}
}
let previewArgs: any = undefined
function selectArgs(selected_args: any) {
previewArgs = selected_args
}
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
})
}
</script>
{#if runnableId}
<JobLoader
bind:jobs
path={runnableId}
isSkipped={false}
jobKindsCat="all"
user={null}
label={null}
folder={null}
concurrencyKey={null}
tag={null}
success="running"
argFilter={undefined}
bind:loading
syncQueuedRunsCount={false}
refreshRate={10000}
computeMinAndMax={undefined}
perPage={5}
/>
{/if}
<div class="min-w-[300px] h-full">
<Splitpanes horizontal={true}>
<Pane>
<div class="w-full flex flex-col gap-4 p-2">
<div class="w-full flex justify-between items-center gap-4 flex-wrap">
<span class="text-sm font-semibold flex-shrink-0"
>Saved Inputs<Tooltip
>Shared inputs are available to anyone with access to the script</Tooltip
></span
>
{#if canSaveInputs}
<SaveInputsButton
{args}
disabled={!isValid}
{runnableId}
{runnableType}
on:update={() => {
loadSavedInputs()
}}
/>
{/if}
</div>
<div class="w-full flex flex-col gap-1 h-full overflow-y-auto p">
{#if savedInputs === undefined}
<Skeleton layout={[[8]]} />
{:else if savedInputs?.length > 0}
{#each savedInputs as i}
<button
class={classNames(
`w-full flex items-center text-sm group justify-between gap-4 py-1.5 px-4 text-left border rounded-sm hover:bg-surface-hover transition-all`,
selectedInput === i ? 'border-blue-500 bg-blue-50 dark:bg-blue-900' : ''
)}
on:click={async () => {
if (!i.isEditing) {
if (selectedInput === i) {
selectedInput = null
} else {
selectedInput = i
}
}
selectArgs(await loadLargeArgs(i.id, true, false))
}}
>
<div class="w-full h-full items-center justify-between flex gap-1 min-w-0">
{#if i.isEditing}
<form
on:submit={() => {
updateInput(i)
i.isEditing = false
i.isSaving = false
}}
class="w-full"
>
<input type="text" bind:value={i.name} class="text-secondary" />
</form>
{:else}
<small
class="whitespace-nowrap overflow-hidden text-ellipsis flex-shrink text-left"
>
{i.name}
</small>
{/if}
{#if i.created_by == $userStore?.username || $userStore?.is_admin || $userStore?.is_super_admin}
<div class="items-center flex gap-2">
{#if !i.isEditing}
<div class="group-hover:block hidden -my-2">
<Toggle
size="xs"
options={{ right: 'shared' }}
bind:checked={i.is_public}
on:change={() => {
updateInput(i)
}}
/>
</div>
{/if}
<Button
loading={i.isSaving}
color="light"
size="xs"
variant="border"
spacingSize="xs2"
btnClasses={'group-hover:block hidden -my-2'}
on:click={(e) => {
e.stopPropagation()
i.isEditing = !i.isEditing
if (!i.isEditing) {
updateInput(i)
i.isSaving = false
}
}}
>
<Edit class="w-4 h-4" />
</Button>
<Button
color="red"
size="xs"
spacingSize="xs2"
variant="border"
btnClasses={i.isEditing ? 'block' : 'group-hover:block hidden -my-2'}
on:click={() => deleteInput(i)}
>
<X class="w-4 h-4" />
</Button>
</div>
{:else}
<span class="text-xs text-tertiary">By {i.created_by}</span>
{/if}
</div>
</button>
{/each}
{:else}
<div class="text-center text-tertiary">No saved Inputs</div>
{/if}
</div>
</div>
</Pane>
<Pane>
<div class="w-full flex flex-col gap-4 p-2">
<span class="text-sm font-semibold">Previous runs</span>
<div class="w-full flex flex-col gap-1 p-0 h-full overflow-y-auto">
{#if loading && (jobs == undefined || jobs?.length == 0)}
<div class="text-left text-tertiary text-xs">Loading current runs...</div>
{:else if jobs?.length > 0}
{#each jobs as i (i.id)}
<button
class={classNames(
`w-full flex items-center justify-between gap-4 py-2 px-4 text-left border rounded-sm hover:bg-surface-hover transition-a`,
'border-orange-400'
)}
on:click={async () => {
if (!$workspaceStore) {
return
}
const args = await JobService.getJobArgs({
workspace: $workspaceStore,
id: i.id
})
selectedInput = {
id: i.id,
name: 'Running job: ' + i.id,
created_at: i.created_at ?? '',
created_by: i.created_by ?? '',
is_public: true
}
selectArgs(args)
}}
>
<div
class="w-full h-full items-center text-xs font-normal grid grid-cols-8 gap-4 min-w-0"
>
<div class="">
<div class="rounded-full w-2 h-2 bg-orange-400 animate-pulse"></div>
</div>
<div class="col-span-2 truncate">
{i.created_by}
</div>
<div
class="whitespace-nowrap col-span-3 !text-tertiary !text-2xs overflow-hidden text-ellipsis flex-shrink text-center"
>
<TimeAgo noDate date={i.started_at ?? ''} />
</div>
<div class="col-span-2">
<a
target="_blank"
href="{base}/run/{i.id}?workspace={$workspaceStore}"
class="text-right float-right text-secondary"
title="See run detail in a new tab"
>
<ExternalLink size={16} />
</a>
</div>
</div>
</button>
{/each}
{#if jobs?.length == 5}
<div class="text-left text-tertiary text-xs"
>... there may be more runs not displayed here as the limit is 5</div
>
{/if}
{:else}
<div class="text-left text-tertiary text-xs">No job currently running</div>
{/if}
</div>
<div class="w-full flex flex-col gap-1 p-0 h-full overflow-y-auto">
{#if previousInputs === undefined}
<Skeleton layout={[[8]]} />
{:else if previousInputs?.length > 0}
{#each previousInputs as i (i.id)}
<button
class={classNames(
`w-full flex items-center justify-between gap-4 py-2 px-4 text-left border rounded-sm hover:bg-surface-hover transition-a`,
selectedInput === i ? 'border-blue-500 bg-blue-50 dark:bg-blue-900' : ''
)}
on:click={async () => {
if (selectedInput === i) {
selectedInput = null
} else {
selectedInput = i
}
selectArgs(await loadLargeArgs(i.id, false, false))
}}
>
<div
class="w-full h-full items-center text-xs font-normal grid grid-cols-8 gap-4 min-w-0"
>
<div class="">
<div class="rounded-full w-2 h-2 {i.success ? 'bg-green-400' : 'bg-red-400'}"></div>
</div>
<div class="col-span-2 truncate" title={i.created_by}>
{i.created_by}
</div>
<div
class="whitespace-nowrap col-span-2 !text-tertiary !text-2xs overflow-hidden text-ellipsis flex-shrink text-center"
>
{displayDateOnly(new Date(i.created_at))}
</div>
<div
class="whitespace-nowrap col-span-2 !text-tertiary !text-2xs overflow-hidden text-ellipsis flex-shrink text-center"
>
<TimeAgo noDate date={i.created_at ?? ''} />
</div>
<div class="col-span-1">
<a
target="_blank"
href="{base}/run/{i.id}?workspace={$workspaceStore}"
class="text-right float-right text-secondary"
title="See run detail in a new tab"
>
<ExternalLink size={16} />
</a>
</div>
</div>
</button>
{/each}
{:else}
<div class="text-center text-tertiary">No previous Runs</div>
{/if}
</div>
</div>
</Pane>
<Pane>
<div class="h-full overflow-hidden min-h-0 flex flex-col justify-between">
<div class="w-full flex flex-col min-h-0 gap-2 px-2 py-2 grow">
<div class="w-full flex flex-col">
<Button
color="blue"
btnClasses="w-full"
size="sm"
spacingSize="xl"
on:click={async () => {
dispatch('selected_args', await loadLargeArgs(selectedInput?.id, undefined, false))
}}
disabled={!selectedInput}
>
<ArrowLeftIcon class="w-4 h-4 mr-2" />
Use input
</Button>
</div>
<div class="w-full min-h-0 grow overflow-auto">
{#if typeof previewArgs == 'string' && previewArgs == 'WINDMILL_TOO_BIG'}
<div class="text-secondary mt-2">
Payload too big to preview but can still be loaded</div
>
{:else if Object.keys(previewArgs || {}).length > 0}
<div class=" overflow-auto h-full p-2">
<ObjectViewer json={previewArgs} />
</div>
{:else}
<div class="text-center text-tertiary">
Select an Input to preview scripts arguments
</div>
{/if}
</div>
</div>
</div></Pane
>
</Splitpanes>
</div>