mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-25 16:02:11 +00:00
Run page v2 (#1805)
* feat(frontend): Run page v2 * feat(frontend): Run page v2 * feat(frontend): Run page v2 * feat(frontend): migrate script details * feat(frontend): migrate script details * feat(frontend): done * feat(frontend): revert * feat(frontend): fix archive script * feat(frontend): remove old run page * feat(frontend): Update wording * feat(frontend): done * feat(frontend): Remove buttons for operator mode * feat(frontend): Fix spacing + add protocol * feat(frontend): Fix build
This commit is contained in:
@@ -1,35 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { slide } from 'svelte/transition'
|
||||
import InlineCodeCopy from './InlineCodeCopy.svelte'
|
||||
|
||||
$: opened = false
|
||||
$: url = `${$page.url.protocol}//${$page.url.hostname}/`
|
||||
</script>
|
||||
|
||||
<div class="text-sm mt-2 flex">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<button
|
||||
class="underline mr-4 text-gray-600"
|
||||
on:click={() => {
|
||||
opened = !opened
|
||||
}}
|
||||
>
|
||||
CLI quick setup
|
||||
<Icon class="ml-2" data={opened ? faChevronUp : faChevronDown} scale={0.7} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if opened}
|
||||
<div
|
||||
transition:slide|local={{ duration: 100 }}
|
||||
class="border border-gray-200 rounded-md p-4 mt-2 text-sm text-gray-600"
|
||||
role="alert"
|
||||
id="dynamic-input-help-box"
|
||||
>
|
||||
<div>
|
||||
<div class="text-sm font-semibold"> CLI quick setup </div>
|
||||
<div class="text-sm text-gray-600" role="alert" id="dynamic-input-help-box">
|
||||
<ul class="pl-0 pt-2 list-decimal list-inside">
|
||||
<li
|
||||
>Install the latest wmill CLI from deno.land: <InlineCodeCopy
|
||||
@@ -45,4 +24,4 @@
|
||||
<li>Use the run command above!</li>
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
import Icon from 'svelte-awesome'
|
||||
import { Button } from './common'
|
||||
import { slide } from 'svelte/transition'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let open = false
|
||||
export let text: string
|
||||
export let small = false
|
||||
</script>
|
||||
|
||||
<div class="flex">
|
||||
<div class={twMerge('flex', $$props.class)}>
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
<div
|
||||
class="{noSide
|
||||
? 'col-span-3'
|
||||
: 'sm:col-span-2 col-span-3'} w-full border border-gray-400 max-h-full"
|
||||
: 'sm:col-span-2 col-span-3'} w-full border border-gray-200 max-h-full"
|
||||
class:overflow-auto={overflowAuto}
|
||||
>
|
||||
<FlowGraph
|
||||
|
||||
@@ -34,7 +34,9 @@
|
||||
schema: flow.schema
|
||||
}
|
||||
|
||||
export let tab: 'ui' | 'json' | 'schema' = 'ui'
|
||||
export let noGraph: boolean = false
|
||||
|
||||
export let tab: 'ui' | 'json' | 'schema' = noGraph ? 'schema' : 'ui'
|
||||
export let noSummary = false
|
||||
|
||||
let open: { [id: number]: boolean } = {}
|
||||
@@ -48,7 +50,9 @@
|
||||
</script>
|
||||
|
||||
<Tabs bind:selected={tab}>
|
||||
<Tab value="ui">Graph</Tab>
|
||||
{#if !noGraph}
|
||||
<Tab value="ui">Graph</Tab>
|
||||
{/if}
|
||||
<Tab value="json">JSON</Tab>
|
||||
<Tab value="schema">Input Schema</Tab>
|
||||
|
||||
@@ -91,9 +95,9 @@
|
||||
<div class="relative pt-2">
|
||||
<Button
|
||||
on:click={() => copyToClipboard(JSON.stringify(flowFiltered, null, 4))}
|
||||
color="dark"
|
||||
color="light"
|
||||
variant="border"
|
||||
size="sm"
|
||||
size="xs"
|
||||
startIcon={{ icon: faClipboard }}
|
||||
btnClasses="absolute top-2 right-2 w-min"
|
||||
>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<pre class="bg-gray-700 text-gray-100 p-2 font-mono text-sm whitespace-pre-wrap my-2"
|
||||
<pre class="bg-gray-700 text-gray-100 p-2 font-mono text-sm whitespace-pre-wrap my-2 rounded-md"
|
||||
>{content} <span on:click={() => copyToClipboard(content)} class="cursor-pointer ml-2"
|
||||
><Icon data={faClipboard} /></span
|
||||
></pre
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
{#if runnable?.schema}
|
||||
<div class="my-2" />
|
||||
{#if !runnable.schema.properties || Object.keys(runnable.schema.properties).length === 0}
|
||||
<div class="text-sm p-4">No arguments</div>
|
||||
<div class="text-sm py-4 italic">No arguments</div>
|
||||
{:else}
|
||||
{#key reloadArgs}
|
||||
<SchemaForm
|
||||
@@ -150,67 +150,70 @@
|
||||
{#if schedulable}
|
||||
<div class="mt-10" />
|
||||
<div class="flex gap-2 items-start flex-wrap justify-between mt-2 md:mt-6 mb-6">
|
||||
<div class="flex-row-reverse flex grow">
|
||||
<div class="flex-row-reverse flex w-full gap-2">
|
||||
<Button
|
||||
{loading}
|
||||
color="dark"
|
||||
btnClasses="!px-6 !py-1"
|
||||
btnClasses="!px-6 !py-1 !h-8"
|
||||
disabled={!isValid}
|
||||
on:click={() => runAction(scheduledForStr, args, invisible_to_owner)}
|
||||
>
|
||||
{scheduledForStr ? 'Schedule to run later' : buttonText}
|
||||
</Button>
|
||||
<div>
|
||||
<CollapseLink small text="Advanced" class="justify-end">
|
||||
<div class="flex flex-col gap-4 mt-2 border p-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
{#if SCRIPT_VIEW_SHOW_SCHEDULE_RUN_LATER}
|
||||
<div class="border rounded-md p-3 pt-4">
|
||||
<div class="px-2 font-semibold text-sm">Schedule to run later</div>
|
||||
|
||||
<div class="flex flex-row items-end">
|
||||
<div class="w-max md:w-2/3 mt-2 mb-1">
|
||||
<label for="run-time" />
|
||||
<input
|
||||
class="inline-block"
|
||||
type="datetime-local"
|
||||
id="run-time"
|
||||
name="run-scheduled-time"
|
||||
bind:value={scheduledForStr}
|
||||
min={getToday().toISOString().slice(0, 16)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="border"
|
||||
color="blue"
|
||||
size="sm"
|
||||
btnClasses="mx-2 mb-1"
|
||||
on:click={() => {
|
||||
scheduledForStr = undefined
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if runnable?.path?.startsWith(`u/${$userStore?.username}`) != true && (runnable?.path?.split('/')?.length ?? 0) > 2}
|
||||
<div class="flex items-center gap-1">
|
||||
<Toggle
|
||||
options={{
|
||||
right: `make run invisible to others`
|
||||
}}
|
||||
bind:checked={invisible_to_owner}
|
||||
/>
|
||||
<Tooltip
|
||||
>By default, runs are visible to the owner(s) of the script or flow being
|
||||
triggered</Tooltip
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</CollapseLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CollapseLink small text="Advanced">
|
||||
<div class="flex flex-col gap-4 mt-2 border p-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
{#if SCRIPT_VIEW_SHOW_SCHEDULE_RUN_LATER}
|
||||
<div class="border rounded-md p-3 pt-4">
|
||||
<div class="px-2 font-semibold text-sm">Schedule to run later</div>
|
||||
|
||||
<div class="flex flex-row items-end">
|
||||
<div class="w-max md:w-2/3 mt-2 mb-1">
|
||||
<label for="run-time" />
|
||||
<input
|
||||
class="inline-block"
|
||||
type="datetime-local"
|
||||
id="run-time"
|
||||
name="run-scheduled-time"
|
||||
bind:value={scheduledForStr}
|
||||
min={getToday().toISOString().slice(0, 16)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="border"
|
||||
color="blue"
|
||||
size="sm"
|
||||
btnClasses="mx-2 mb-1"
|
||||
on:click={() => {
|
||||
scheduledForStr = undefined
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if runnable?.path?.startsWith(`u/${$userStore?.username}`) != true && (runnable?.path?.split('/')?.length ?? 0) > 2}
|
||||
<div class="flex items-center gap-1">
|
||||
<Toggle
|
||||
options={{
|
||||
right: `make run invisible to others`
|
||||
}}
|
||||
bind:checked={invisible_to_owner}
|
||||
/>
|
||||
<Tooltip
|
||||
>By default, runs are visible to the owner(s) of the script or flow being triggered</Tooltip
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</CollapseLink>
|
||||
{:else if !topButton}
|
||||
<Button
|
||||
btnClasses="!px-6 !py-1 w-full"
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
disabled={!isValid}
|
||||
loading={savingInputs}
|
||||
startIcon={{ icon: faSave }}
|
||||
color="blue"
|
||||
color="light"
|
||||
size="xs"
|
||||
>
|
||||
<span>Save Current Input</span>
|
||||
@@ -257,7 +257,7 @@
|
||||
|
||||
<Pane>
|
||||
<div class="w-full flex flex-col gap-4 p-2">
|
||||
<span class="text-sm font-extrabold">Previous Inputs</span>
|
||||
<span class="text-sm font-extrabold">Previous runs</span>
|
||||
|
||||
<div class="w-full flex flex-col gap-1 p-0 h-full overflow-y-auto">
|
||||
{#if previousInputs.length > 0}
|
||||
|
||||
@@ -6,7 +6,12 @@
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Skeleton } from '$lib/components/common'
|
||||
import FlowModuleScript from './flows/content/FlowModuleScript.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Button from './common/button/Button.svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let openDetails: boolean = false
|
||||
export let scriptPath: string
|
||||
|
||||
let selectedVersion: string | undefined = undefined
|
||||
@@ -38,6 +43,18 @@
|
||||
on:click={() => (selectedVersion = version)}
|
||||
>
|
||||
<span class="text-xs truncate">{version}</span>
|
||||
{#if openDetails}
|
||||
<Button
|
||||
on:click={() => {
|
||||
dispatch('openDetails', { version })
|
||||
}}
|
||||
size="xs2"
|
||||
color="light"
|
||||
variant="border"
|
||||
>
|
||||
Open details
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
export let versions: number[]
|
||||
|
||||
debugger
|
||||
|
||||
let selectedVersion: number | undefined = undefined
|
||||
let selected: AppWithLastVersion | undefined = undefined
|
||||
|
||||
|
||||
@@ -100,16 +100,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
href="/apps/get/{path}"
|
||||
color="dark"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
startIcon={{ icon: faEye }}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
</span>
|
||||
<Dropdown
|
||||
placement="bottom-end"
|
||||
|
||||
@@ -12,10 +12,8 @@
|
||||
faCalendarAlt,
|
||||
faCodeFork,
|
||||
faEdit,
|
||||
faEye,
|
||||
faFileExport,
|
||||
faList,
|
||||
faPlay,
|
||||
faShare,
|
||||
faTrashAlt
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
@@ -68,7 +66,7 @@
|
||||
|
||||
<ScheduleEditor on:update={() => goto('/schedules')} bind:this={scheduleEditor} />
|
||||
<Row
|
||||
href={`/flows/run/${path}`}
|
||||
href="/flows/get/{path}?workspace={$workspaceStore}"
|
||||
kind="flow"
|
||||
workspaceId={workspace_id ?? $workspaceStore ?? ''}
|
||||
{marked}
|
||||
@@ -115,28 +113,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if !draft_only}
|
||||
<Button
|
||||
href="/flows/get/{path}?workspace={$workspaceStore}"
|
||||
color="light"
|
||||
variant="border"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
startIcon={{ icon: faEye }}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
<Button
|
||||
href="/flows/run/{path}"
|
||||
color="dark"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
endIcon={{ icon: faPlay }}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<Dropdown
|
||||
|
||||
@@ -22,21 +22,27 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="hover:bg-gray-50 w-full inline-flex items-center px-4 py-4 gap-4 first-of-type:!border-t-0
|
||||
class="hover:bg-gray-50 w-full inline-flex items-center gap-4 first-of-type:!border-t-0
|
||||
first-of-type:rounded-t-md last-of-type:rounded-b-md {color}"
|
||||
>
|
||||
<RowIcon {href} {kind} />
|
||||
|
||||
<a {href} class="min-w-0 grow hover:underline decoration-gray-400">
|
||||
<div class="text-gray-900 flex-wrap text-left text-sm font-semibold mb-1 truncate">
|
||||
{#if marked}
|
||||
{@html marked}
|
||||
{:else}
|
||||
{!summary || summary.length == 0 ? path : summary}
|
||||
{/if}
|
||||
<a
|
||||
{href}
|
||||
class="min-w-0 grow hover:underline decoration-gray-400 px-4 py-4 flex flex-row items-center gap-4"
|
||||
>
|
||||
<div>
|
||||
<RowIcon {href} {kind} />
|
||||
</div>
|
||||
<div class="text-gray-600 text-2xs truncate text-left font-light">
|
||||
{path}
|
||||
<div>
|
||||
<div class="text-gray-900 flex-wrap text-left text-sm font-semibold mb-1 truncate">
|
||||
{#if marked}
|
||||
{@html marked}
|
||||
{:else}
|
||||
{!summary || summary.length == 0 ? path : summary}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-gray-600 text-2xs truncate text-left font-light">
|
||||
{path}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{#if $$slots.badges}
|
||||
|
||||
@@ -13,10 +13,8 @@
|
||||
faCalendarAlt,
|
||||
faCodeFork,
|
||||
faEdit,
|
||||
faEye,
|
||||
faFileExport,
|
||||
faList,
|
||||
faPlay,
|
||||
faShare,
|
||||
faTrashAlt
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
@@ -86,7 +84,7 @@
|
||||
<ScheduleEditor on:update={() => goto('/schedules')} bind:this={scheduleEditor} />
|
||||
|
||||
<Row
|
||||
href={`/scripts/run/${hash}`}
|
||||
href="/scripts/get/{hash}?workspace={$workspaceStore}"
|
||||
kind="script"
|
||||
{marked}
|
||||
{path}
|
||||
@@ -139,28 +137,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if !draft_only}
|
||||
<Button
|
||||
href="/scripts/get/{hash}?workspace={$workspaceStore}"
|
||||
color="light"
|
||||
variant="border"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
startIcon={{ icon: faEye }}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
<Button
|
||||
href="/scripts/run/{hash}"
|
||||
color="dark"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
endIcon={{ icon: faPlay }}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
{/if}
|
||||
</span>
|
||||
<Dropdown
|
||||
placement="bottom-end"
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { copyToClipboard } from '$lib/utils'
|
||||
import { Clipboard } from 'lucide-svelte'
|
||||
|
||||
export let content: string
|
||||
export let title: string
|
||||
</script>
|
||||
|
||||
<div class="text-xs font-semibold">{title}</div>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="px-2 py-1 border flex items-center cursor-pointer bg-gray-100 text-gray-900 justify-between rounded-md"
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
copyToClipboard(content)
|
||||
}}
|
||||
>
|
||||
<div class="text-xs whitespace-pre-wrap">{content}</div>
|
||||
<Clipboard size={14} />
|
||||
</div>
|
||||
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/common'
|
||||
|
||||
import Menu from '$lib/components/details/Menu.svelte'
|
||||
import MenuItem from '$lib/components/common/menu/MenuItem.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
|
||||
type MainButton = {
|
||||
label: string
|
||||
href: string
|
||||
buttonProps: ButtonProps
|
||||
}
|
||||
|
||||
type ButtonProps = any
|
||||
type MenuItemButton = {
|
||||
label: string
|
||||
Icon: any
|
||||
onclick: () => void
|
||||
color?: 'red'
|
||||
}
|
||||
|
||||
export let mainButtons: MainButton[] = []
|
||||
export let menuItems: MenuItemButton[] = []
|
||||
export let title: string
|
||||
</script>
|
||||
|
||||
<div class="border-b p-2 shadow-md">
|
||||
<div class="mx-auto">
|
||||
<div class="flex w-full flex-wrap md:flex-nowrap justify-end gap-x-2 gap-y-4 h-8 items-center">
|
||||
<div class="grow text-lg font-bold">
|
||||
{title}
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<Menu>
|
||||
<svelte:fragment slot="items">
|
||||
{#each menuItems as { label, Icon, onclick, color } (label)}
|
||||
<MenuItem
|
||||
on:click={() => {
|
||||
const div = document.querySelector('[id^="headlessui-menu-items"]')
|
||||
div?.parentElement?.remove()
|
||||
|
||||
onclick()
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class={classNames(
|
||||
'text-xs flex items-center gap-2 flex-row-2',
|
||||
color === 'red' ? 'text-red-500' : ''
|
||||
)}
|
||||
>
|
||||
<Icon class="h-4" />
|
||||
{label}
|
||||
</div>
|
||||
</MenuItem>
|
||||
{/each}
|
||||
</svelte:fragment>
|
||||
</Menu>
|
||||
{#each mainButtons as btn}
|
||||
<Button
|
||||
{...btn.buttonProps}
|
||||
startIcon={{ icon: btn.buttonProps.startIcon }}
|
||||
on:click={btn.buttonProps.onClick}
|
||||
>
|
||||
{btn.label}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,83 @@
|
||||
<script lang="ts">
|
||||
import { Tabs, Tab, TabContent } from '$lib/components/common'
|
||||
import SplitPanesWrapper from '$lib/components/splitPanes/SplitPanesWrapper.svelte'
|
||||
import { CalendarCheck2, Terminal, Webhook } from 'lucide-svelte'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
|
||||
let triggerSelected: 'webhooks' | 'schedule' | 'cli' = 'webhooks'
|
||||
|
||||
export let isOperator: boolean = false
|
||||
</script>
|
||||
|
||||
<main class="h-screen w-full">
|
||||
<div class="flex flex-col h-full">
|
||||
<slot name="header" />
|
||||
<SplitPanesWrapper>
|
||||
<Splitpanes>
|
||||
<Pane size={60} minSize={20} maxSize={70}>
|
||||
<slot name="form" />
|
||||
</Pane>
|
||||
<Pane size={40} minSize={20}>
|
||||
<Splitpanes horizontal class="h-full">
|
||||
<Pane size={100}>
|
||||
<Tabs selected="saved_inputs">
|
||||
<Tab value="saved_inputs">Saved inputs</Tab>
|
||||
{#if !isOperator}
|
||||
<Tab value="details">Details & Triggers</Tab>
|
||||
{/if}
|
||||
<svelte:fragment slot="content">
|
||||
<div class="overflow-hidden" style="height:calc(100% - 32px);">
|
||||
<TabContent value="saved_inputs" class="flex flex-col flex-1 h-full">
|
||||
<slot name="save_inputs" />
|
||||
</TabContent>
|
||||
<TabContent value="details" class="flex flex-col flex-1 h-full">
|
||||
<Splitpanes horizontal class="h-full">
|
||||
<Pane size={50} minSize={20}>
|
||||
<slot name="details" />
|
||||
</Pane>
|
||||
<Pane size={50} minSize={20}>
|
||||
<Tabs bind:selected={triggerSelected}>
|
||||
<Tab value="webhooks">
|
||||
<span class="flex flex-row gap-2 items-center">
|
||||
<Webhook size={14} />
|
||||
Webhooks
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="schedule">
|
||||
<span class="flex flex-row gap-2 items-center">
|
||||
<CalendarCheck2 size={14} />
|
||||
Schedule
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="cli">
|
||||
<span class="flex flex-row gap-2 items-center">
|
||||
<Terminal size={14} />
|
||||
CLI
|
||||
</span>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<div class="h-[calc(100%-32px)]">
|
||||
<div class="h-full overflow-auto">
|
||||
{#if triggerSelected === 'webhooks'}
|
||||
<slot name="webhooks" />
|
||||
{:else if triggerSelected === 'schedule'}
|
||||
<slot name="schedule" />
|
||||
{:else if triggerSelected === 'cli'}
|
||||
<slot name="cli" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</TabContent>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</SplitPanesWrapper>
|
||||
</div>
|
||||
</main>
|
||||
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
import { Menu, Transition, MenuButton, MenuItems } from '@rgossiaux/svelte-headlessui'
|
||||
import { MoreVertical } from 'lucide-svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { createPopperActions, type PopperOptions } from 'svelte-popperjs'
|
||||
import Portal from 'svelte-portal'
|
||||
|
||||
export let hasPadding: boolean = true
|
||||
|
||||
const [popperRef, popperContent] = createPopperActions({ placement: 'auto' })
|
||||
|
||||
const popperOptions: PopperOptions<{}> = {
|
||||
placement: 'bottom-end',
|
||||
strategy: 'fixed',
|
||||
modifiers: [
|
||||
{ name: 'offset', options: { offset: [8, 8] } },
|
||||
{
|
||||
name: 'arrow',
|
||||
options: {
|
||||
padding: 10
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<Menu let:open as="div" class="relative hover:z-50 flex w-full h-full">
|
||||
<span use:popperRef>
|
||||
<MenuButton
|
||||
class={twMerge('h-full w-full flex flex-row gap-2 items-center', hasPadding ? 'px-2' : '')}
|
||||
>
|
||||
<MoreVertical class="w-5 h-5" />
|
||||
</MenuButton>
|
||||
</span>
|
||||
|
||||
<Portal>
|
||||
<div use:popperContent={popperOptions} class="z-[2000]">
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition ease-out duration-[25ms]"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-[25ms]"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<MenuItems
|
||||
class="absolute border right-0 z-50 w-56 origin-top-right top-1 rounded-md bg-white shadow-md focus:outline-none cursor-pointer"
|
||||
>
|
||||
<div class="my-1">
|
||||
<slot name="items" />
|
||||
</div>
|
||||
</MenuItems>
|
||||
</Transition>
|
||||
</div>
|
||||
</Portal>
|
||||
</Menu>
|
||||
@@ -0,0 +1,246 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import bash from 'svelte-highlight/languages/bash'
|
||||
import { Tabs, Tab, TabContent, Button } from '$lib/components/common'
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
||||
import {
|
||||
DEFAULT_WEBHOOK_TYPE,
|
||||
SCRIPT_VIEW_SHOW_EXAMPLE_CURL,
|
||||
SCRIPT_VIEW_SHOW_CREATE_TOKEN_BUTTON
|
||||
} from '$lib/consts'
|
||||
import { ArrowDownRight, ArrowUpRight } from 'lucide-svelte'
|
||||
import UserSettings from '../UserSettings.svelte'
|
||||
import { Highlight } from 'svelte-highlight'
|
||||
import { typescript } from 'svelte-highlight/languages'
|
||||
import ClipboardPanel from './ClipboardPanel.svelte'
|
||||
|
||||
let userSettings: UserSettings
|
||||
|
||||
export let webhooks: {
|
||||
async: {
|
||||
hash?: string
|
||||
path: string
|
||||
}
|
||||
sync: {
|
||||
hash?: string
|
||||
path: string
|
||||
get_path?: string
|
||||
}
|
||||
}
|
||||
export let args: any
|
||||
export let scopes: string[] = []
|
||||
export let isFlow: boolean = false
|
||||
|
||||
let webhookType: 'async' | 'sync' = DEFAULT_WEBHOOK_TYPE
|
||||
let requestType: 'hash' | 'path' | 'get_path' = isFlow ? 'path' : 'path'
|
||||
let tokenType: 'query' | 'headers' = 'headers'
|
||||
|
||||
$: if (webhookType === 'async' && requestType === 'get_path') {
|
||||
// Request type is not supported for async webhooks
|
||||
requestType = 'hash'
|
||||
}
|
||||
|
||||
function getProtocol(url: string) {
|
||||
if (url.startsWith('localhost')) {
|
||||
return 'http://'
|
||||
} else if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
return ''
|
||||
} else {
|
||||
return 'https://'
|
||||
}
|
||||
}
|
||||
|
||||
$: url = webhooks[webhookType][requestType]
|
||||
|
||||
let token = 'YOUR_TOKEN'
|
||||
</script>
|
||||
|
||||
<UserSettings bind:this={userSettings} {scopes} />
|
||||
|
||||
<div class="p-2 flex flex-col w-full gap-4">
|
||||
{#if SCRIPT_VIEW_SHOW_CREATE_TOKEN_BUTTON}
|
||||
<div class="flex flex-row justify-between my-2 gap-2">
|
||||
<input bind:value={token} placeholder="YOUR_TOKEN" class="!text-xs" />
|
||||
<Button size="xs" color="light" variant="border" on:click={userSettings.openDrawer}>
|
||||
Create a Webhook-specific Token
|
||||
<Tooltip light>
|
||||
The token will have a scope such that it can only be used to trigger this script. It is
|
||||
safe to share as it cannot be used to impersonate you.
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="text-xs font-semibold flex flex-row items-center">Request type</div>
|
||||
<ToggleButtonGroup class="h-[30px] w-auto" bind:selected={webhookType}>
|
||||
<ToggleButton
|
||||
label="Async"
|
||||
value="async"
|
||||
tooltip="Jobs can be triggered in asynchronous mode, meaning that the webhook is triggered, and the returning value is the uuid of the job assigned to execute the underlying code."
|
||||
/>
|
||||
<ToggleButton
|
||||
label="Sync"
|
||||
value="sync"
|
||||
tooltip="The second type of autogenerated endpoint is the synchronous webhook. This webhook triggers the execution, automatically extracts the underlying code's return value and returns it as the response."
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{#if !isFlow}
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="text-xs font-semibold flex flex-row items-center">Call method</div>
|
||||
<ToggleButtonGroup class="h-[30px] w-auto" bind:selected={requestType}>
|
||||
<ToggleButton
|
||||
label="POST by path"
|
||||
value="path"
|
||||
icon={ArrowUpRight}
|
||||
selectedColor="#fb923c"
|
||||
/>
|
||||
<ToggleButton
|
||||
label="POST by hash"
|
||||
value="hash"
|
||||
icon={ArrowUpRight}
|
||||
selectedColor="#fb923c"
|
||||
/>
|
||||
|
||||
<ToggleButton
|
||||
label="GET by path"
|
||||
value="get_path"
|
||||
icon={ArrowDownRight}
|
||||
disabled={webhookType !== 'sync'}
|
||||
selectedColor="#14b8a6"
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="text-xs font-semibold flex flex-row items-center">Token configuration</div>
|
||||
<ToggleButtonGroup class="h-[30px] w-auto" bind:selected={tokenType}>
|
||||
<ToggleButton label="Token in Headers" value="headers" />
|
||||
<ToggleButton label="Token in Query" value="query" />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<Tabs selected="rest">
|
||||
<Tab value="rest" size="xs">REST</Tab>
|
||||
{#if SCRIPT_VIEW_SHOW_EXAMPLE_CURL}
|
||||
<Tab value="curl" size="xs">Curl</Tab>
|
||||
{/if}
|
||||
<Tab value="fetch" size="xs">Fetch</Tab>
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
<TabContent value="rest" class="flex flex-col flex-1 h-full ">
|
||||
<div class="flex flex-col gap-2">
|
||||
<ClipboardPanel
|
||||
title="Url"
|
||||
content={tokenType === 'query'
|
||||
? `${getProtocol(url)}${url}?token=${token}`
|
||||
: `${getProtocol(url)}${url}`}
|
||||
/>
|
||||
|
||||
{#if requestType !== 'get_path'}
|
||||
<ClipboardPanel title="Body" content={JSON.stringify(args, null, 2)} />
|
||||
{/if}
|
||||
|
||||
<ClipboardPanel
|
||||
title="Headers"
|
||||
content={JSON.stringify(
|
||||
tokenType === 'query'
|
||||
? {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</TabContent>
|
||||
<TabContent value="curl" class="flex flex-col flex-1 h-full">
|
||||
<div class="flex flex-col gap-2">
|
||||
<Highlight
|
||||
language={bash}
|
||||
code={`${requestType !== 'get_path' ? `BODY='${JSON.stringify(args)}'` : ''}
|
||||
URL=${tokenType === 'query' ? `${url}?token=${token}` : url}\n
|
||||
curl -H 'Content-Type: application/json' ${
|
||||
tokenType === 'headers' ? `-H 'Authorization: Bearer ${token}' ` : ''
|
||||
}-X ${requestType === 'get_path' ? 'GET' : 'POST'} ${
|
||||
requestType !== 'get_path' ? `-d "$BODY" ` : ''
|
||||
}$URL
|
||||
`}
|
||||
class="p-2 whitespace-pre-wrap border rounded-md"
|
||||
/>
|
||||
|
||||
<Highlight
|
||||
language={bash}
|
||||
class="p-2 whitespace-pre-wrap border rounded-md"
|
||||
code={`${
|
||||
webhookType === 'sync'
|
||||
? 'echo $RESPONSE'
|
||||
: `UUID=$(echo $RESPONSE | jq -r .uuid)
|
||||
URL=${$page.url.protocol}//${$page.url.hostname}/api/w/${$workspaceStore}/jobs_u/completed/get_result_maybe/$UUID
|
||||
while true; do
|
||||
RESULT=$(curl -H "Authorization: Bearer $TOKEN" $URL)
|
||||
COMPLETED=$(echo $RESULT | jq .completed)
|
||||
if [ "$COMPLETED" = "true" ]; then
|
||||
echo 'Job result: ' $RESULT
|
||||
break
|
||||
else
|
||||
sleep 1
|
||||
fi
|
||||
done`
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</TabContent>
|
||||
<TabContent value="fetch" class="flex flex-col flex-1 h-full">
|
||||
<Highlight
|
||||
language={typescript}
|
||||
class="p-2 whitespace-pre-wrap border rounded-md"
|
||||
code={`${
|
||||
requestType !== 'get_path' ? 'const body = ' + JSON.stringify(args, null, 2) : ''
|
||||
}
|
||||
fetch(${tokenType === 'query' ? `\`${url}?token=${token}\`` : `\`${url}\``}, {
|
||||
method: '${requestType === 'get_path' ? 'GET' : 'POST'}',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
${tokenType === 'headers' ? `'Authorization': 'Bearer ${token}',` : ''}
|
||||
},
|
||||
${requestType !== 'get_path' ? `body: body` : ''}
|
||||
}).then(
|
||||
response => response.json()
|
||||
).then(data => {
|
||||
${
|
||||
webhookType === 'sync'
|
||||
? 'console.log(data)'
|
||||
: `let UUID = data.uuid;
|
||||
let checkCompletion = setInterval(() => {
|
||||
fetch(\`${$page.url.protocol}//${$page.url.hostname}/api/w/${$workspaceStore}/jobs_u/completed/get_result_maybe/\$\{UUID\}\`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ${token}'
|
||||
}
|
||||
}).then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.completed) {
|
||||
console.log('Job result: ', data);
|
||||
clearInterval(checkCompletion);
|
||||
}
|
||||
});
|
||||
}, 1000);`
|
||||
}
|
||||
});`}
|
||||
/>
|
||||
</TabContent>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
</div>
|
||||
@@ -0,0 +1,65 @@
|
||||
import { ScriptService, JobService, Script } from '$lib/gen'
|
||||
|
||||
export async function deleteScript(hash: string, workspace: string) {
|
||||
return await ScriptService.deleteScriptByHash({ workspace: workspace, hash })
|
||||
}
|
||||
|
||||
export async function archiveScript(hash: string, workspace: string) {
|
||||
return await ScriptService.archiveScriptByHash({ workspace: workspace, hash })
|
||||
}
|
||||
|
||||
export async function unarchiveScript(hash: string, workspace: string) {
|
||||
const r = await ScriptService.getScriptByHash({ workspace: workspace, hash })
|
||||
const ns = await ScriptService.createScript({
|
||||
workspace: workspace,
|
||||
requestBody: {
|
||||
...r,
|
||||
parent_hash: hash,
|
||||
lock: r.lock?.split('\n')
|
||||
}
|
||||
})
|
||||
return ns
|
||||
}
|
||||
|
||||
export async function getScriptDeploymentStatus(hash: string, workspace: string): Promise<any> {
|
||||
return await ScriptService.getScriptDeploymentStatus({
|
||||
workspace: workspace,
|
||||
hash: hash
|
||||
})
|
||||
}
|
||||
|
||||
export async function loadScript(hash: string, workspace: string): Promise<any> {
|
||||
let script: Script
|
||||
try {
|
||||
script = await ScriptService.getScriptByHash({ workspace: workspace, hash })
|
||||
} catch {
|
||||
script = await ScriptService.getScriptByPath({ workspace: workspace, path: hash })
|
||||
hash = script.hash
|
||||
}
|
||||
return { script, hash }
|
||||
}
|
||||
|
||||
export async function runScript(
|
||||
scheduledForStr: string | undefined,
|
||||
args: Record<string, any>,
|
||||
hash: string,
|
||||
workspace: string,
|
||||
invisibleToOwner?: boolean
|
||||
) {
|
||||
const scheduledFor = scheduledForStr ? new Date(scheduledForStr).toISOString() : undefined
|
||||
let run = await JobService.runScriptByHash({
|
||||
workspace: workspace,
|
||||
hash: hash,
|
||||
requestBody: args,
|
||||
scheduledFor,
|
||||
invisibleToOwner
|
||||
})
|
||||
return run
|
||||
}
|
||||
export function curlCommand(async: boolean, args: any, $page: any, workspace: string, script: any) {
|
||||
return `curl -H 'Content-Type: application/json' -H "Authorization: Bearer $TOKEN" -X POST -d '${JSON.stringify(
|
||||
args
|
||||
)}' ${$page.url.protocol}//${$page.url.hostname}/api/w/${workspace}/jobs/run${
|
||||
async ? '' : '_wait_result'
|
||||
}/p/${script?.path}`
|
||||
}
|
||||
@@ -3,56 +3,47 @@
|
||||
import { FlowService, JobService, ScheduleService, type Flow, type Schedule } from '$lib/gen'
|
||||
import {
|
||||
canWrite,
|
||||
copyToClipboard,
|
||||
defaultIfEmptyString,
|
||||
displayDaysAgo,
|
||||
emptyString,
|
||||
encodeState
|
||||
} from '$lib/utils'
|
||||
import {
|
||||
faArchive,
|
||||
faCalendar,
|
||||
faChevronDown,
|
||||
faChevronUp,
|
||||
faClipboard,
|
||||
faCodeFork,
|
||||
faEdit,
|
||||
faFileExport,
|
||||
faList,
|
||||
faPlay,
|
||||
faShare,
|
||||
faTrash
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { faCalendar, faCodeFork, faEdit } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
|
||||
import DetailPageLayout from '$lib/components/details/DetailPageLayout.svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import { Alert, Badge, Button, Skeleton, Tab, TabContent, Tabs } from '$lib/components/common'
|
||||
import CronInput from '$lib/components/CronInput.svelte'
|
||||
import FlowViewer from '$lib/components/FlowViewer.svelte'
|
||||
import { Alert, Button, Skeleton } from '$lib/components/common'
|
||||
import JobArgs from '$lib/components/JobArgs.svelte'
|
||||
import MoveDrawer from '$lib/components/MoveDrawer.svelte'
|
||||
import RunForm from '$lib/components/RunForm.svelte'
|
||||
import ScheduleEditor from '$lib/components/ScheduleEditor.svelte'
|
||||
import ShareModal from '$lib/components/ShareModal.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import UserSettings from '$lib/components/UserSettings.svelte'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { slide } from 'svelte/transition'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import Urlize from '$lib/components/Urlize.svelte'
|
||||
import DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte'
|
||||
import SavedInputs from '$lib/components/SavedInputs.svelte'
|
||||
import { FolderOpen, Archive, Trash, Server, Share, PenBox, ListOrdered } from 'lucide-svelte'
|
||||
|
||||
let userSettings: UserSettings
|
||||
import DetailPageHeader from '$lib/components/details/DetailPageHeader.svelte'
|
||||
import WebhooksPanel from '$lib/components/details/WebhooksPanel.svelte'
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import CliHelpBox from '$lib/components/CliHelpBox.svelte'
|
||||
import InlineCodeCopy from '$lib/components/InlineCodeCopy.svelte'
|
||||
import FlowGraphViewer from '$lib/components/FlowGraphViewer.svelte'
|
||||
import SplitPanesWrapper from '$lib/components/splitPanes/SplitPanesWrapper.svelte'
|
||||
import SchemaViewer from '$lib/components/SchemaViewer.svelte'
|
||||
|
||||
let flow: Flow | undefined
|
||||
let schedule: Schedule | undefined
|
||||
let can_write = false
|
||||
|
||||
let path = $page.params.path
|
||||
let shareModal: ShareModal
|
||||
|
||||
$: cliCommand = `wmill flow run ${flow?.path} -d '${JSON.stringify(args)}'`
|
||||
|
||||
$: {
|
||||
if ($workspaceStore && $userStore) {
|
||||
loadFlow()
|
||||
@@ -100,6 +91,8 @@
|
||||
requestBody: { enabled }
|
||||
})
|
||||
loadSchedule()
|
||||
|
||||
sendUserToast(`Schedule ${enabled ? 'enabled' : 'disabled'}`)
|
||||
} catch (err) {
|
||||
sendUserToast(`Cannot ` + (enabled ? 'disable' : 'enable') + ` schedule: ${err}`, true)
|
||||
loadSchedule()
|
||||
@@ -135,35 +128,95 @@
|
||||
}
|
||||
let scheduleEditor: ScheduleEditor
|
||||
|
||||
let viewWebhookCommand = false
|
||||
|
||||
let args = undefined
|
||||
|
||||
function curlCommand(async: boolean) {
|
||||
return `curl -H 'Content-Type: application/json' -H "Authorization: Bearer $TOKEN" -X POST -d '${JSON.stringify(
|
||||
args
|
||||
)}' ${$page.url.protocol}//${$page.url.hostname}/api/w/${$workspaceStore}/jobs/run${
|
||||
async ? '' : '_wait_result'
|
||||
}/f/${flow?.path}`
|
||||
}
|
||||
|
||||
let webhook: HTMLHeadElement
|
||||
let moveDrawer: MoveDrawer
|
||||
let deploymentDrawer: DeployWorkspaceDrawer
|
||||
let runForm: RunForm
|
||||
|
||||
function getMainButtons(flow: Flow | undefined) {
|
||||
if (!flow || $userStore?.operator) return []
|
||||
|
||||
const buttons: any = []
|
||||
if (!$userStore?.operator) {
|
||||
buttons.push({
|
||||
label: 'Fork',
|
||||
buttonProps: {
|
||||
href: `/flows/add?template=${flow.path}`,
|
||||
variant: 'border',
|
||||
color: 'light',
|
||||
size: 'xs',
|
||||
disabled: !can_write,
|
||||
startIcon: faCodeFork
|
||||
}
|
||||
})
|
||||
|
||||
buttons.push({
|
||||
label: 'Edit',
|
||||
buttonProps: {
|
||||
href: `/flows/edit/${path}?nodraft=true&args=${encodeState(args)}`,
|
||||
variant: 'contained',
|
||||
size: 'sm',
|
||||
color: 'dark',
|
||||
disabled: !can_write,
|
||||
startIcon: faEdit
|
||||
}
|
||||
})
|
||||
}
|
||||
return buttons
|
||||
}
|
||||
|
||||
function getMenuItems(flow: Flow | undefined) {
|
||||
if (!flow || $userStore?.operator) return []
|
||||
|
||||
const menuItems: any = []
|
||||
|
||||
menuItems.push({
|
||||
label: 'Share',
|
||||
onclick: () => shareModal.openDrawer(flow?.path ?? '', 'flow'),
|
||||
Icon: Share,
|
||||
disabled: !can_write
|
||||
})
|
||||
|
||||
menuItems.push({
|
||||
label: 'Move/Rename',
|
||||
onclick: () => moveDrawer.openDrawer(flow?.path ?? '', flow?.summary, 'flow'),
|
||||
Icon: FolderOpen
|
||||
})
|
||||
|
||||
menuItems.push({
|
||||
label: 'Deploy to staging/prod',
|
||||
onclick: () => deploymentDrawer.openDrawer(flow?.path ?? '', 'flow'),
|
||||
Icon: Server
|
||||
})
|
||||
|
||||
if (can_write) {
|
||||
menuItems.push({
|
||||
label: flow.archived ? 'Unarchive' : 'Archive',
|
||||
onclick: () => flow?.path && archiveFlow(),
|
||||
Icon: Archive,
|
||||
color: 'red'
|
||||
})
|
||||
menuItems.push({
|
||||
label: 'Delete',
|
||||
onclick: () => flow?.path && deleteFlow(),
|
||||
Icon: Trash,
|
||||
color: 'red'
|
||||
})
|
||||
}
|
||||
return menuItems
|
||||
}
|
||||
</script>
|
||||
|
||||
<ScheduleEditor on:update={() => loadSchedule()} bind:this={scheduleEditor} />
|
||||
|
||||
<UserSettings bind:this={userSettings} scopes={[`run:flow/${flow?.path}`]} />
|
||||
|
||||
<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 }]]}
|
||||
/>
|
||||
|
||||
<ScheduleEditor on:update={() => loadSchedule()} bind:this={scheduleEditor} />
|
||||
<DeployWorkspaceDrawer bind:this={deploymentDrawer} />
|
||||
|
||||
<ShareModal bind:this={shareModal} />
|
||||
<MoveDrawer
|
||||
bind:this={moveDrawer}
|
||||
on:update={async (e) => {
|
||||
@@ -173,261 +226,117 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<CenteredPage>
|
||||
{#if flow}
|
||||
<div class="prose-sm mx-auto mt-6">
|
||||
<div
|
||||
class="flex flex-row-reverse w-full flex-wrap md:flex-nowrap justify-between gap-x-2 gap-y-4"
|
||||
>
|
||||
<div class="flex flex-row-reverse gap-2 h-full">
|
||||
<Button
|
||||
href="/flows/run/{path}"
|
||||
variant="contained"
|
||||
color="blue"
|
||||
size="md"
|
||||
startIcon={{ icon: faPlay }}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
{#if !$userStore?.operator}
|
||||
<Button
|
||||
href="/flows/edit/{path}?nodraft=true&args={encodeState(args)}"
|
||||
variant="contained"
|
||||
color="blue"
|
||||
size="md"
|
||||
startIcon={{ icon: faEdit }}
|
||||
disabled={!can_write}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
href="/flows/add?template={flow.path}"
|
||||
variant="border"
|
||||
color="blue"
|
||||
size="md"
|
||||
startIcon={{ icon: faCodeFork }}
|
||||
>
|
||||
Fork
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
href="/runs/{flow.path}"
|
||||
variant="border"
|
||||
color="blue"
|
||||
size="md"
|
||||
startIcon={{ icon: faList }}
|
||||
>
|
||||
View runs
|
||||
</Button>
|
||||
</div>
|
||||
<h1 class="mb-1 truncate grow">
|
||||
{defaultIfEmptyString(flow.summary, flow.path)}
|
||||
</h1>
|
||||
</div>
|
||||
{#if !emptyString(flow.summary)}
|
||||
<span class="text-lg font-semibold">{flow.path}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<ShareModal bind:this={shareModal} />
|
||||
{#if flow}
|
||||
<DetailPageLayout isOperator={$userStore?.operator}>
|
||||
<svelte:fragment slot="header">
|
||||
<DetailPageHeader
|
||||
mainButtons={getMainButtons(flow)}
|
||||
menuItems={getMenuItems(flow)}
|
||||
title={defaultIfEmptyString(flow.summary, flow.path)}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="form">
|
||||
<SplitPanesWrapper>
|
||||
<Splitpanes horizontal>
|
||||
<Pane size={60} minSize={20}>
|
||||
<div class="p-8 w-full max-w-3xl mx-auto gap-2">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
{#if !emptyString(flow.summary)}
|
||||
<span class="text-lg font-semibold">{flow.path}</span>
|
||||
{/if}
|
||||
<span class="text-sm text-gray-600">
|
||||
Edited {displayDaysAgo(flow.edited_at ?? '')} by {flow.edited_by}
|
||||
</span>
|
||||
|
||||
<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}
|
||||
<div>
|
||||
<span class="text-sm text-gray-600">
|
||||
Edited {displayDaysAgo(flow.edited_at ?? '')} by {flow.edited_by}
|
||||
{#if flow.archived}
|
||||
<div class="" />
|
||||
<Alert type="error" title="Archived">This flow was archived</Alert>
|
||||
{/if}
|
||||
|
||||
{#if schedule}
|
||||
<a href="#primary-schedule" class="ml-2">
|
||||
<Badge color="dark-blue">Primary schedule</Badge>
|
||||
</a>{/if}</span
|
||||
>
|
||||
|
||||
{#if flow.archived}
|
||||
<div class="mt-2" />
|
||||
<Alert type="error" title="Archived">This flow was archived</Alert>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-2 flex-wrap mt-2">
|
||||
<!-- <Button
|
||||
target="_blank"
|
||||
href={flowToHubUrl(flow).toString()}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
startIcon={{ icon: faGlobe }}
|
||||
>
|
||||
Publish to Hub
|
||||
</Button> -->
|
||||
<Button
|
||||
on:click={() => shareModal.openDrawer(flow?.path ?? '', 'flow')}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
startIcon={{ icon: faShare }}
|
||||
disabled={!can_write}
|
||||
>
|
||||
Share
|
||||
</Button>
|
||||
<Button
|
||||
on:click={() => scheduleEditor?.openNew(true, flow?.path ?? '')}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
startIcon={{ icon: faCalendar }}
|
||||
>
|
||||
New Schedule
|
||||
</Button>
|
||||
<Button
|
||||
on:click={() => moveDrawer.openDrawer(flow?.path ?? '', flow?.summary, 'flow')}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
startIcon={{ icon: faEdit }}
|
||||
>
|
||||
Move/Rename
|
||||
</Button>
|
||||
<Button
|
||||
on:click={() => deploymentDrawer.openDrawer(flow?.path ?? '', 'flow')}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
startIcon={{ icon: faFileExport }}
|
||||
>
|
||||
Deploy to staging/prod
|
||||
</Button>
|
||||
<Button
|
||||
btnClasses="ml-2"
|
||||
variant="border"
|
||||
size="xs"
|
||||
on:click={() => webhook.scrollIntoView()}>Webhook</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 grid grid-cols-1 sm:grid-cols-3 gap-6">
|
||||
<div class="col-span-2 box">
|
||||
<RunForm
|
||||
{loading}
|
||||
autofocus
|
||||
detailed={false}
|
||||
bind:isValid
|
||||
runnable={flow}
|
||||
runAction={runFlow}
|
||||
bind:args
|
||||
viewCliRun
|
||||
isFlow
|
||||
/>
|
||||
</div>
|
||||
{#if !emptyString(flow.description)}
|
||||
<div class="box overflow-auto break-words whitespace-pre-wrap">
|
||||
<Urlize text={defaultIfEmptyString(flow.description, 'No description')} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<FlowViewer {flow} noSummary={true} />
|
||||
|
||||
<h2 bind:this={webhook} class="mt-10 text-gray-700 pb-1 mb-3 border-b"
|
||||
>Webhooks<Tooltip>
|
||||
Pass the input as a json payload, the token as a Bearer token or as query arg
|
||||
`?token=XXX` and pass as header: 'Content-Type: application/json'. The webhook also
|
||||
support 'x-www-form-urlencoded' encoded payloads <a
|
||||
href="https://www.windmill.dev/docs/core_concepts/webhooks">See docs</a
|
||||
></Tooltip
|
||||
></h2
|
||||
>
|
||||
<div class="box max-w-5xl">
|
||||
<div class="flex w-full flex-justify-between mb-1">
|
||||
<a
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
copyToClipboard($page.url.protocol + '//' + urlAsync)
|
||||
}}
|
||||
href={$page.url.protocol + '//' + urlAsync}
|
||||
class="whitespace-nowrap text-ellipsis overflow-hidden mr-1 w-full"
|
||||
>
|
||||
{urlAsync}
|
||||
<span class="text-gray-700 ml-2">
|
||||
<Icon data={faClipboard} />
|
||||
</span>
|
||||
</a>
|
||||
<Badge>UUID/Async</Badge>
|
||||
</div>
|
||||
<div class="mb-2 w-full flex flex-justify-between">
|
||||
<a
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
copyToClipboard($page.url.protocol + '//' + urlSync)
|
||||
}}
|
||||
href={$page.url.protocol + '//' + urlSync}
|
||||
class="whitespace-nowrap text-ellipsis overflow-hidden mr-1 w-full"
|
||||
>
|
||||
{urlSync}
|
||||
<span class="text-gray-700 ml-2">
|
||||
<Icon data={faClipboard} />
|
||||
</span>
|
||||
</a>
|
||||
<Badge>Result/Sync</Badge>
|
||||
</div>
|
||||
<div class="flex flex-row-reverse">
|
||||
<Button size="xs" on:click={userSettings.openDrawer}
|
||||
>Create a Webhook-specific Token <Tooltip
|
||||
>The token will have a scope such that it can only be used to trigger this flow. It
|
||||
is safe to share as it cannot be used to impersonate you.</Tooltip
|
||||
></Button
|
||||
>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 mt-2">
|
||||
<div class="flex">
|
||||
<Button
|
||||
color="light"
|
||||
size="lg"
|
||||
endIcon={{ icon: viewWebhookCommand ? faChevronUp : faChevronDown }}
|
||||
on:click={() => (viewWebhookCommand = !viewWebhookCommand)}
|
||||
>
|
||||
CURL
|
||||
</Button>
|
||||
</div>
|
||||
{#if viewWebhookCommand}
|
||||
<div transition:slide|local class="px-4">
|
||||
<Tabs selected="async">
|
||||
<Tab value="async">UUID/Async</Tab>
|
||||
<Tab value="sync">Result/Sync</Tab>
|
||||
<svelte:fragment slot="content">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<pre class="bg-gray-700 text-gray-100 p-2 font-mono text-sm whitespace-pre-wrap"
|
||||
><TabContent value="async"
|
||||
>{curlCommand(true)} <span
|
||||
on:click={() => copyToClipboard(curlCommand(true))}
|
||||
class="cursor-pointer ml-2"><Icon data={faClipboard} /></span
|
||||
><br /><br />//^ returns an UUID. Fetch result until completed == true<br
|
||||
/>curl -H "Authorization: Bearer $TOKEN" {$page.url.protocol}//{$page.url
|
||||
.hostname}/api/w/{$workspaceStore}/jobs_u/completed/get_result_maybe/$UUID</TabContent
|
||||
><TabContent value="sync"
|
||||
>{curlCommand(false)} <span
|
||||
on:click={() => copyToClipboard(curlCommand(false))}
|
||||
class="cursor-pointer ml-2"><Icon data={faClipboard} /></span
|
||||
></TabContent
|
||||
></pre
|
||||
>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
{#if !emptyString(flow.description)}
|
||||
<div class=" break-words whitespace-pre-wrap text-sm">
|
||||
<Urlize text={defaultIfEmptyString(flow.description, 'No description')} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if schedule}
|
||||
<div class="mt-10">
|
||||
<h2
|
||||
id="primary-schedule"
|
||||
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>
|
||||
<RunForm
|
||||
{loading}
|
||||
autofocus
|
||||
detailed={false}
|
||||
bind:isValid
|
||||
runnable={flow}
|
||||
runAction={runFlow}
|
||||
bind:args
|
||||
isFlow
|
||||
bind:this={runForm}
|
||||
/>
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane size={40} minSize={20}>
|
||||
<FlowGraphViewer download {flow} overflowAuto noSide={true} />
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</SplitPanesWrapper>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="save_inputs">
|
||||
<SavedInputs
|
||||
flowPath={flow?.path}
|
||||
{isValid}
|
||||
args={args ?? {}}
|
||||
on:selected_args={(e) => {
|
||||
runForm?.setArgs(JSON.parse(JSON.stringify(e.detail)))
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="details">
|
||||
<div class="p-1">
|
||||
<SchemaViewer schema={flow.schema} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="webhooks">
|
||||
<WebhooksPanel
|
||||
scopes={[`run:flow/${flow?.path}`]}
|
||||
webhooks={{
|
||||
async: {
|
||||
path: urlAsync
|
||||
},
|
||||
sync: {
|
||||
path: urlSync
|
||||
}
|
||||
}}
|
||||
isFlow={true}
|
||||
{args}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="schedule">
|
||||
<div class="flex flex-row justify-end p-2">
|
||||
<Button
|
||||
on:click={() => scheduleEditor?.openNew(true, flow?.path ?? '')}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
startIcon={{ icon: faCalendar }}
|
||||
>
|
||||
New Schedule
|
||||
</Button>
|
||||
</div>
|
||||
{#if schedule}
|
||||
<div class="p-2 flex flex-col gap-2">
|
||||
<div class="flex flex-row justify-between h-8">
|
||||
<div class="flex flex-row gap-2">
|
||||
<input
|
||||
class="inline-block !w-32"
|
||||
type="text"
|
||||
id="cron-schedule"
|
||||
name="cron-schedule"
|
||||
placeholder="*/30 * * * *"
|
||||
value={schedule.schedule}
|
||||
disabled={true}
|
||||
/>
|
||||
<Badge color="indigo" small>Primary schedule</Badge>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2">
|
||||
<Toggle
|
||||
checked={schedule.enabled}
|
||||
on:change={(e) => {
|
||||
@@ -437,51 +346,41 @@
|
||||
sendUserToast('not enough permission', true)
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'On'
|
||||
}}
|
||||
size="xs"
|
||||
/>
|
||||
<Button size="xs" on:click={() => scheduleEditor?.openEdit(flow?.path ?? '', true)}
|
||||
>Edit schedule</Button
|
||||
<Button size="xs" variant="border" color="light" href={`/runs/${flow?.path}`}>
|
||||
<div class="flex flex-row gap-2">
|
||||
<ListOrdered size={14} />
|
||||
Runs
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
on:click={() => scheduleEditor?.openEdit(flow?.path ?? '', true)}
|
||||
>
|
||||
</h2>
|
||||
<div class="max-w-lg">
|
||||
<PenBox size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{#if Object.keys(schedule?.args ?? {}).length > 0}
|
||||
<div class="">
|
||||
<JobArgs args={schedule.args ?? {}} />
|
||||
</div>
|
||||
<div class="box max-w-5xl mt-2">
|
||||
<CronInput
|
||||
disabled={true}
|
||||
schedule={schedule.schedule}
|
||||
timezone={schedule.timezone}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="text-xs texg-gray-700"> This flow takes no argument </div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="cli">
|
||||
<div class="p-2">
|
||||
<InlineCodeCopy content={cliCommand} />
|
||||
<CliHelpBox />
|
||||
</div>
|
||||
<div>
|
||||
{#if can_write}
|
||||
<h2 class="mt-4">Danger zone</h2>
|
||||
<div class="flex gap-2 mt-6">
|
||||
<Button
|
||||
on:click={() => flow?.path && archiveFlow()}
|
||||
variant="border"
|
||||
color="red"
|
||||
size="md"
|
||||
startIcon={{ icon: faArchive }}
|
||||
disabled={!can_write}
|
||||
>
|
||||
{flow.archived ? 'Unarchive' : 'Archive'}
|
||||
</Button>
|
||||
<Button
|
||||
on:click={() => flow?.path && deleteFlow()}
|
||||
variant="border"
|
||||
color="red"
|
||||
size="md"
|
||||
startIcon={{ icon: faTrash }}
|
||||
disabled={!can_write}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</CenteredPage>
|
||||
</svelte:fragment>
|
||||
</DetailPageLayout>
|
||||
{/if}
|
||||
|
||||
@@ -2,71 +2,70 @@
|
||||
import { page } from '$app/stores'
|
||||
import { JobService, ScriptService, type Script } from '$lib/gen'
|
||||
import {
|
||||
truncateHash,
|
||||
displayDaysAgo,
|
||||
defaultIfEmptyString,
|
||||
copyToClipboard,
|
||||
emptyString,
|
||||
encodeState,
|
||||
canWrite
|
||||
canWrite,
|
||||
displayDaysAgo,
|
||||
truncateHash
|
||||
} from '$lib/utils'
|
||||
import {
|
||||
faPlay,
|
||||
faEdit,
|
||||
faArchive,
|
||||
faList,
|
||||
faTrash,
|
||||
faCalendar,
|
||||
faShare,
|
||||
faGlobe,
|
||||
faCodeFork,
|
||||
faClipboard,
|
||||
faArrowLeft,
|
||||
faChevronUp,
|
||||
faChevronDown,
|
||||
faFileExport
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { faEdit, faCalendar, faCodeFork, faHistory } from '@fortawesome/free-solid-svg-icons'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import ShareModal from '$lib/components/ShareModal.svelte'
|
||||
import { superadmin, userStore, workspaceStore } from '$lib/stores'
|
||||
import SharedBadge from '$lib/components/SharedBadge.svelte'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import SchemaViewer from '$lib/components/SchemaViewer.svelte'
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import { onDestroy } from 'svelte'
|
||||
import HighlightCode from '$lib/components/HighlightCode.svelte'
|
||||
import { Badge, Tabs, Tab, TabContent, Button, Alert } from '$lib/components/common'
|
||||
import {
|
||||
Tabs,
|
||||
Tab,
|
||||
TabContent,
|
||||
Button,
|
||||
Badge,
|
||||
Alert,
|
||||
DrawerContent,
|
||||
Drawer
|
||||
} from '$lib/components/common'
|
||||
import Skeleton from '$lib/components/common/skeleton/Skeleton.svelte'
|
||||
import UserSettings from '$lib/components/UserSettings.svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import RunForm from '$lib/components/RunForm.svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import ScheduleEditor from '$lib/components/ScheduleEditor.svelte'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import { slide } from 'svelte/transition'
|
||||
import MoveDrawer from '$lib/components/MoveDrawer.svelte'
|
||||
import {
|
||||
DEFAULT_WEBHOOK_TYPE,
|
||||
SCRIPT_VIEW_SHOW_PUBLISH_TO_HUB,
|
||||
SCRIPT_VIEW_SHOW_SCHEDULE,
|
||||
SCRIPT_VIEW_SHOW_EXAMPLE_CURL,
|
||||
SCRIPT_VIEW_SHOW_CREATE_TOKEN_BUTTON,
|
||||
SCRIPT_VIEW_WEBHOOK_INFO_LINK,
|
||||
SCRIPT_VIEW_WEBHOOK_INFO_TIP
|
||||
} from '$lib/consts'
|
||||
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { scriptToHubUrl } from '$lib/hub'
|
||||
import Urlize from '$lib/components/Urlize.svelte'
|
||||
import DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte'
|
||||
|
||||
let userSettings: UserSettings
|
||||
import SavedInputs from '$lib/components/SavedInputs.svelte'
|
||||
import WebhooksPanel from '$lib/components/details/WebhooksPanel.svelte'
|
||||
import DetailPageLayout from '$lib/components/details/DetailPageLayout.svelte'
|
||||
import DetailPageHeader from '$lib/components/details/DetailPageHeader.svelte'
|
||||
import InlineCodeCopy from '$lib/components/InlineCodeCopy.svelte'
|
||||
import CliHelpBox from '$lib/components/CliHelpBox.svelte'
|
||||
import {
|
||||
Archive,
|
||||
ArchiveRestore,
|
||||
FolderOpen,
|
||||
Globe2,
|
||||
Loader2,
|
||||
Server,
|
||||
Share,
|
||||
Trash
|
||||
} from 'lucide-svelte'
|
||||
import { SCRIPT_VIEW_SHOW_PUBLISH_TO_HUB } from '$lib/consts'
|
||||
import { scriptToHubUrl } from '$lib/hub'
|
||||
import SharedBadge from '$lib/components/SharedBadge.svelte'
|
||||
import ScriptVersionHistory from '$lib/components/ScriptVersionHistory.svelte'
|
||||
|
||||
let script: Script | undefined
|
||||
let topHash: string | undefined
|
||||
let can_write = false
|
||||
let deploymentInProgress = false
|
||||
let intervalId: NodeJS.Timer
|
||||
|
||||
let shareModal: ShareModal
|
||||
let runForm: RunForm
|
||||
|
||||
$: cliCommand = `wmill script run ${script?.path} -d '${JSON.stringify(args)}'`
|
||||
|
||||
$: loading = !script
|
||||
$: if ($workspaceStore) {
|
||||
@@ -186,20 +185,147 @@
|
||||
}
|
||||
}
|
||||
let scheduleEditor: ScheduleEditor
|
||||
let webhookElem: HTMLHeadingElement
|
||||
|
||||
let viewWebhookCommand = false
|
||||
|
||||
let args = undefined
|
||||
function curlCommand(async: boolean) {
|
||||
return `curl -H 'Content-Type: application/json' -H "Authorization: Bearer $TOKEN" -X POST -d '${JSON.stringify(
|
||||
args
|
||||
)}' ${$page.url.protocol}//${$page.url.hostname}/api/w/${$workspaceStore}/jobs/run${
|
||||
async ? '' : '_wait_result'
|
||||
}/p/${script?.path}`
|
||||
}
|
||||
let moveDrawer: MoveDrawer
|
||||
let deploymentDrawer: DeployWorkspaceDrawer
|
||||
|
||||
function getMainButtons(script: Script | undefined) {
|
||||
if (!script || $userStore?.operator) return []
|
||||
|
||||
const buttons: any = []
|
||||
|
||||
if (Array.isArray(script.parent_hashes) && script.parent_hashes.length > 0) {
|
||||
buttons.push({
|
||||
label: `History`,
|
||||
buttonProps: {
|
||||
onClick: () => {
|
||||
versionsDrawerOpen = !versionsDrawerOpen
|
||||
},
|
||||
|
||||
size: 'xs',
|
||||
color: 'light',
|
||||
startIcon: faHistory
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (!$userStore?.operator) {
|
||||
if (!topHash) {
|
||||
buttons.push({
|
||||
label: 'Fork',
|
||||
buttonProps: {
|
||||
href: `/scripts/add?template=${script.path}`,
|
||||
|
||||
size: 'xs',
|
||||
color: 'light',
|
||||
startIcon: faCodeFork
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
buttons.push({
|
||||
label: 'Edit',
|
||||
buttonProps: {
|
||||
href: `/scripts/edit/${script.path}?args=${encodeState(args)}${
|
||||
topHash ? `&hash=${script.hash}&topHash=` + topHash : ''
|
||||
}`,
|
||||
disabled: !can_write,
|
||||
size: 'xs',
|
||||
startIcon: faEdit,
|
||||
color: 'dark',
|
||||
variant: 'contained'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return buttons
|
||||
}
|
||||
|
||||
function getMenuItems(script: Script | undefined) {
|
||||
if (!script || $userStore?.operator) return []
|
||||
|
||||
const menuItems: any = []
|
||||
|
||||
menuItems.push({
|
||||
label: 'Move/Rename',
|
||||
Icon: FolderOpen,
|
||||
onclick: () => {
|
||||
moveDrawer.openDrawer(script?.path ?? '', script?.summary, 'script')
|
||||
}
|
||||
})
|
||||
|
||||
menuItems.push({
|
||||
label: 'Share',
|
||||
Icon: Share,
|
||||
onclick: () => {
|
||||
shareModal.openDrawer(script?.path ?? '', 'script')
|
||||
}
|
||||
})
|
||||
|
||||
menuItems.push({
|
||||
label: 'Deploy to staging/prod',
|
||||
Icon: Server,
|
||||
onclick: () => {
|
||||
deploymentDrawer.openDrawer(script?.path ?? '', 'script')
|
||||
}
|
||||
})
|
||||
|
||||
if (SCRIPT_VIEW_SHOW_PUBLISH_TO_HUB) {
|
||||
menuItems.push({
|
||||
label: 'Publish to Hub',
|
||||
Icon: Globe2,
|
||||
onclick: () => {
|
||||
if (!script) return
|
||||
|
||||
goto(
|
||||
scriptToHubUrl(
|
||||
script.content,
|
||||
script.summary,
|
||||
script.description ?? '',
|
||||
script.kind,
|
||||
script.language,
|
||||
script.schema,
|
||||
script.language == 'deno' ? '' : script.lock
|
||||
).toString()
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (script.archived) {
|
||||
menuItems.push({
|
||||
label: 'Unarchive',
|
||||
Icon: ArchiveRestore,
|
||||
onclick: async () => {
|
||||
unarchiveScript(script.hash)
|
||||
},
|
||||
color: 'red'
|
||||
})
|
||||
} else {
|
||||
menuItems.push({
|
||||
label: 'Archive',
|
||||
Icon: Archive,
|
||||
onclick: async () => {
|
||||
archiveScript(script.hash)
|
||||
},
|
||||
color: 'red'
|
||||
})
|
||||
}
|
||||
|
||||
menuItems.push({
|
||||
label: 'Delete',
|
||||
Icon: Trash,
|
||||
onclick: async () => {
|
||||
deleteScript(script.path)
|
||||
},
|
||||
color: 'red'
|
||||
})
|
||||
|
||||
return menuItems
|
||||
}
|
||||
|
||||
let versionsDrawerOpen = false
|
||||
</script>
|
||||
|
||||
<MoveDrawer
|
||||
@@ -209,237 +335,158 @@
|
||||
loadScript($page.params.hash)
|
||||
}}
|
||||
/>
|
||||
<DeployWorkspaceDrawer bind:this={deploymentDrawer} />
|
||||
|
||||
<DeployWorkspaceDrawer bind:this={deploymentDrawer} />
|
||||
<ScheduleEditor bind:this={scheduleEditor} />
|
||||
<ShareModal bind:this={shareModal} />
|
||||
|
||||
{#if script}
|
||||
<CenteredPage>
|
||||
<Skeleton {loading} layout={[[{ h: 1.5, w: 40 }], 1, [{ h: 1, w: 30 }]]} />
|
||||
<Drawer bind:open={versionsDrawerOpen} size="1200px">
|
||||
<DrawerContent title="Versions History" on:close={() => (versionsDrawerOpen = false)}>
|
||||
<ScriptVersionHistory
|
||||
scriptPath={script.path}
|
||||
openDetails
|
||||
on:openDetails={(e) => {
|
||||
if (script) {
|
||||
goto(`/scripts/get/${e.detail.version}?workspace=${$workspaceStore}`)
|
||||
}
|
||||
versionsDrawerOpen = false
|
||||
}}
|
||||
/>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
<DetailPageLayout isOperator={$userStore?.operator}>
|
||||
<svelte:fragment slot="header">
|
||||
<DetailPageHeader
|
||||
mainButtons={getMainButtons(script)}
|
||||
menuItems={getMenuItems(script)}
|
||||
title={defaultIfEmptyString(script.summary, script.path)}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="form">
|
||||
<div class="p-8 w-full max-w-3xl mx-auto">
|
||||
<div class="flex flex-col gap-0.5 mb-8">
|
||||
{#if script.lock_error_logs || topHash || script.archived || script.deleted}
|
||||
<div class="flex flex-col gap-2 my-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="mt-2" />
|
||||
<Alert type="warning" title="Not HEAD">
|
||||
This hash is not HEAD (latest non-archived version at this path) :
|
||||
<a href="/scripts/get/{topHash}?workspace={$workspaceStore}"
|
||||
>Go to the HEAD of this path</a
|
||||
>
|
||||
</Alert>
|
||||
{/if}
|
||||
{#if script.archived && !topHash}
|
||||
<Alert type="error" title="Archived">This path was archived</Alert>
|
||||
{/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}
|
||||
|
||||
<div class="prose-sm mx-auto mt-6">
|
||||
<div
|
||||
class="flex flex-row-reverse w-full flex-wrap md:flex-nowrap justify-between gap-x-2 gap-y-4"
|
||||
>
|
||||
<div class="flex flex-row-reverse gap-2 h-full">
|
||||
<Button
|
||||
href={`/scripts/run/${script.hash}`}
|
||||
color="blue"
|
||||
size="md"
|
||||
startIcon={{ icon: faPlay }}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
{#if !$userStore?.operator}
|
||||
<Button
|
||||
href={`/scripts/edit/${script.path}?args=${encodeState(args)}${
|
||||
topHash ? `&hash=${script.hash}&topHash=` + topHash : ''
|
||||
}`}
|
||||
color="blue"
|
||||
size="md"
|
||||
startIcon={{ icon: faEdit }}
|
||||
disabled={!can_write}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
{#if !topHash}
|
||||
<Button
|
||||
href={`/scripts/add?template=${script.path}`}
|
||||
variant="border"
|
||||
size="md"
|
||||
startIcon={{ icon: faCodeFork }}
|
||||
>
|
||||
Fork
|
||||
</Button>
|
||||
{#if !emptyString(script.summary)}
|
||||
<span class="text-lg font-semibold">{script.path}</span>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-row gap-x-2 flex-wrap items-center mt-2">
|
||||
<span class="text-sm text-gray-600">
|
||||
Edited {displayDaysAgo(script.created_at || '')} by {script.created_by || 'unknown'}
|
||||
</span>
|
||||
<Badge color="dark-gray">
|
||||
{truncateHash(script?.hash ?? '')}
|
||||
</Badge>
|
||||
{#if script?.is_template}
|
||||
<Badge color="blue">Template</Badge>
|
||||
{/if}
|
||||
{/if}
|
||||
<Button
|
||||
href={`/runs/${script.path}`}
|
||||
size="md"
|
||||
startIcon={{ icon: faList }}
|
||||
color="light"
|
||||
variant="border"
|
||||
>
|
||||
View Runs
|
||||
</Button>
|
||||
</div>
|
||||
<div class="grow truncate">
|
||||
<h1 class="mb-1 truncate">{defaultIfEmptyString(script.summary, script.path)}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !emptyString(script.summary)}
|
||||
<span class="text-lg font-semibold">{script.path}</span>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-row gap-x-2 flex-wrap items-center mt-2">
|
||||
<span class="text-sm text-gray-600">
|
||||
Edited {displayDaysAgo(script.created_at || '')} by {script.created_by || 'unknown'}
|
||||
</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}
|
||||
{#if deploymentInProgress}
|
||||
<Badge color="yellow">
|
||||
<Loader2 size={12} class="inline animate-spin mr-1" />
|
||||
Deployment in progress
|
||||
</Badge>
|
||||
{/if}
|
||||
<SharedBadge canWrite={can_write} extraPerms={script?.extra_perms ?? {}} />
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 flex-wrap mt-4">
|
||||
{#if SCRIPT_VIEW_SHOW_PUBLISH_TO_HUB}
|
||||
<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="light"
|
||||
size="xs"
|
||||
startIcon={{ icon: faGlobe }}
|
||||
>
|
||||
Publish to Hub
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
on:click={() => shareModal.openDrawer(script?.path ?? '', 'script')}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
startIcon={{ icon: faShare }}
|
||||
disabled={!can_write}
|
||||
>
|
||||
Share
|
||||
</Button>
|
||||
{#if SCRIPT_VIEW_SHOW_SCHEDULE}
|
||||
<Button
|
||||
on:click={() => scheduleEditor?.openNew(false, script?.path ?? '')}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
startIcon={{ icon: faCalendar }}
|
||||
>
|
||||
New Schedule
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
on:click={() => moveDrawer.openDrawer(script?.path ?? '', script?.summary, 'script')}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
startIcon={{ icon: faEdit }}
|
||||
>
|
||||
Move/Rename
|
||||
</Button>
|
||||
<Button
|
||||
on:click={() => deploymentDrawer.openDrawer(script?.path ?? '', 'script')}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
startIcon={{ icon: faFileExport }}
|
||||
>
|
||||
Deploy to staging/prod
|
||||
</Button>
|
||||
<Button
|
||||
color="dark"
|
||||
variant="border"
|
||||
size="xs"
|
||||
on:click={() => webhookElem.scrollIntoView()}>Webhooks</Button
|
||||
>
|
||||
{#if Array.isArray(script.parent_hashes) && script.parent_hashes.length > 0}
|
||||
<Button
|
||||
color="dark"
|
||||
variant="contained"
|
||||
size="xs"
|
||||
startIcon={{ icon: faArrowLeft }}
|
||||
href="/scripts/get/{script.parent_hashes[0]}?workspace={$workspaceStore}"
|
||||
dropdownItems={script.parent_hashes.map((hash) => ({
|
||||
href: `/scripts/get/${hash}?workspace=${$workspaceStore}`,
|
||||
label: hash
|
||||
}))}
|
||||
>
|
||||
Previous version ({script.parent_hashes.length})
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if script.lock_error_logs || topHash || script.archived || script.deleted}
|
||||
<div class="flex flex-col gap-2 my-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="mt-2" />
|
||||
<Alert type="warning" title="Not HEAD">
|
||||
This hash is not HEAD (latest non-archived version at this path) :
|
||||
<a href="/scripts/get/{topHash}?workspace={$workspaceStore}"
|
||||
>Go to the HEAD of this path</a
|
||||
>
|
||||
</Alert>
|
||||
{/if}
|
||||
{#if script.archived && !topHash}
|
||||
<Alert type="error" title="Archived">This path was archived</Alert>
|
||||
{/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}
|
||||
|
||||
<div class="mt-6 grid grid-cols-1 sm:grid-cols-3 gap-6">
|
||||
<div class="col-span-2 box">
|
||||
<RunForm
|
||||
loading={runLoading}
|
||||
autofocus
|
||||
detailed={false}
|
||||
bind:isValid
|
||||
runnable={script}
|
||||
runAction={runScript}
|
||||
bind:args
|
||||
viewCliRun
|
||||
isFlow={false}
|
||||
/>
|
||||
</div>
|
||||
{#if !emptyString(script.description)}
|
||||
<div class="box overflow-auto break-words whitespace-pre-wrap">
|
||||
<Urlize text={defaultIfEmptyString(script.description, 'No description')} />
|
||||
{#if script && script.kind !== 'script'}
|
||||
<Badge color="blue">
|
||||
{script?.kind}
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if deploymentInProgress}
|
||||
<Badge color="yellow">
|
||||
<Loader2 size={12} class="inline animate-spin mr-1" />
|
||||
Deployment in progress
|
||||
</Badge>
|
||||
{/if}
|
||||
<SharedBadge canWrite={can_write} extraPerms={script?.extra_perms ?? {}} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
{#if !emptyString(script.description)}
|
||||
<div class="border p-2">
|
||||
<Urlize text={defaultIfEmptyString(script.description, 'No description')} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<RunForm
|
||||
loading={runLoading}
|
||||
autofocus
|
||||
detailed={false}
|
||||
bind:isValid
|
||||
runnable={script}
|
||||
runAction={runScript}
|
||||
bind:args
|
||||
schedulable={true}
|
||||
isFlow={false}
|
||||
bind:this={runForm}
|
||||
/>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="save_inputs">
|
||||
{#if args}
|
||||
<SavedInputs
|
||||
scriptPath={script?.path}
|
||||
scriptHash={topHash}
|
||||
{isValid}
|
||||
{args}
|
||||
on:selected_args={(e) => {
|
||||
runForm?.setArgs(JSON.parse(JSON.stringify(e.detail)))
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="webhooks">
|
||||
<WebhooksPanel scopes={[`run:script/${script?.path}`]} {webhooks} {args} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="schedule">
|
||||
<div class="p-2 flex flex-col">
|
||||
<Button
|
||||
on:click={() => scheduleEditor?.openNew(false, script?.path ?? '')}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
startIcon={{ icon: faCalendar }}
|
||||
>
|
||||
New Schedule
|
||||
</Button>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="details">
|
||||
<div>
|
||||
<Skeleton {loading} layout={[[20]]} />
|
||||
|
||||
<Tabs selected="code">
|
||||
<Tab value="code">Code</Tab>
|
||||
<Tab value="dependencies">Dependencies lock file</Tab>
|
||||
<Tab value="arguments"
|
||||
><span class="inline-flex items-center gap-1">
|
||||
Arguments JSON Schema
|
||||
<Tab value="code" size="xs">Code</Tab>
|
||||
<Tab value="dependencies" size="xs">Lock file</Tab>
|
||||
<Tab value="arguments" size="xs">
|
||||
<span class="inline-flex items-center gap-1">
|
||||
Inputs
|
||||
<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
|
||||
@@ -452,21 +499,25 @@
|
||||
</Tab>
|
||||
<svelte:fragment slot="content">
|
||||
<TabContent value="code">
|
||||
<div class="border rounded-sm mt-2">
|
||||
<HighlightCode language={script.language} code={script.content} />
|
||||
<div class="p-2">
|
||||
<HighlightCode
|
||||
language={script.language}
|
||||
code={script.content}
|
||||
class="whitespace-pre"
|
||||
/>
|
||||
</div>
|
||||
</TabContent>
|
||||
<TabContent value="dependencies">
|
||||
<div class="border rounded-sm mt-2">
|
||||
<div class="">
|
||||
{#if script?.lock}
|
||||
<pre>{script.lock}</pre>
|
||||
<pre class="bg-gray-50 text-sm p-2">{script.lock}</pre>
|
||||
{:else}
|
||||
<p>There is no lock file for this script</p>
|
||||
<p class="bg-gray-50 text-sm p-2"> There is no lock file for this script</p>
|
||||
{/if}
|
||||
</div>
|
||||
</TabContent>
|
||||
<TabContent value="arguments">
|
||||
<div class="max-w-2xl">
|
||||
<div class="p-2">
|
||||
<SchemaViewer schema={script.schema} />
|
||||
</div>
|
||||
</TabContent>
|
||||
@@ -482,155 +533,12 @@
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
<div class="max-w-2xl mt-12">
|
||||
<h3 class="mb-4" bind:this={webhookElem} id="webhooks">
|
||||
Webhooks
|
||||
<Tooltip>
|
||||
{SCRIPT_VIEW_WEBHOOK_INFO_TIP}
|
||||
<a href={SCRIPT_VIEW_WEBHOOK_INFO_LINK} class="text-blue-500"> See docs </a>
|
||||
</Tooltip>
|
||||
</h3>
|
||||
<Skeleton {loading} layout={[[8.5]]} />
|
||||
<Tabs selected={DEFAULT_WEBHOOK_TYPE}>
|
||||
<Tab value="async">UUID/Async</Tab>
|
||||
<Tab value="sync">Result/Sync</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]}
|
||||
{@const href = $page.url.protocol + '//' + url}
|
||||
<li class="flex justify-between items-center mt-2">
|
||||
<a
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
copyToClipboard(href)
|
||||
}}
|
||||
{href}
|
||||
class="whitespace-nowrap text-ellipsis overflow-hidden mr-1"
|
||||
>
|
||||
{url}
|
||||
<span class="text-gray-700 ml-2">
|
||||
<Icon data={faClipboard} />
|
||||
</span>
|
||||
</a>
|
||||
{#if type == 'get_path'}
|
||||
<div class="flex flex-row gap-1">
|
||||
<Badge>GET</Badge>
|
||||
<Tooltip
|
||||
>This webhook unlike the others which are all POST takes in a GET
|
||||
request. The payload must be passed as the query arg `payload` and
|
||||
encoded in JSON first, then in an URL safe base64. e.g:
|
||||
`encodeURIComponent(btoa(JSON.stringify({'{a: 2}'})))` `
|
||||
</Tooltip>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row gap-1">
|
||||
<Badge>POST</Badge>
|
||||
|
||||
<Badge color="dark-gray" capitalize>
|
||||
{type}
|
||||
</Badge>
|
||||
</div>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if SCRIPT_VIEW_SHOW_CREATE_TOKEN_BUTTON}
|
||||
<div class="flex flex-row-reverse mt-2">
|
||||
<Button size="xs" on:click={userSettings.openDrawer}
|
||||
>Create a Webhook-specific Token <Tooltip
|
||||
>The token will have a scope such that it can only be used to trigger this
|
||||
script. It is safe to share as it cannot be used to impersonate you.</Tooltip
|
||||
></Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{#if SCRIPT_VIEW_SHOW_EXAMPLE_CURL}
|
||||
<div class="flex">
|
||||
<Button
|
||||
color="light"
|
||||
size="lg"
|
||||
endIcon={{ icon: viewWebhookCommand ? faChevronUp : faChevronDown }}
|
||||
on:click={() => (viewWebhookCommand = !viewWebhookCommand)}
|
||||
>
|
||||
CURL
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if viewWebhookCommand}
|
||||
{@const command = curlCommand(key == 'async')}
|
||||
<div transition:slide|local class="px-4">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<pre class="bg-gray-700 text-gray-100 p-2 font-mono text-sm whitespace-pre-wrap"
|
||||
>{command} <span
|
||||
on:click={() => copyToClipboard(command)}
|
||||
class="cursor-pointer ml-2"><Icon data={faClipboard} /></span
|
||||
>{#if key == 'async'}<br /><br
|
||||
/>//^ returns an UUID. Fetch until completed == true<br
|
||||
/>curl -H "Authorization: Bearer $TOKEN" {$page.url.protocol}//{$page.url
|
||||
.hostname}/api/w/{$workspaceStore}/jobs_u/completed/get_result_maybe/$UUID{/if}</pre
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</TabContent>
|
||||
{/each}
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="cli">
|
||||
<div class="p-2 flex flex-col gap-4">
|
||||
<InlineCodeCopy content={cliCommand} />
|
||||
<CliHelpBox />
|
||||
</div>
|
||||
<div class="mt-32">
|
||||
{#if can_write}
|
||||
<h3>Danger zone</h3>
|
||||
<div class="flex gap-2">
|
||||
<Popover>
|
||||
<Button
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
script?.hash && deleteScript(script.hash)
|
||||
}}
|
||||
color="red"
|
||||
variant="contained"
|
||||
startIcon={{ icon: faTrash }}
|
||||
disabled={!($superadmin || ($userStore?.is_admin ?? false))}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<span slot="text">require to be admin</span>
|
||||
</Popover>
|
||||
{#if script.archived}
|
||||
<Button
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
script?.hash && unarchiveScript(script.hash)
|
||||
}}
|
||||
color="red"
|
||||
variant="border"
|
||||
startIcon={{ icon: faArchive }}
|
||||
>
|
||||
Unarchive
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
script?.hash && archiveScript(script.hash)
|
||||
}}
|
||||
color="red"
|
||||
variant="border"
|
||||
startIcon={{ icon: faArchive }}
|
||||
>
|
||||
Archive
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</CenteredPage>
|
||||
</svelte:fragment>
|
||||
</DetailPageLayout>
|
||||
{/if}
|
||||
|
||||
<UserSettings bind:this={userSettings} scopes={[`run:script/${script?.path}`]} />
|
||||
|
||||
<ShareModal bind:this={shareModal} />
|
||||
|
||||
Reference in New Issue
Block a user