Files
windmill/frontend/src/lib/components/SavedInputs.svelte
T

407 lines
11 KiB
Svelte

<script lang="ts">
import { Button } from '$lib/components/common'
import { InputService, type Input, RunnableType, type CreateInput, Job } from '$lib/gen/index.js'
import { userStore, workspaceStore } from '$lib/stores.js'
import { classNames, displayDate, 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, Save, 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'
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[] = []
let savedInputs: EditableInput[] = []
let selectedInput: Input | null
let jobs: Job[] = []
let loading: boolean = false
let savingInputs = false
const dispatch = createEventDispatcher()
$: runnableId = scriptHash || scriptPath || flowPath || undefined
$: runnableType = scriptHash
? RunnableType.SCRIPT_HASH
: scriptPath
? RunnableType.SCRIPT_PATH
: flowPath
? RunnableType.FLOW_PATH
: undefined
async function loadInputHistory() {
previousInputs = await InputService.getInputHistory({
workspace: $workspaceStore!,
runnableId,
runnableType,
perPage: 10
})
}
async function loadSavedInputs() {
savedInputs = await InputService.listInputs({
workspace: $workspaceStore!,
runnableId,
runnableType,
perPage: 10
})
}
async function saveInput(args: object) {
savingInputs = true
const requestBody: CreateInput = {
name: 'Saved ' + displayDate(new Date()),
args
}
try {
let id = await InputService.createInput({
workspace: $workspaceStore!,
runnableId,
runnableType,
requestBody
})
const input = {
id,
created_by: '',
created_at: new Date().toISOString(),
is_public: false,
...requestBody
}
savedInputs = [input, ...savedInputs]
} catch (err) {
console.error(err)
sendUserToast(`Failed to save Input: ${err}`, true)
}
savingInputs = false
}
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 && jobs && (scriptHash || scriptPath || flowPath)) {
console.log('loading inputs')
loadInputHistory()
loadSavedInputs()
}
}
function selectArgs(selected_args: any) {
dispatch('selected_args', selected_args)
}
</script>
<JobLoader
bind:jobs
path={runnableId ?? null}
isSkipped={false}
jobKindsCat="jobs"
jobKinds="all"
user={null}
folder={null}
success="running"
argFilter={undefined}
bind:loading
syncQueuedRunsCount={false}
refreshRate={10000}
computeMinAndMax={undefined}
/>
<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}
<Button
on:click={() => saveInput(args)}
disabled={!isValid}
loading={savingInputs}
startIcon={{ icon: Save }}
color="light"
size="xs"
>
<span>Save Current Input</span>
</Button>
{/if}
</div>
<div class="w-full flex flex-col gap-2 h-full overflow-y-auto p">
{#if savedInputs.length > 0}
{#each savedInputs as i}
<button
class={classNames(
`w-full flex items-center group justify-between gap-4 py-2 px-4 text-left border rounded-md hover:bg-surface-hover transition-all`,
selectedInput === i ? 'border-blue-500 bg-blue-50 dark:bg-blue-900' : ''
)}
on:click={() => {
if (!i.isEditing) {
if (selectedInput === i) {
selectedInput = null
} else {
selectedInput = i
}
}
}}
>
<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 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'
)}
>
<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 class="col-span-2">
{i.created_by}
</div>
<div
class="whitespace-nowrap col-span-3 !text-tertiary !text-2xs overflow-hidden text-ellipsis flex-shrink text-center"
>
<TimeAgo date={i.created_at ?? ''} />
</div>
<div class="col-span-2">
<a
target="_blank"
href="/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-left text-tertiary text-xs">No running runs</div>
{/if}
</div>
<div class="w-full flex flex-col gap-1 p-0 h-full overflow-y-auto">
{#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={() => {
if (selectedInput === i) {
selectedInput = null
} else {
selectedInput = i
}
}}
>
<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 class="col-span-2">
{i.created_by}
</div>
<div
class="whitespace-nowrap col-span-3 !text-tertiary !text-2xs overflow-hidden text-ellipsis flex-shrink text-center"
>
<TimeAgo date={i.created_at ?? ''} />
</div>
<div class="col-span-2">
<a
target="_blank"
href="/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="text-sm font-semibold">Preview</div>
<div class="w-full flex flex-col">
<Button
color="blue"
btnClasses="w-full"
size="sm"
spacingSize="xl"
on:click={() => selectArgs(selectedInput?.args)}
disabled={Object.keys(selectedInput?.args || {}).length === 0}
>
<ArrowLeftIcon class="w-4 h-4 mr-2" />
Use Input
</Button>
</div>
<div class="w-full min-h-0 grow overflow-auto">
{#if Object.keys(selectedInput?.args || {}).length > 0}
<div class=" overflow-auto h-full p-2">
<ObjectViewer json={selectedInput?.args} />
</div>
{:else}
<div class="text-center text-tertiary">
Select an Input to preview scripts arguments
</div>
{/if}
</div>
</div>
</div></Pane
>
</Splitpanes>
</div>