UX improvements for run script

This commit is contained in:
Ruben Fiszel
2022-11-06 16:08:10 +01:00
parent bff8331eb0
commit 8fd0e23c80
16 changed files with 790 additions and 565 deletions
+2 -2
View File
@@ -621,8 +621,8 @@ async fn delete_script_by_hash(
require_admin(authed.is_admin, &authed.username)?;
let script = sqlx::query_as::<_, Script>(
"UPDATE script SET content = '', archived = true, deleted = true WHERE hash = $1 AND \
workspace_id = $2RETURNING *",
"UPDATE script SET content = '', archived = true, deleted = true, lock = '', schema = null WHERE hash = $1 AND \
workspace_id = $2 RETURNING *",
)
.bind(&hash.0)
.bind(&w_id)
@@ -22,7 +22,7 @@
class="border-l border-gray-300 px-2 inline-block py-0 my-0"
on:click={() => dispatch('clickDropdown')}
>
<div slot="name" class="py-0 my-0">
<div class="py-0 my-0">
<Icon class="text-white align-top py-0 my-0" data={faSortDown} />
</div>
</Dropdown>
+6 -3
View File
@@ -15,6 +15,7 @@
// This can cause the dropdown to be hidden if it is in an overflow-hidden div.
// In that case, set relative to false and control the dropdown positioning from the div
export let relative = true
export let left = true
const dispatch = createEventDispatcher()
@@ -33,14 +34,16 @@
on:click_outside={handleClickOutsideMenu}
>
<Button color="light" size="xs" btnClasses="!text-blue-500 bg-transparent" on:click={toggle}>
{#if !name}
{#if !$$slots}
<Icon data={faEllipsisH} scale={1.2} />
{:else}
{name}
<slot />
{/if}
</Button>
<div
class="flex flex-col z-50 origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none {open
class="flex flex-col z-50 origin-top-right absolute {left
? 'right-0'
: 'left-0'} mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none {open
? 'visible'
: 'hidden'}"
role="menu"
@@ -185,7 +185,7 @@
<UnsavedConfirmationModal />
<div class="flex flex-col flex-1 h-full">
<div class="flex flex-col flex-1 h-screen">
<!-- Nav between steps-->
<div class="justify-between flex flex-row w-full py-2 px-4 space-x-4">
<div class="flex flex-row space-x-2">
@@ -26,6 +26,8 @@
}
export let tab: 'ui' | 'json' | 'schema' = 'ui'
export let noSummary = false
let open: { [id: number]: boolean } = {}
if (initialOpen) {
open[initialOpen] = true
@@ -44,8 +46,10 @@
<svelte:fragment slot="content">
<TabContent value="ui">
<div class="flow-root w-full pb-4">
<h2 class="my-4">{flow.summary}</h2>
<SvelteMarkdown source={flow.description ?? ''} />
{#if !noSummary}
<h2 class="my-4">{flow.summary}</h2>
<SvelteMarkdown source={flow.description ?? ''} />
{/if}
<p class="font-black text-lg w-full my-4">
<span>Inputs</span>
+68 -29
View File
@@ -1,16 +1,36 @@
<script lang="ts">
import { page } from '$app/stores'
import { decodeArgs, decodeState, getToday } from '$lib/utils'
import {
decodeArgs,
defaultIfEmptyString,
displayDaysAgo,
emptyString,
getToday,
truncateHash
} from '$lib/utils'
import { slide } from 'svelte/transition'
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
import SvelteMarkdown from 'svelte-markdown'
import SchemaForm from './SchemaForm.svelte'
import Tooltip from './Tooltip.svelte'
import type { Schema } from '$lib/common'
import { Button } from './common'
import { Badge, Button } from './common'
import SharedBadge from './SharedBadge.svelte'
export let runnable: { summary?: string; schema?: Schema; description?: string } | undefined
export let runnable:
| {
summary?: string
schema?: Schema
description?: string
path?: string
is_template?: boolean
hash?: string
kind?: string
can_write?: boolean
created_at?: string
created_by?: string
extra_perms?: Record<string, boolean>
}
| undefined
export let runAction: (scheduledForStr: string | undefined, args: Record<string, any>) => void
export let buttonText = 'Run'
export let schedulable = true
@@ -18,7 +38,11 @@
export let args: Record<string, any> = decodeArgs($page.url.searchParams.get('args') ?? undefined)
let isValid = true
export function run() {
runAction(scheduledForStr, args)
}
export let isValid = true
// Run later
let viewOptions = false
@@ -27,34 +51,49 @@
<div class="max-w-6xl">
{#if detailed}
<div class="grid grid-cols-3 gap-2">
<div>
<h2 class="mb-1">Summary</h2>
<div class="mb-2 md:mb-3 text-sm">
{runnable?.summary ? runnable?.summary : 'No summary'}
</div>
</div>
<div class="col-span-2">
<h2 class="mb-1">Description</h2>
<div class="mb-2 md:mb-6">
<div class="prose text-sm">
<SvelteMarkdown
source={runnable?.description ? runnable?.description : 'No description'}
/>
{#if runnable}
<div class="flex flex-row flex-wrap justify-between gap-4">
<div>
<div class="flex flex-col mb-2">
<h1 class="break-words py-2 mr-2">
{defaultIfEmptyString(runnable.summary, runnable.path ?? '')}
</h1>
{#if !emptyString(runnable.summary)}
<h2 class="font-bold pb-4">{runnable.path}</h2>
{/if}
<div class="flex items-center gap-2">
<span class="text-sm text-gray-500">
{#if runnable}
Edited {displayDaysAgo(runnable.created_at || '')} by {runnable.created_by ||
'unknown'}
{/if}
</span>
<Badge color="dark-gray">
{truncateHash(runnable?.hash ?? '')}
</Badge>
{#if runnable?.is_template}
<Badge color="blue">Template</Badge>
{/if}
{#if runnable && runnable.kind !== 'runnable'}
<Badge color="blue">
{runnable?.kind}
</Badge>
{/if}
<SharedBadge
canWrite={runnable.can_write ?? true}
extraPerms={runnable?.extra_perms ?? {}}
/>
</div>
</div>
</div>
</div>
</div>
{:else}
<h1>Loading...</h1>
{/if}
{/if}
{#if runnable?.schema}
{#if detailed}
<h2>
Arguments
<Tooltip>
The optional fields, if left blank, will use the placeholder value as default.
</Tooltip>
</h2>
{/if}
<div class="my-2" />
{#if !runnable.schema.properties || Object.keys(runnable.schema.properties).length === 0}
<div class="text-sm p-4">No arguments</div>
{:else}
@@ -84,8 +84,8 @@
<DrawerContent title="Share {path}" on:close={drawer.closeDrawer}>
<div>
<Alert type="info" title="Owners/Editors/Readers"
>Owner is the user or group that is prefix of a given path. Sharing allow other users or
group to be able to read or write to this item without being an owner.</Alert
>Owner is the user or group that is prefix of its path. Sharing allow other users or group
to be able to read or write to this item without being an owner.</Alert
>
<div class="flex flex-row flex-wrap pb-0 my-5 items-end">
<div class="flex gap-4 mr-2 flex-row">
@@ -122,7 +122,7 @@
<Toggle bind:checked={write} />
</label>
</div>
<div>
<div class="mt-2">
<Button size="sm" on:click={() => addAcl(newOwner, write)}>Add permission</Button>
</div>
</div>
@@ -2,7 +2,7 @@
export let applyPageWidth = false
export let stickToTop = false
$: wide = applyPageWidth ? 'max-w-6xl mx-auto px-4 sm:px-6 md:px-8 ' : ''
$: wide = applyPageWidth ? 'max-w-6xl mx-auto px-4 sm:px-6 md:px-8' : ''
let scrollY: number
</script>
@@ -10,8 +10,8 @@
<svelte:window bind:scrollY />
<div
class={'bg-white py-3 ' +
(stickToTop ? 'sticky top-0 ' + (scrollY >= 30 ? 'border-b ' : '') : '') +
class={'bg-white py-3 w-full ' +
(stickToTop ? 'lg:sticky lg:top-0 ' + (scrollY >= 30 ? 'border-b ' : '') : '') +
$$props.class}
>
<div class={'w-full flex flex-wrap justify-between items-center gap-4 ' + wide}>
@@ -4,7 +4,7 @@
import { truncateHash } from '$lib/utils'
import { faPencil, faPlay } from '@fortawesome/free-solid-svg-icons'
import { Button, Badge, ActionRow } from '$lib/components/common'
import { Button, Badge } from '$lib/components/common'
export let script: Script
</script>
+5 -1
View File
@@ -194,8 +194,12 @@ export function canWrite(
return false
}
export function emptyString(str: string | undefined | null): boolean {
return str === undefined || str === null || str === ''
}
export function defaultIfEmptyString(str: string | undefined, dflt: string): string {
return str == undefined || str == '' ? dflt : str
return emptyString(str) ? dflt : str!
}
export function removeKeysWithEmptyValues(obj: any): any {
+11 -5
View File
@@ -34,7 +34,7 @@
let innerWidth = window.innerWidth
$: innerWidth < 1248 && innerWidth > 768 && (isCollapsed = true)
$: innerWidth < 1248 && innerWidth >= 768 && (isCollapsed = true)
$: (innerWidth >= 1248 || innerWidth < 768) && (isCollapsed = false)
</script>
@@ -100,7 +100,10 @@
</button>
</div>
<div class="bg-blue-500 h-full">
<div class="flex items-center gap-x-2 flex-shrink-0 p-4 font-extrabold text-white w-40">
<div
class="flex items-center gap-x-2 flex-shrink-0 p-4 font-extrabold text-white w-10"
class:w-40={!isCollapsed}
>
<WindmillIcon white={true} height="20px" width="20px" />
{#if !isCollapsed}Windmill{/if}
</div>
@@ -131,7 +134,10 @@
isCollapsed = !isCollapsed
}}
>
<div class="flex items-center gap-x-2 flex-shrink-0 p-4 font-extrabold text-white w-40">
<div
class="flex items-center gap-x-2 flex-shrink-0 p-4 font-extrabold text-white w-14"
class:w-40={!isCollapsed}
>
<WindmillIcon white={true} height="20px" width="20px" />
{#if !isCollapsed}Windmill{/if}
</div>
@@ -164,9 +170,9 @@
</div>
</div>
</div>
<div class={classNames('flex flex-col flex-1', isCollapsed ? 'md:pl-12' : 'md:pl-48')}>
<div class={classNames('w-full flex flex-col flex-1', isCollapsed ? 'md:pl-12' : 'md:pl-48')}>
<main>
<div class="w-full h-screen overflow-auto">
<div class="w-full relative">
<div
class="py-2 px-2 sm:px-4 md:px-8 flex justify-between items-center shadow-sm max-w-6xl mx-auto md:hidden"
>
+174 -184
View File
@@ -15,7 +15,8 @@
sendUserToast,
defaultIfEmptyString,
flowToHubUrl,
copyToClipboard
copyToClipboard,
emptyString
} from '$lib/utils'
import {
faPlay,
@@ -38,10 +39,8 @@
import Dropdown from '$lib/components/Dropdown.svelte'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import FlowViewer from '$lib/components/FlowViewer.svelte'
import ObjectViewer from '$lib/components/propertyPicker/ObjectViewer.svelte'
import { Button, ActionRow, Skeleton, Badge } from '$lib/components/common'
import UserSettings from '$lib/components/UserSettings.svelte'
import ArgInput from '$lib/components/ArgInput.svelte'
import JobArgs from '$lib/components/JobArgs.svelte'
import CronInput from '$lib/components/CronInput.svelte'
@@ -101,193 +100,184 @@
<UserSettings bind:this={userSettings} />
<Skeleton
class="!max-w-6xl !px-4 sm:!px-6 md:!px-8"
loading={!flow}
layout={[0.75, [2, 0, 2], 2.25, [{ h: 1.5, w: 40 }], 0.2, [{ h: 1, w: 30 }]]}
/>
{#if flow}
<ActionRow applyPageWidth stickToTop>
<svelte:fragment slot="left">
<Button
href="/flows/run/{path}"
variant="contained"
color="blue"
size="xs"
startIcon={{ icon: faPlay }}
>
Run
</Button>
<Button
href="/flows/edit/{path}"
variant="contained"
color="blue"
size="xs"
startIcon={{ icon: faEdit }}
disabled={!can_write}
>
Edit
</Button>
<Button
href="/flows/add?template={flow.path}"
variant="contained"
color="blue"
size="xs"
startIcon={{ icon: faCodeFork }}
>
Use as template/Fork
</Button>
</svelte:fragment>
<svelte:fragment slot="right">
<Button
href="/runs/{flow.path}"
variant="border"
color="blue"
size="xs"
startIcon={{ icon: faList }}
>
View runs
</Button>
<Button
target="_blank"
href={flowToHubUrl(flow).toString()}
variant="border"
color="blue"
size="xs"
startIcon={{ icon: faGlobe }}
>
Publish to Hub
</Button>
<Dropdown
dropdownItems={[
{
displayName: 'Use as template',
icon: faEdit,
href: `/flows/add?template=${flow.path}`
},
{
displayName: 'Share',
icon: faShare,
action: () => {
shareModal.openDrawer()
},
disabled: !can_write
},
{
displayName: 'Schedule',
icon: faCalendar,
href: `/schedule/add?path=${flow.path}&isFlow=true`
},
{
displayName: 'Archive',
icon: faArchive,
type: 'delete',
action: () => {
flow?.path && archiveFlow()
},
disabled: flow.archived || !can_write
}
]}
/>
</svelte:fragment>
</ActionRow>
{/if}
<CenteredPage>
<div class="relative h-screen overflow-auto">
<Skeleton
class="!max-w-6xl !px-4 sm:!px-6 md:!px-8"
loading={!flow}
layout={[0.75, [2, 0, 2], 2.25, [{ h: 1.5, w: 40 }], 0.2, [{ h: 1, w: 30 }]]}
/>
{#if flow}
<h1>
<a href="/flows/get/{path}">{flow?.path}</a>
<SharedBadge canWrite={can_write} extraPerms={flow?.extra_perms ?? {}} />
</h1>
<ActionRow applyPageWidth stickToTop>
<svelte:fragment slot="left">
<Button
on:click={() => flow?.path && archiveFlow()}
variant="border"
color="red"
size="xs"
startIcon={{ icon: faArchive }}
disabled={flow.archived || !can_write}
>
Archive
</Button>
<Button
target="_blank"
href={flowToHubUrl(flow).toString()}
variant="border"
color="light"
size="xs"
startIcon={{ icon: faGlobe }}
>
Publish to Hub
</Button>
<Button
href="/schedule/add?path={flow.path}&isFlow=true"
variant="border"
color="light"
size="xs"
startIcon={{ icon: faCalendar }}
>
Schedule
</Button>
<Button
on:click={() => shareModal.openDrawer()}
variant="border"
color="light"
size="xs"
startIcon={{ icon: faShare }}
disabled={!can_write}
>
Share
</Button>
</svelte:fragment>
<svelte:fragment slot="right">
<Button
href="/flows/run/{path}"
variant="contained"
color="blue"
size="xs"
startIcon={{ icon: faPlay }}
>
Run
</Button>
<Button
href="/flows/edit/{path}"
variant="contained"
color="blue"
size="xs"
startIcon={{ icon: faEdit }}
disabled={!can_write}
>
Edit
</Button>
<Button
href="/flows/add?template={flow.path}"
variant="contained"
color="blue"
size="xs"
startIcon={{ icon: faCodeFork }}
>
Use as template/Fork
</Button>
<Button href="/runs/{flow.path}" color="blue" size="xs" startIcon={{ icon: faList }}>
View runs
</Button>
</svelte:fragment>
</ActionRow>
{/if}
<ShareModal bind:this={shareModal} kind="flow" path={flow?.path ?? ''} />
<div class="grid grid-cols-1 gap-6 max-w-7xl pb-6">
<Skeleton
loading={!flow}
layout={[[{ h: 1.5, w: 40 }], 1, [4], 2.25, [{ h: 1.5, w: 30 }], 1, [10]]}
/>
<CenteredPage>
{#if flow}
<p class="text-sm text-gray-600"
>Edited {displayDaysAgo(flow.edited_at ?? '')} by {flow.edited_by}</p
>
<div>
<h2 class="font-bold mb-2"
>{flow.summary && flow.summary != '' ? flow.summary : 'No summary'}</h2
>
<div class="prose text-xs box">
<SvelteMarkdown source={defaultIfEmptyString(flow.description, 'No description')} />
</div>
</div>
{#if flow.archived}
<div class="bg-red-100 border-l-4 border-red-500 text-orange-700 p-4" role="alert">
<p class="font-bold">Archived</p>
<p>This version was archived</p>
</div>
<h1 class="break-words py-2 mr-2">
{defaultIfEmptyString(flow.summary, flow.path)}
</h1>
{#if !emptyString(flow.summary)}
<h2 class="font-bold pb-4">{flow.path}</h2>
{/if}
<div>
<h2 class="text-gray-700 pb-1 mb-3 border-b">Flow</h2>
<FlowViewer {flow} />
{/if}
<h2 class="my-4 text-gray-700 pb-1 mb-3 border-b"
>Webhook<Tooltip
>To trigger this script with a webhook, do a POST request to the endpoint below. Flows
are not public and can only be run by users with at least view rights on them. You will
need to pass a bearer token to authentify as a user. You can either pass it as a Bearer
token or as query arg `?token=XXX`. <a
href="https://docs.windmill.dev/docs/getting_started/webhooks">See docs</a
></Tooltip
></h2
<ShareModal bind:this={shareModal} kind="flow" path={flow?.path ?? ''} />
<div class="grid grid-cols-1 gap-6 max-w-7xl pb-6">
<Skeleton
loading={!flow}
layout={[[{ h: 1.5, w: 40 }], 1, [4], 2.25, [{ h: 1.5, w: 30 }], 1, [10]]}
/>
{#if flow}
<p class="text-sm text-gray-600"
>Edited {displayDaysAgo(flow.edited_at ?? '')} by {flow.edited_by}</p
>
<div class="box max-w-2xl">
<div class="flex flex-row gap-x-2 w-full">
<a
href={$page.url.protocol + '//' + url}
class="whitespace-nowrap text-ellipsis overflow-hidden mr-1"
>
{url}
</a>
<Button
on:click={() => copyToClipboard($page.url.protocol + '//' + url)}
color="blue"
size="xs"
startIcon={{ icon: faClipboard }}
btnClasses="ml-2"
>
Copy
</Button>
<Button size="xs" on:click={userSettings.toggleDrawer}>Create token</Button>
</div>
</div>
{#if schedule}
<div class="mt-8">
<h2 class="text-gray-700 pb-1 mb-3 border-b inline-flex flex-row items-center gap-x-4"
><div>Primary Schedule </div>
<Badge color="gray">{schedule.schedule}</Badge>
<Toggle
checked={schedule.enabled}
on:change={(e) => {
if (can_write) {
setScheduleEnabled(path, e.detail)
} else {
sendUserToast('not enough permission', true)
}
}}
/>
<Button size="xs" href="/schedule/add?edit={flow.path}&isFlow=true"
>Edit schedule</Button
>
</h2>
<div class="max-w-lg">
<JobArgs args={schedule.args ?? {}} />
</div>
<div class="box max-w-lg mt-2">
<CronInput disabled={true} schedule={schedule.schedule} />
</div>
{#if flow.archived}
<div class="bg-red-100 border-l-4 border-red-500 text-orange-700 p-4" role="alert">
<p class="font-bold">Archived</p>
<p>This version was archived</p>
</div>
{/if}
</div>
{/if}
</div>
</CenteredPage>
<div class="prose text-sm box max-w-6xl w-full">
<SvelteMarkdown source={defaultIfEmptyString(flow?.description, 'No description')} />
</div>
<div>
<FlowViewer {flow} noSummary={true} />
<h2 class="my-4 text-gray-700 pb-1 mb-3 border-b"
>Webhook<Tooltip
>To trigger this script with a webhook, do a POST request to the endpoint below. Flows
are not public and can only be run by users with at least view rights on them. You
will need to pass a bearer token to authentify as a user. You can either pass it as a
Bearer token or as query arg `?token=XXX`. <a
href="https://docs.windmill.dev/docs/getting_started/webhooks">See docs</a
></Tooltip
></h2
>
<div class="box max-w-2xl">
<div class="flex flex-row gap-x-2 w-full">
<a
href={$page.url.protocol + '//' + url}
class="whitespace-nowrap text-ellipsis overflow-hidden mr-1"
>
{url}
</a>
<Button
on:click={() => copyToClipboard($page.url.protocol + '//' + url)}
color="blue"
size="xs"
startIcon={{ icon: faClipboard }}
btnClasses="ml-2"
>
Copy
</Button>
<Button size="xs" on:click={userSettings.toggleDrawer}>Create token</Button>
</div>
</div>
{#if schedule}
<div class="mt-8">
<h2 class="text-gray-700 pb-1 mb-3 border-b inline-flex flex-row items-center gap-x-4"
><div>Primary Schedule </div>
<Badge color="gray">{schedule.schedule}</Badge>
<Toggle
checked={schedule.enabled}
on:change={(e) => {
if (can_write) {
setScheduleEnabled(path, e.detail)
} else {
sendUserToast('not enough permission', true)
}
}}
/>
<Button size="xs" href="/schedule/add?edit={flow.path}&isFlow=true"
>Edit schedule</Button
>
</h2>
<div class="max-w-lg">
<JobArgs args={schedule.args ?? {}} />
</div>
<div class="box max-w-lg mt-2">
<CronInput disabled={true} schedule={schedule.schedule} />
</div>
</div>
{/if}
</div>
{/if}
</div>
</CenteredPage>
</div>
+81 -8
View File
@@ -8,21 +8,35 @@
<script lang="ts">
import { page } from '$app/stores'
import { sendUserToast } from '$lib/utils'
import {
canWrite,
defaultIfEmptyString,
displayDaysAgo,
emptyString,
sendUserToast
} from '$lib/utils'
import { FlowService, type Flow, JobService } from '$lib/gen'
import { goto } from '$app/navigation'
import { workspaceStore } from '$lib/stores'
import { userStore, workspaceStore } from '$lib/stores'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import RunForm from '$lib/components/RunForm.svelte'
import PageHeader from '$lib/components/PageHeader.svelte'
import { Button } from '$lib/components/common'
import { faPlay, faScroll } from '@fortawesome/free-solid-svg-icons'
import SharedBadge from '$lib/components/SharedBadge.svelte'
import SvelteMarkdown from 'svelte-markdown'
const path = $page.params.path
let flow: Flow | undefined
let runForm: RunForm | undefined
let isValid = true
let can_write = false
async function loadFlow() {
try {
if (path) {
flow = await FlowService.getFlowByPath({ workspace: $workspaceStore!, path })
can_write =
flow.workspace_id == $workspaceStore && canWrite(flow.path, flow.extra_perms!, $userStore)
} else {
sendUserToast(`Failed to fetch flow path from URL`, true)
}
@@ -40,7 +54,7 @@
requestBody: args,
scheduledFor
})
sendUserToast(`Job <a href='/run/${run}'>${run}</a> was created.`)
sendUserToast(`Job <a href='/run/${run}'>${run}</a> started`)
goto('/run/' + run)
}
@@ -49,11 +63,70 @@
loadFlow()
}
}
function onKeyDown(event: KeyboardEvent) {
switch (event.key) {
case 'Enter':
if (event.ctrlKey) {
if (isValid) {
event.preventDefault()
runForm?.run()
} else {
sendUserToast('Please fix errors before running', true)
}
}
break
}
}
</script>
<svelte:window on:keydown={onKeyDown} />
<CenteredPage>
<PageHeader title="Run flow {flow?.path ?? '...'}">
<a href="/flows/get/{flow?.path}">View flow {flow?.path ?? ''} details</a>
</PageHeader>
<RunForm runnable={flow} runAction={runFlow} />
{#if flow}
<div class="flex flex-row flex-wrap justify-between gap-4">
<div class="w-full">
<div class="flex flex-col mt-6 mb-2 w-full">
<div class="flex flex-row flex-wrap w-full justify-between "
><div class="flex flex-col">
<h1 class="break-words py-2 mr-2">
{defaultIfEmptyString(flow.summary, flow.path)}
</h1>
{#if !emptyString(flow.summary)}
<h2 class="font-bold pb-4">{flow.path}</h2>
{/if}
</div>
<div class="flex-row hidden lg:flex">
<div>
<Button
startIcon={{ icon: faScroll }}
disabled={flow == undefined}
btnClasses="mr-4"
variant="border"
href="/flows/get/{flow?.path}">View flow</Button
>
<Button
startIcon={{ icon: faPlay }}
disabled={runForm == undefined || !isValid}
on:click={() => runForm?.run()}>Run (Ctrl+Enter)</Button
>
</div>
</div></div
>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-500">
{#if flow}
Edited {displayDaysAgo(flow.edited_at || '')} by {flow.edited_by || 'unknown'}
{/if}
</span>
<SharedBadge canWrite={can_write} extraPerms={flow?.extra_perms ?? {}} />
</div>
</div>
</div>
<div class="prose text-sm box max-w-6xl w-full mb-4">
<SvelteMarkdown source={defaultIfEmptyString(flow.description, 'No description')} />
</div>
</div>
{/if}
<RunForm bind:this={runForm} bind:isValid detailed={false} runnable={flow} runAction={runFlow} />
</CenteredPage>
+24 -16
View File
@@ -105,6 +105,30 @@
{#if job?.job_kind === 'script' || job?.job_kind === 'flow'}
<ActionRow applyPageWidth stickToTop>
<svelte:fragment slot="left">
{@const isScript = job?.job_kind === 'script'}
{@const runsHref = `/runs/${job?.script_path}${!isScript ? '?jobKind=flow' : ''}`}
{#if job && 'deleted' in job && !job?.deleted && ($userStore?.is_admin ?? false)}
<Button
variant="border"
color="red"
size="xs"
startIcon={{ icon: faTrash }}
on:click={() => job?.id && deleteCompletedJob(job.id)}
>
Delete
</Button>
<Button
href={runsHref}
variant="border"
color="blue"
size="xs"
startIcon={{ icon: faList }}
>
View runs
</Button>
{/if}
</svelte:fragment>
<svelte:fragment slot="right">
{@const stem = `/${job?.job_kind}s`}
{@const isScript = job?.job_kind === 'script'}
{@const route = isScript ? job?.script_hash : job?.script_path}
@@ -146,22 +170,6 @@
<Button href={viewHref} color="blue" size="xs" startIcon={{ icon: faScroll }}>
View {job?.job_kind}
</Button>
<Button href={runsHref} variant="border" color="blue" size="xs" startIcon={{ icon: faList }}>
View runs
</Button>
</svelte:fragment>
<svelte:fragment slot="right">
{#if job && 'deleted' in job && !job?.deleted && ($userStore?.is_admin ?? false)}
<Button
variant="border"
color="red"
size="xs"
startIcon={{ icon: faTrash }}
on:click={() => job?.id && deleteCompletedJob(job.id)}
>
Delete
</Button>
{/if}
</svelte:fragment>
</ActionRow>
{/if}
+301 -298
View File
@@ -16,7 +16,8 @@
canWrite,
defaultIfEmptyString,
scriptToHubUrl,
copyToClipboard
copyToClipboard,
emptyString
} from '$lib/utils'
import {
faPlay,
@@ -133,320 +134,322 @@
<UserSettings bind:this={userSettings} />
<Skeleton {loading} class="!px-4 sm:!px-6 md:!px-8 !max-w-6xl" layout={[0.75, [2, 0, 2], 1]} />
{#if script}
<ActionRow applyPageWidth stickToTop>
<svelte:fragment slot="left">
<Button
href={`/scripts/run/${script.hash}`}
color="blue"
size="xs"
startIcon={{ icon: faPlay }}
>
Run
</Button>
<Button
href={`/scripts/edit/${script.hash}?step=2`}
color="blue"
size="xs"
startIcon={{ icon: faEdit }}
disabled={!can_write}
>
Edit
</Button>
{#if !topHash}
<div class="h-screen relative overflow-auto">
{#if script}
<ActionRow applyPageWidth stickToTop>
<svelte:fragment slot="left">
<Dropdown
left={false}
dropdownItems={[
{
displayName: 'Archive',
icon: faArchive,
type: 'delete',
action: () => {
script?.hash && archiveScript(script.hash)
},
disabled: script.archived || !can_write
},
{
displayName: 'Delete',
icon: faTrash,
type: 'delete',
action: () => {
script?.hash && deleteScript(script.hash)
},
disabled: script.deleted || !($userStore?.is_admin ?? false)
}
]}><span class="text-red-500">delete...</span></Dropdown
>
<Button
href={`/scripts/add?template=${script.path}`}
disabled={deploymentInProgress}
target="_blank"
href={scriptToHubUrl(
script.content,
script.summary,
script.description ?? '',
script.kind,
script.language,
script.schema,
script.language == 'deno' ? '' : script.lock
).toString()}
variant="border"
color="light"
size="xs"
startIcon={{ icon: faGlobe }}
>
Publish to Hub
</Button>
<Button
on:click={() => shareModal.openDrawer()}
variant="border"
color="light"
size="xs"
startIcon={{ icon: faShare }}
disabled={!can_write}
>
Share
</Button>
<Button
href="/schedule/add?path={script.path}"
variant="border"
color="light"
size="xs"
startIcon={{ icon: faCalendar }}
>
Schedule
</Button>
</svelte:fragment>
<svelte:fragment slot="right">
<Button
href={`/scripts/run/${script.hash}`}
color="blue"
size="xs"
startIcon={{ icon: faCodeFork }}
startIcon={{ icon: faPlay }}
>
Use as template/Fork
Run
</Button>
{/if}
</svelte:fragment>
<svelte:fragment slot="right">
<Button
href={`/runs/${script.path}`}
variant="border"
color="blue"
size="xs"
startIcon={{ icon: faList }}
>
View runs
</Button>
<Button
disabled={deploymentInProgress}
target="_blank"
href={scriptToHubUrl(
script.content,
script.summary,
script.description ?? '',
script.kind,
script.language,
script.schema,
script.language == 'deno' ? '' : script.lock
).toString()}
variant="border"
color="blue"
size="xs"
startIcon={{ icon: faGlobe }}
>
Publish to Hub
</Button>
<Dropdown
dropdownItems={[
{
displayName: 'Use as template',
icon: faEdit,
href: `/scripts/add?template=${script.path}`
},
{
displayName: 'Share',
icon: faShare,
action: () => {
shareModal.openDrawer()
},
disabled: !can_write
},
{
displayName: 'Schedule',
icon: faCalendar,
href: `/schedule/add?path=${script.path}`
},
{
displayName: 'Archive',
icon: faArchive,
type: 'delete',
action: () => {
script?.hash && archiveScript(script.hash)
},
disabled: script.archived || !can_write
},
{
displayName: 'Delete',
icon: faTrash,
type: 'delete',
action: () => {
script?.hash && deleteScript(script.hash)
},
disabled: script.deleted || !($userStore?.is_admin ?? false)
}
]}
/>
</svelte:fragment>
</ActionRow>
{/if}
<CenteredPage>
<Skeleton {loading} layout={[[{ h: 1.5, w: 40 }], 1, [{ h: 1, w: 30 }]]} />
{#if script}
<div class="flex flex-row flex-wrap justify-between gap-4">
<div>
<div class="flex items-center flex-wrap mb-2">
<h1 class="font-bold text-blue-500 break-words !p-0 mr-2">
{script?.path ?? 'Loading...'}
</h1>
<div class="flex items-center gap-2">
<Badge color="dark-gray">
{truncateHash(script?.hash ?? '')}
</Badge>
{#if script?.is_template}
<Badge color="blue">Template</Badge>
<Button
href={`/scripts/edit/${script.hash}?step=2`}
color="blue"
size="xs"
startIcon={{ icon: faEdit }}
disabled={!can_write}
>
Edit
</Button>
{#if !topHash}
<Button
href={`/scripts/add?template=${script.path}`}
color="blue"
size="xs"
startIcon={{ icon: faCodeFork }}
>
Use as template/Fork
</Button>
{/if}
<Button href={`/runs/${script.path}`} size="xs" startIcon={{ icon: faList }}>
View runs
</Button>
</svelte:fragment>
</ActionRow>
{/if}
<CenteredPage>
<Skeleton {loading} layout={[[{ h: 1.5, w: 40 }], 1, [{ h: 1, w: 30 }]]} />
{#if script}
<div class="flex flex-row flex-wrap justify-between gap-4">
<div>
<div class="flex flex-col mb-2">
<h1 class="break-words py-2 mr-2">
{defaultIfEmptyString(script.summary, script.path)}
</h1>
{#if !emptyString(script.summary)}
<h2 class="font-bold pb-4">{script.path}</h2>
{/if}
{#if script && script.kind !== 'script'}
<Badge color="blue">
{script?.kind}
<div class="flex items-center gap-2">
<span class="text-sm text-gray-500">
{#if script}
Edited {displayDaysAgo(script.created_at || '')} by {script.created_by ||
'unknown'}
{/if}
</span>
<Badge color="dark-gray">
{truncateHash(script?.hash ?? '')}
</Badge>
{/if}
{#if deploymentInProgress}
<Badge
color="yellow"
icon={{ data: faSpinner, position: 'right', class: 'animate-spin' }}
>
Deployment in progress
</Badge>
{/if}
{#if script?.is_template}
<Badge color="blue">Template</Badge>
{/if}
{#if script && script.kind !== 'script'}
<Badge color="blue">
{script?.kind}
</Badge>
{/if}
{#if deploymentInProgress}
<Badge
color="yellow"
icon={{ data: faSpinner, position: 'right', class: 'animate-spin' }}
>
Deployment in progress
</Badge>
{/if}
<SharedBadge canWrite={can_write} extraPerms={script?.extra_perms ?? {}} />
</div>
</div>
</div>
<p class="mb-2">
<SharedBadge canWrite={can_write} extraPerms={script?.extra_perms ?? {}} />
<span class="text-sm text-gray-500">
{#if script}
Edited {displayDaysAgo(script.created_at || '')} by {script.created_by || 'unknown'}
{/if}
</span>
</p>
</div>
</div>
{/if}
<ShareModal bind:this={shareModal} kind="script" path={script?.path ?? ''} />
<div class="flex flex-col gap-8 max-w-7xl pt-4 pb-2">
<Skeleton {loading} layout={[[3], 1]} />
{#if script}
<div>
<h2 class="font-bold mb-2"
>{script.summary && script.summary != '' ? script.summary : 'No summary'}</h2
>
<div class="prose text-xs box">
<SvelteMarkdown source={defaultIfEmptyString(script.description, 'No description')} />
</div>
</div>
{#if script.lock_error_logs || topHash || script.archived || script.deleted}
<div class="flex flex-col gap-2">
{#if script.lock_error_logs}
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4" role="alert">
<p class="font-bold">Error deploying this script</p>
<p>This script has not been deployed successfully because of the following errors:</p>
<pre class="w-full text-xs mt-2 whitespace-pre-wrap">{script.lock_error_logs}</pre>
</div>
{/if}
{#if topHash}
<div
class="bg-orange-100 border-l-4 border-orange-500 text-orange-700 p-4"
role="alert"
>
<p class="font-bold">Not HEAD</p>
<p>
This hash is not HEAD (latest non-archived version at this path) :
<a href="/scripts/get/{topHash}">Go to the HEAD of this path</a>
</p>
</div>
{/if}
{#if script.archived}
<div class="bg-red-100 border-l-4 border-red-500 text-orange-700 p-4" role="alert">
<p class="font-bold">Archived</p>
<p>This version was archived</p>
</div>
{/if}
{#if script.deleted}
<div class="bg-red-100 border-l-4 border-red-600 text-orange-700 p-4" role="alert">
<p class="font-bold">Deleted</p>
<p>The content of this script was deleted (by an admin, no less)</p>
</div>
{/if}
</div>
{/if}
{/if}
<div class="">
<h2 class="text-lg mb-1 font-bold text-gray-600">
Arguments JSON schema
<Tooltip>
The jsonschema defines the constraints that the payload must respect to be compatible with
the input parameters of this script. The UI form is generated automatically from the
script jsonschema. See
<a href="https://json-schema.org/"> jsonschema documentation </a>
</Tooltip>
</h2>
<Skeleton {loading} layout={[[15]]} />
<ShareModal bind:this={shareModal} kind="script" path={script?.path ?? ''} />
<div class="flex flex-col gap-8 max-w-7xl pt-4 pb-2">
<Skeleton {loading} layout={[[3], 1]} />
{#if script}
<div class="box mt-2">
<SchemaViewer schema={script.schema} />
</div>
{/if}
</div>
<div>
<h2 class="text-lg mb-1 font-bold text-gray-600 border-b">Code</h2>
<Skeleton {loading} layout={[[20]]} />
{#if script}
<HighlightCode language={script.language} code={script.content} />
{/if}
</div>
</div>
<div class="flex flex-col lg:flex-row gap-4 mt-8">
<div class="lg:w-1/2">
<h3 class="text-lg mb-1 font-bold text-gray-600"
>Webhooks<Tooltip
>To trigger this script with a webhook, do a POST request to the endpoints below. Scripts
are not public and can only be run by users with at least view rights on them. You will
need to pass a bearer token to authentify as a user. You can either pass it as a Bearer
token or as query arg `?token=XXX`. <a
href="https://docs.windmill.dev/docs/getting_started/webhooks">See docs</a
></Tooltip
></h3
>
<Skeleton {loading} layout={[[8.5]]} />
{#if script}
<div class="box">
<Tabs selected="uuid">
<Tab value="uuid">UUID</Tab>
<Tab value="result">Result</Tab>
<svelte:fragment slot="content">
{#each Object.keys(webhooks) as key}
<TabContent value={key}>
<ul>
{#each Object.keys(webhooks[key]) as type}
{@const url = webhooks[key][type]}
<li class="flex justify-between items-center mt-2">
<a
href={$page.url.protocol + '//' + url}
class="whitespace-nowrap text-ellipsis overflow-hidden mr-1"
>
{url}
</a>
<div class="flex">
<Badge color="dark-gray" capitalize>
{type}
</Badge>
<Button
on:click={() => copyToClipboard($page.url.protocol + '//' + url)}
color="blue"
size="xs"
startIcon={{ icon: faClipboard }}
btnClasses="ml-2"
>
Copy
</Button>
</div>
<div class="flex flex-col lg:flex-row gap-4">
<div class="lg:w-1/2">
<h3 class="text-lg mb-1 font-bold text-gray-600">Description</h3>
<div class="prose text-sm box">
<SvelteMarkdown source={defaultIfEmptyString(script.description, 'No description')} />
</div>
</div>
<div class="lg:w-1/2">
<h3 class="text-lg mb-1 font-bold text-gray-600">Versions</h3>
<Skeleton {loading} layout={[[8.5]]} />
{#if script}
<div class="box">
<h4 class="font-bold text-gray-500">Current</h4>
<div class="mt-1">
{script?.hash}
</div>
<h4 class="font-bold text-gray-500 mt-2">Previous</h4>
{#if script?.parent_hashes?.length}
<ul class="max-h-20 overflow-y-auto">
{#each script.parent_hashes as hash}
<li class="mt-1">
<a href="/scripts/get/{hash}">{hash}</a>
</li>
{/each}
</ul>
<div class="flex flex-row-reverse mt-2">
<Button size="xs" on:click={userSettings.toggleDrawer}>Create token</Button>
</div>
</TabContent>
{/each}
</svelte:fragment>
</Tabs>
</div>
{/if}
</div>
<div class="lg:w-1/2">
<h3 class="text-lg mb-1 font-bold text-gray-600">Versions</h3>
<Skeleton {loading} layout={[[8.5]]} />
{#if script}
<div class="box">
<h4 class="font-bold text-gray-500">Current</h4>
<div class="mt-1">
{script?.hash}
{:else}
<p class="text-sm text-gray-500">There are no previous versions</p>
{/if}
</div>
{/if}
</div>
<h4 class="font-bold text-gray-500 mt-2">Previous</h4>
{#if script?.parent_hashes?.length}
<ul class="max-h-20 overflow-y-auto">
{#each script.parent_hashes as hash}
<li class="mt-1">
<a href="/scripts/get/{hash}">{hash}</a>
</li>
{/each}
</ul>
{:else}
<p class="text-sm text-gray-500">There are no previous versions</p>
{/if}
</div>
{#if script.lock_error_logs || topHash || script.archived || script.deleted}
<div class="flex flex-col gap-2">
{#if script.lock_error_logs}
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4" role="alert">
<p class="font-bold">Error deploying this script</p>
<p
>This script has not been deployed successfully because of the following errors:</p
>
<pre class="w-full text-xs mt-2 whitespace-pre-wrap">{script.lock_error_logs}</pre>
</div>
{/if}
{#if topHash}
<div
class="bg-orange-100 border-l-4 border-orange-500 text-orange-700 p-4"
role="alert"
>
<p class="font-bold">Not HEAD</p>
<p>
This hash is not HEAD (latest non-archived version at this path) :
<a href="/scripts/get/{topHash}">Go to the HEAD of this path</a>
</p>
</div>
{/if}
{#if script.archived}
<div class="bg-red-100 border-l-4 border-red-500 text-orange-700 p-4" role="alert">
<p class="font-bold">Archived</p>
<p>This version was archived</p>
</div>
{/if}
{#if script.deleted}
<div class="bg-red-100 border-l-4 border-red-600 text-orange-700 p-4" role="alert">
<p class="font-bold">Deleted</p>
<p>The content of this script was deleted (by an admin, no less)</p>
</div>
{/if}
</div>
{/if}
{/if}
<div class="">
<h2 class="text-lg mb-1 font-bold text-gray-600">
Arguments JSON schema
<Tooltip>
The jsonschema defines the constraints that the payload must respect to be compatible
with the input parameters of this script. The UI form is generated automatically from
the script jsonschema. See
<a href="https://json-schema.org/"> jsonschema documentation </a>
</Tooltip>
</h2>
<Skeleton {loading} layout={[[15]]} />
{#if script}
<div class="box mt-2">
<SchemaViewer schema={script.schema} />
</div>
{/if}
</div>
<div>
<h2 class="text-lg mb-1 font-bold text-gray-600 border-b">Code</h2>
<Skeleton {loading} layout={[[20]]} />
{#if script}
<HighlightCode language={script.language} code={script.content} />
{/if}
</div>
</div>
<h3 class="text-lg mb-1 mt-8 font-bold text-gray-600"
>Webhooks<Tooltip
>To trigger this script with a webhook, do a POST request to the endpoints below. Scripts
are not public and can only be run by users with at least view rights on them. You will need
to pass a bearer token to authentify as a user. You can either pass it as a Bearer token or
as query arg `?token=XXX`. <a href="https://docs.windmill.dev/docs/getting_started/webhooks"
>See docs</a
></Tooltip
></h3
>
<Skeleton {loading} layout={[[8.5]]} />
{#if script}
<div class="box max-w-3xl">
<Tabs selected="uuid">
<Tab value="uuid">UUID</Tab>
<Tab value="result">Result</Tab>
<svelte:fragment slot="content">
{#each Object.keys(webhooks) as key}
<TabContent value={key}>
<ul>
{#each Object.keys(webhooks[key]) as type}
{@const url = webhooks[key][type]}
<li class="flex justify-between items-center mt-2">
<a
href={$page.url.protocol + '//' + url}
class="whitespace-nowrap text-ellipsis overflow-hidden mr-1"
>
{url}
</a>
<div class="flex">
<Badge color="dark-gray" capitalize>
{type}
</Badge>
<Button
on:click={() => copyToClipboard($page.url.protocol + '//' + url)}
color="blue"
size="xs"
startIcon={{ icon: faClipboard }}
btnClasses="ml-2"
>
Copy
</Button>
</div>
</li>
{/each}
</ul>
<div class="flex flex-row-reverse mt-2">
<Button size="xs" on:click={userSettings.toggleDrawer}>Create token</Button>
</div>
</TabContent>
{/each}
</svelte:fragment>
</Tabs>
</div>
{/if}
<div>
<h2 class="text-lg mb-1 font-bold text-gray-600 border-b mt-8">Dependencies lock file</h2>
<Skeleton {loading} layout={[[5]]} />
{#if script}
{#if script?.lock}
<pre class="text-xs overflow-auto">{script.lock}</pre>
{:else}
<p class="text-sm text-gray-500">There is no lock file for this script</p>
{/if}
{/if}
</div>
</div>
<div>
<h2 class="text-lg mb-1 font-bold text-gray-600 border-b mt-8">Dependencies lock file</h2>
<Skeleton {loading} layout={[[5]]} />
{#if script}
{#if script?.lock}
<pre class="text-xs">{script.lock}</pre>
{:else}
<p class="text-sm text-gray-500">There is no lock file for this script</p>
{/if}
{/if}
</div>
</CenteredPage>
</CenteredPage>
</div>
@@ -8,17 +8,31 @@
<script lang="ts">
import { page } from '$app/stores'
import { emptySchema, sendUserToast } from '$lib/utils'
import {
canWrite,
defaultIfEmptyString,
displayDaysAgo,
emptySchema,
emptyString,
sendUserToast,
truncateHash
} from '$lib/utils'
import { ScriptService, type Script, JobService } from '$lib/gen'
import { goto } from '$app/navigation'
import { workspaceStore } from '$lib/stores'
import { userStore, workspaceStore } from '$lib/stores'
import { inferArgs } from '$lib/infer'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import RunForm from '$lib/components/RunForm.svelte'
import PageHeader from '$lib/components/PageHeader.svelte'
import { Badge, Button } from '$lib/components/common'
import SharedBadge from '$lib/components/SharedBadge.svelte'
import SvelteMarkdown from 'svelte-markdown'
import { faPlay, faScroll } from '@fortawesome/free-solid-svg-icons'
const hash = $page.params.hash
let script: Script | undefined
let runForm: RunForm | undefined
let isValid = true
let can_write = false
async function loadScript() {
if (hash) {
@@ -28,6 +42,9 @@
inferArgs(script.language, script.content, script.schema)
script = script
}
can_write =
script.workspace_id == $workspaceStore &&
canWrite(script.path, script.extra_perms!, $userStore)
} else {
sendUserToast(`Failed to fetch script hash from URL`, true)
}
@@ -42,7 +59,7 @@
requestBody: args,
scheduledFor
})
sendUserToast(`Job <a href='/run/${run}'>${run}</a> was created.`)
sendUserToast(`Job <a href='/run/${run}'>${run}</a> started`)
goto('/run/' + run)
} catch (err) {
sendUserToast(`Could not create job: ${err}`, true)
@@ -54,12 +71,84 @@
loadScript()
}
}
function onKeyDown(event: KeyboardEvent) {
switch (event.key) {
case 'Enter':
if (event.ctrlKey) {
if (isValid) {
event.preventDefault()
runForm?.run()
} else {
sendUserToast('Please fix errors before running', true)
}
}
break
}
}
</script>
<svelte:window on:keydown={onKeyDown} />
<CenteredPage>
<PageHeader title="Run script {script?.path ?? '...'}">
<a href="/scripts/get/{script?.hash}">View script {script?.path} at hash {script?.hash}</a>
</PageHeader>
{#if script}
<div class="flex flex-row flex-wrap justify-between gap-4">
<div class="w-full">
<div class="flex flex-col mt-6 mb-2 w-full">
<div class="flex flex-row flex-wrap w-full justify-between "
><div class="flex flex-col">
<h1 class="break-words py-2 mr-2">
{defaultIfEmptyString(script.summary, script.path)}
</h1>
{#if !emptyString(script.summary)}
<h2 class="font-bold pb-4">{script.path}</h2>
{/if}
</div>
<div class="flex-row hidden lg:flex">
<div>
<Button
startIcon={{ icon: faScroll }}
disabled={script == undefined}
btnClasses="mr-4"
variant="border"
href="/scripts/get/{script?.hash}">View script</Button
>
<Button
startIcon={{ icon: faPlay }}
disabled={runForm == undefined || !isValid}
on:click={() => runForm?.run()}>Run (Ctrl+Enter)</Button
>
</div>
</div></div
>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-500">
{#if script}
Edited {displayDaysAgo(script.created_at || '')} by {script.created_by || 'unknown'}
{/if}
</span>
<Badge color="dark-gray">
{truncateHash(script?.hash ?? '')}
</Badge>
{#if script?.is_template}
<Badge color="blue">Template</Badge>
{/if}
{#if script && script.kind !== 'script'}
<Badge color="blue">
{script?.kind}
</Badge>
{/if}
<SharedBadge canWrite={can_write} extraPerms={script?.extra_perms ?? {}} />
</div>
</div>
</div>
<div class="prose text-sm box max-w-6xl w-full mb-4">
<SvelteMarkdown source={defaultIfEmptyString(script.description, 'No description')} />
</div>
</div>
{/if}
{#if script?.lock_error_logs}
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4" role="alert">
<p class="font-bold">Not deployed properly</p>
@@ -75,6 +164,12 @@
<p>Refresh this page in a few seconds.</p>
</div>
{:else}
<RunForm runnable={script} runAction={runScript} />
<RunForm
detailed={false}
bind:isValid
bind:this={runForm}
runnable={script}
runAction={runScript}
/>
{/if}
</CenteredPage>