mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-23 16:00:38 +00:00
UX improvements for run script
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user