Files
windmill/frontend/src/lib/components/details/WebhooksPanel.svelte
T

374 lines
11 KiB
Svelte

<script lang="ts">
import { page } from '$app/stores'
import Tooltip from '$lib/components/Tooltip.svelte'
import { userStore, 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, Clipboard } from 'lucide-svelte'
import UserSettings from '../UserSettings.svelte'
import { Highlight } from 'svelte-highlight'
import { typescript } from 'svelte-highlight/languages'
import ClipboardPanel from './ClipboardPanel.svelte'
import { copyToClipboard, generateRandomString } from '$lib/utils'
import HighlightTheme from '../HighlightTheme.svelte'
import { base } from '$lib/base'
let userSettings: UserSettings
export let token: string
export let args: any
export let scopes: string[] = []
export let isFlow: boolean = false
export let hash: string | undefined = undefined
export let path: string
let selectedTab: string = 'rest'
let webhooks: {
async: {
hash?: string
path: string
}
sync: {
hash?: string
path: string
get_path?: string
}
}
$: webhooks = isFlow ? computeFlowWebhooks(path) : computeScriptWebhooks(hash, path)
function computeScriptWebhooks(hash: string | undefined, path: string) {
let webhookBase = `${$page.url.origin}${base}/api/w/${$workspaceStore}/jobs`
return {
async: {
hash: `${webhookBase}/run/h/${hash}`,
path: `${webhookBase}/run/p/${path}`
},
sync: {
hash: `${webhookBase}/run_wait_result/h/${hash}`,
path: `${webhookBase}/run_wait_result/p/${path}`,
get_path: `${webhookBase}/run_wait_result/p/${path}`
}
}
}
function computeFlowWebhooks(path: string) {
let webhooksBase = `${$page.url.origin}${base}/api/w/${$workspaceStore}/jobs`
let urlAsync = `${webhooksBase}/run/f/${path}`
let urlSync = `${webhooksBase}/run_wait_result/f/${path}`
return {
async: {
path: urlAsync
},
sync: {
path: urlSync,
get_path: urlSync
}
}
}
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'
}
$: url =
webhooks[webhookType][requestType] +
(tokenType === 'query'
? `?token=${token}${
requestType === 'get_path'
? `&payload=${encodeURIComponent(btoa(JSON.stringify(args)))}`
: ''
}`
: `${
requestType === 'get_path'
? `?payload=${encodeURIComponent(btoa(JSON.stringify(args)))}`
: ''
}`)
function headers() {
const headers = {}
if (requestType != 'get_path') {
headers['Content-Type'] = 'application/json'
}
if (tokenType === 'headers') {
headers['Authorization'] = `Bearer ${token}`
}
return headers
}
function fetchCode() {
if (webhookType === 'sync') {
return `
export async function main() {
const jobTriggerResponse = await triggerJob();
const data = await jobTriggerResponse.json();
return data;
}
async function triggerJob() {
${
requestType === 'get_path'
? '// Payload is a base64 encoded string of the arguments'
: `const body = JSON.stringify(${JSON.stringify(args, null, 2).replaceAll('\n', '\n\t')});`
}
const endpoint = \`${url}\`;
return await fetch(endpoint, {
method: '${requestType === 'get_path' ? 'GET' : 'POST'}',
headers: ${JSON.stringify(headers(), null, 2).replaceAll('\n', '\n\t\t')}${
requestType === 'get_path' ? '' : `,\n\t\tbody`
}
});
}`
}
// Main function
let mainFunction = `
export async function main() {
const jobTriggerResponse = await triggerJob();
const UUID = await jobTriggerResponse.text();
const jobCompletionData = await waitForJobCompletion(UUID);
return jobCompletionData;
}`
// triggerJob function
let triggerJobFunction = `
async function triggerJob() {
const body = JSON.stringify(${JSON.stringify(args, null, 2).replaceAll('\n', '\n\t')});
const endpoint = \`${url}\`;
return await fetch(endpoint, {
method: '${requestType === 'get_path' ? 'GET' : 'POST'}',
headers: ${JSON.stringify(headers(), null, 2).replaceAll('\n', '\n\t\t')},
body
});
}`
// waitForJobCompletion function
let waitForJobCompletionFunction = `
function waitForJobCompletion(UUID) {
return new Promise(async (resolve, reject) => {
try {
const endpoint = \`${
$page.url.origin
}/api/w/${$workspaceStore}/jobs_u/completed/get_result_maybe/\${UUID}\`;
const checkResponse = await fetch(endpoint, {
method: 'GET',
headers: ${JSON.stringify(headers(), null, 2).replaceAll('\n', '\n\t\t\t\t')}
});
const checkData = await checkResponse.json();
if (checkData.completed) {
resolve(checkData);
} else {
// If not completed, wait for a second then try again
setTimeout(async () => {
const result = await waitForJobCompletion(UUID);
resolve(result);
}, 1000);
}
} catch (error) {
reject(error);
}
});
}`
// Combine and return
return `${mainFunction}\n\n${triggerJobFunction}\n\n${waitForJobCompletionFunction}`
}
function curlCode() {
return `TOKEN='${token}'
${requestType !== 'get_path' ? `BODY='${JSON.stringify(args)}'` : ''}
URL='${url}'
${webhookType === 'sync' ? 'RESULT' : 'UUID'}=$(curl -s ${
requestType != 'get_path' ? "-H 'Content-Type: application/json'" : ''
} ${tokenType === 'headers' ? `-H "Authorization: Bearer $TOKEN"` : ''} -X ${
requestType === 'get_path' ? 'GET' : 'POST'
} ${requestType !== 'get_path' ? `-d "$BODY" ` : ''}$URL)
${
webhookType === 'sync'
? 'echo -E $RESULT | jq'
: `
URL="${$page.url.origin}/api/w/${$workspaceStore}/jobs_u/completed/get_result_maybe/$UUID"
while true; do
curl -s -H "Authorization: Bearer $TOKEN" $URL -o res.json
COMPLETED=$(cat res.json | jq .completed)
if [ "$COMPLETED" = "true" ]; then
cat res.json | jq .result
break
else
sleep 1
fi
done`
}`
}
</script>
<HighlightTheme />
<UserSettings
bind:this={userSettings}
on:tokenCreated={(e) => {
token = e.detail
}}
newTokenLabel={`webhook-${$userStore?.username ?? 'superadmin'}-${generateRandomString(4)}`}
{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="paste your token here once created to alter examples below"
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="The returning value is the uuid of the job assigned to execute the job."
/>
<ToggleButton
label="Sync"
value="sync"
tooltip="Triggers the execution, wait for the job to complete and return it as a response."
/>
</ToggleButtonGroup>
</div>
<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"
/>
{#if !isFlow}
<ToggleButton
label="POST by hash"
value="hash"
icon={ArrowUpRight}
selectedColor="#fb923c"
/>
{/if}
<ToggleButton
label="GET by path"
value="get_path"
icon={ArrowDownRight}
disabled={webhookType !== 'sync'}
selectedColor="#14b8a6"
/>
</ToggleButtonGroup>
</div>
<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 -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<Tabs bind:selected={selectedTab}>
<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">
{#key token}
<TabContent value="rest" class="flex flex-col flex-1 h-full ">
<div class="flex flex-col gap-2">
<ClipboardPanel title="Url" content={url} />
{#if requestType !== 'get_path'}
<ClipboardPanel title="Body" content={JSON.stringify(args, null, 2)} />
{/if}
{#key requestType}
{#key tokenType}
<ClipboardPanel title="Headers" content={JSON.stringify(headers(), null, 2)} />
{/key}
{/key}
</div>
</TabContent>
<TabContent value="curl" class="flex flex-col flex-1 h-full">
<div class="relative">
{#key args}
{#key requestType}
{#key webhookType}
{#key tokenType}
<div
class="flex flex-row flex-1 h-full border p-2 rounded-md overflow-auto relative"
on:click={(e) => {
e.preventDefault()
copyToClipboard(curlCode())
}}
>
<Highlight language={bash} code={curlCode()} />
<Clipboard size={14} class="w-8 top-2 right-2 absolute" />
</div>
{/key}
{/key}
{/key}
{/key}
</div>
</TabContent>
<TabContent value="fetch">
{#key args}
{#key requestType}
{#key webhookType}
{#key tokenType}
{#key token}
<div
class="flex flex-row flex-1 h-full border p-2 rounded-md overflow-auto relative"
on:click={(e) => {
e.preventDefault()
copyToClipboard(fetchCode())
}}
>
<Highlight language={typescript} code={fetchCode()} />
<Clipboard size={14} class="w-8 top-2 right-2 absolute" />
</div>
{/key}{/key}{/key}{/key}
{/key}
</TabContent>
{/key}
</svelte:fragment>
</Tabs>
</div>