mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 00:04:10 +00:00
Glm/rework flow settings v2 (#4497)
* fat(frontend): simplify flow settings menu * improve scroll * changing mute toggle * Add advanced settings badge * Add nord theme colors * Add bage for advanced options * fix minor issue * fix minor issue * Add triggers menu to flow settings * Add quick trigger access * remove triggers in flow settings * fix minor issue * Move triggers settings to flow right panel * polishing * fix unset store * remove save up to for triggers * fix padding * reset default tag color * remove custom select component * revert path change * revert section modif * Revert unused feature --------- Co-authored-by: Guilhem <guilhem@mbp-de-windmill.home>
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
import AssignableTagsInner from './AssignableTagsInner.svelte'
|
||||
|
||||
export let placement: 'bottom-end' | 'top-end' = 'bottom-end'
|
||||
export let color: 'nord' | 'dark' = 'dark'
|
||||
</script>
|
||||
|
||||
<Popup
|
||||
@@ -13,7 +14,7 @@
|
||||
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<Button color="dark" size="xs" nonCaptureEvent={true}>
|
||||
<Button {color} size="xs" nonCaptureEvent={true}>
|
||||
<div class="flex flex-row gap-1 items-center"
|
||||
><Pen size={14} /> Custom Tags <Tooltip light
|
||||
>Tags are assigned to scripts and flows. Workers only accept jobs that correspond to their
|
||||
|
||||
@@ -481,9 +481,11 @@
|
||||
|
||||
const testStepStore = writable<Record<string, any>>({})
|
||||
const selectedIdStore = writable('settings-metadata')
|
||||
const selectedTriggerStore = writable('webhooks')
|
||||
|
||||
setContext<FlowEditorContext>('FlowEditorContext', {
|
||||
selectedId: selectedIdStore,
|
||||
selectedTrigger: selectedTriggerStore,
|
||||
schedule: scheduleStore,
|
||||
previewArgs: previewArgsStore,
|
||||
scriptEditorDrawer,
|
||||
|
||||
@@ -368,6 +368,7 @@
|
||||
}
|
||||
|
||||
const selectedIdStore = writable<string>(selectedId ?? 'settings-metadata')
|
||||
const selectedTriggerStore = writable('webhooks')
|
||||
|
||||
export function getSelectedId() {
|
||||
return $selectedIdStore
|
||||
@@ -396,6 +397,7 @@
|
||||
|
||||
setContext<FlowEditorContext>('FlowEditorContext', {
|
||||
selectedId: selectedIdStore,
|
||||
selectedTrigger: selectedTriggerStore,
|
||||
schedule: scheduleStore,
|
||||
previewArgs: previewArgsStore,
|
||||
scriptEditorDrawer,
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<script lang="ts">
|
||||
import { Badge, Button } from './common'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { base } from '$lib/base'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import { ListOrdered, PenBox } from 'lucide-svelte'
|
||||
import JobArgs from './JobArgs.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import ScheduleEditor from './ScheduleEditor.svelte'
|
||||
import { type Schedule } from '$lib/gen'
|
||||
|
||||
export let light = false
|
||||
export let schedule: Schedule
|
||||
export let can_write: boolean
|
||||
export let path: string
|
||||
export let isFlow: boolean
|
||||
export let scheduleEditor: ScheduleEditor
|
||||
export let setScheduleEnabled: (path: string, enabled: boolean) => void
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2 grow w-full">
|
||||
<div class="grid grid-cols-3 w-full">
|
||||
<div class="flex justify-start">
|
||||
<Badge color="indigo" small>
|
||||
Primary
|
||||
<Tooltip light>
|
||||
Share the same path as the script or flow it is attached to and its path get renamed
|
||||
whenever the source path is renamed
|
||||
</Tooltip>
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<input
|
||||
size="9"
|
||||
class={light ? '!text-xs !h-6 !text-primary' : 'text-sm bg-red-400'}
|
||||
type="text"
|
||||
id="cron-schedule"
|
||||
name="cron-schedule"
|
||||
placeholder="*/30 * * * *"
|
||||
value={schedule.schedule}
|
||||
disabled={true}
|
||||
/>
|
||||
<Toggle
|
||||
checked={schedule.enabled}
|
||||
on:change={(e) => {
|
||||
if (can_write) {
|
||||
setScheduleEnabled(path, e.detail)
|
||||
} else {
|
||||
sendUserToast('not enough permission', true)
|
||||
}
|
||||
}}
|
||||
options={light
|
||||
? undefined
|
||||
: {
|
||||
right: 'On'
|
||||
}}
|
||||
size="xs"
|
||||
textClass="text-primary font-normal text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
{#if !light}
|
||||
<Button
|
||||
size={light ? 'xs2' : 'xs'}
|
||||
variant="border"
|
||||
color="light"
|
||||
href={`${base}/runs/${path}`}
|
||||
spacingSize={light ? 'xs3' : undefined}
|
||||
>
|
||||
<span class={light ? 'text-2xs px-1 font-normal' : ''}>Runs</span>
|
||||
<ListOrdered size={light ? 12 : 14} />
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
size={light ? 'xs2' : 'xs'}
|
||||
color="dark"
|
||||
on:click={() => scheduleEditor?.openEdit(path ?? '', isFlow)}
|
||||
>
|
||||
<PenBox size={light ? 12 : 14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !light}
|
||||
{#if Object.keys(schedule?.args ?? {}).length > 0}
|
||||
<div class="">
|
||||
<JobArgs args={schedule.args ?? {}} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-xs text-tertiary"> No arguments </div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,16 +1,13 @@
|
||||
<script lang="ts">
|
||||
import ScheduleEditor from './ScheduleEditor.svelte'
|
||||
import { Badge, Button } from './common'
|
||||
import { Button } from './common'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { ScheduleService, type Schedule } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { base } from '$lib/base'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import { ListOrdered, Calendar, PenBox } from 'lucide-svelte'
|
||||
import JobArgs from './JobArgs.svelte'
|
||||
import { Calendar } from 'lucide-svelte'
|
||||
import Skeleton from './common/skeleton/Skeleton.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
|
||||
import PrimarySchedule from './PrimarySchedule.svelte'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
export let isFlow: boolean
|
||||
export let path: string
|
||||
export let can_write: boolean
|
||||
@@ -81,7 +78,7 @@
|
||||
bind:this={scheduleEditor}
|
||||
/>
|
||||
|
||||
<div class="p-2 flex flex-col">
|
||||
<div class="flex flex-col gap-4">
|
||||
<Button
|
||||
on:click={() => scheduleEditor?.openNew(isFlow, path)}
|
||||
variant="border"
|
||||
@@ -91,88 +88,38 @@
|
||||
>
|
||||
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 items-center">
|
||||
<input
|
||||
class="inline-block !w-32"
|
||||
type="text"
|
||||
id="cron-schedule"
|
||||
name="cron-schedule"
|
||||
placeholder="*/30 * * * *"
|
||||
value={schedule.schedule}
|
||||
disabled={true}
|
||||
/>
|
||||
<div>
|
||||
<Badge color="indigo" small
|
||||
>Primary <Tooltip light
|
||||
>Share the same path as the script or flow it is attached to and its path get renamed
|
||||
whenever the source path is renamed</Tooltip
|
||||
></Badge
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<Toggle
|
||||
checked={schedule.enabled}
|
||||
on:change={(e) => {
|
||||
if (can_write) {
|
||||
setScheduleEnabled(path, e.detail)
|
||||
} else {
|
||||
sendUserToast('not enough permission', true)
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'On'
|
||||
}}
|
||||
size="xs"
|
||||
/>
|
||||
<Button size="xs" variant="border" color="light" href={`${base}/runs/${path}`}>
|
||||
<div class="flex flex-row gap-2">
|
||||
<ListOrdered size={14} />
|
||||
Runs
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
on:click={() => scheduleEditor?.openEdit(path ?? '', isFlow)}
|
||||
>
|
||||
<PenBox size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{#if Object.keys(schedule?.args ?? {}).length > 0}
|
||||
<div class="">
|
||||
<JobArgs args={schedule.args ?? {}} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-xs texg-gray-700"> No arguments </div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if schedule == undefined}
|
||||
<Skeleton layout={[[6]]} />
|
||||
{/if}
|
||||
|
||||
<h3 class="px-2 pt-4">Other schedules</h3>
|
||||
|
||||
{#if schedules}
|
||||
{#if schedules.length == 0}
|
||||
<div class="text-xs text-secondary px-2"> No other schedules </div>
|
||||
{:else}
|
||||
<div class="flex flex-col divide-y px-2 pt-2">
|
||||
{#each schedules as schedule (schedule.path)}
|
||||
<div class="grid grid-cols-6 text-2xs items-center py-2"
|
||||
><div class="col-span-3 truncate">{schedule.path}</div><div>{schedule.schedule}</div>
|
||||
<div>{schedule.enabled ? 'on' : 'off'}</div>
|
||||
<button on:click={() => scheduleEditor?.openEdit(schedule.path, isFlow)}>Edit</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if schedule}
|
||||
<PrimarySchedule {schedule} {can_write} {path} {isFlow} {scheduleEditor} {setScheduleEnabled} />
|
||||
{:else if schedule == undefined}
|
||||
<Skeleton layout={[[6]]} />
|
||||
{/if}
|
||||
{:else}
|
||||
<Skeleton layout={[[8]]} />
|
||||
{/if}
|
||||
|
||||
<Label label="Other schedules">
|
||||
{#if schedules}
|
||||
{#if schedules.length == 0}
|
||||
<div class="text-xs text-tertiary"> No other schedules </div>
|
||||
{:else}
|
||||
<div class="flex flex-col divide-y">
|
||||
{#each schedules as schedule (schedule.path)}
|
||||
<div class="grid grid-cols-6 text-xs items-center">
|
||||
<div class="col-span-3 truncate">{schedule.path}</div>
|
||||
<div class="col-span-2 flex flex-row gap-4 flex-nowrap">
|
||||
<div>{schedule.schedule}</div>
|
||||
<div>{schedule.enabled ? 'on' : 'off'}</div>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
on:click={() => scheduleEditor?.openEdit(schedule.path, isFlow)}
|
||||
class="px-2">Edit</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Skeleton layout={[[8]]} />
|
||||
{/if}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
@@ -28,11 +28,9 @@
|
||||
{:else}
|
||||
<ChevronDown size={16} />
|
||||
{/if}
|
||||
{label}
|
||||
</button>
|
||||
{:else}
|
||||
{label}
|
||||
{/if}
|
||||
{label}
|
||||
|
||||
<slot name="header" />
|
||||
{#if tooltip}
|
||||
@@ -48,6 +46,9 @@
|
||||
{/if}
|
||||
</h2>
|
||||
<slot name="action" />
|
||||
{#if collapsable && collapsed}
|
||||
<slot name="badge" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class={collapsable && collapsed ? `hidden ${$$props.class}` : `${$$props.class}`}>
|
||||
<slot />
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
export let disabled = false
|
||||
export let textClass = ''
|
||||
export let textStyle = ''
|
||||
export let color: 'blue' | 'red' = 'blue'
|
||||
export let color: 'blue' | 'red' | 'nord' = 'blue'
|
||||
export let id = (Math.random() + 1).toString(36).substring(10)
|
||||
export let lightMode: boolean = false
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const bothOptions = Boolean(options.left) && Boolean(options.right)
|
||||
|
||||
export let textDisabled = false
|
||||
</script>
|
||||
|
||||
<label
|
||||
@@ -34,7 +36,7 @@
|
||||
<span
|
||||
class={twMerge(
|
||||
'mr-2 font-medium duration-50 select-none',
|
||||
bothOptions ? (checked ? 'text-disabled' : 'text-primary') : 'text-primary',
|
||||
bothOptions || textDisabled ? (checked ? 'text-disabled' : 'text-primary') : 'text-primary',
|
||||
size === 'xs' ? 'text-xs' : 'text-sm',
|
||||
textClass
|
||||
)}
|
||||
@@ -67,7 +69,9 @@
|
||||
"transition-all bg-surface-selected rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:bg-surface after:border-white after:border after:rounded-full after:transition-all ",
|
||||
color == 'red'
|
||||
? 'peer-checked:bg-red-600'
|
||||
: 'peer-checked:bg-blue-600 dark:peer-checked:bg-blue-500',
|
||||
: color == 'blue'
|
||||
? 'peer-checked:bg-blue-600 dark:peer-checked:bg-blue-500'
|
||||
: 'peer-checked:bg-nord-300 dark:peer-checked:bg-nord-500',
|
||||
size === 'sm'
|
||||
? 'w-11 h-6 after:top-0.5 after:left-[2px] after:h-5 after:w-5'
|
||||
: 'w-7 h-4 after:top-0.5 after:left-[2px] after:h-3 after:w-3'
|
||||
@@ -78,7 +82,7 @@
|
||||
<span
|
||||
class={twMerge(
|
||||
'ml-2 font-medium duration-50 select-none',
|
||||
bothOptions ? (checked ? 'text-primary' : 'text-disabled') : 'text-primary',
|
||||
bothOptions || textDisabled ? (checked ? 'text-primary' : 'text-disabled') : 'text-primary',
|
||||
size === 'xs' ? 'text-xs' : 'text-sm',
|
||||
textClass
|
||||
)}
|
||||
|
||||
@@ -71,5 +71,5 @@
|
||||
}}
|
||||
startIcon={{ icon: RotateCw }}
|
||||
/>
|
||||
<AssignableTags placement={popupPlacement} />
|
||||
<AssignableTags placement={popupPlacement} color="nord" />
|
||||
</div>
|
||||
|
||||
@@ -114,6 +114,13 @@
|
||||
contained:
|
||||
'bg-surface border-transparent hover:bg-surface-hover focus-visible:bg-surface-hover text-primary focus-visible:ring-surface-selected',
|
||||
divider: 'divide-x divide-gray-200 dark:divide-gray-700'
|
||||
},
|
||||
nord: {
|
||||
border:
|
||||
'border-nord-200 bg-surface hover:bg-surface-hover focus-visible:bg-surface-hover text-primary hover:text-secondary focus-visible:text-secondary focus-visible:ring-surface-selected-inverse dark:border-nord-200',
|
||||
contained:
|
||||
'bg-nord-300 hover:bg-nord-0 focus-visible:bg-surface-hover-inverse text-primary-inverse focus-visible:ring-surface-selected-inverse dark:bg-nord-400 dark:hover:bg-nord-600 dark:text-primary-inverse',
|
||||
divider: 'divide-x divide-gray-800 dark:divide-gray-200'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
<button
|
||||
class={twMerge(
|
||||
'border-b-2 py-1 px-2 cursor-pointer transition-all z-10 ease-linear font-normal text-primary',
|
||||
'border-b-2 py-1 px-2 cursor-pointer transition-all z-10 ease-linear font-normal text-tertiary',
|
||||
isSelected
|
||||
? 'wm-tab-active font-main'
|
||||
: 'border-gray-300 dark:border-gray-600 border-opacity-0 hover:border-opacity-100 ',
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="px-2 py-1 border flex items-center cursor-pointer bg-surface-secondary text-primary justify-between rounded-md"
|
||||
class="grow min-w-0 w-full px-2 py-1 border flex items-center cursor-pointer bg-surface-secondary text-primary justify-between rounded-md"
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
copyToClipboard(content)
|
||||
}}
|
||||
>
|
||||
<div class="text-xs truncate whitespace-pre-wrap w-11/12">{content}</div>
|
||||
<Clipboard size={14} class="w-1/12" />
|
||||
<div class="text-xs truncate whitespace-no-wrap grow">{content}</div>
|
||||
<Clipboard size={12} class="flex-shrink-0" />
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import { AlertTriangle } from 'lucide-svelte'
|
||||
import Skeleton from '../common/skeleton/Skeleton.svelte'
|
||||
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
let userSettings: UserSettings
|
||||
|
||||
export let token: string
|
||||
@@ -49,6 +49,10 @@
|
||||
.toLowerCase()
|
||||
return `${pathOrHash}+${encodedPrefix}@${emailDomain}`
|
||||
}
|
||||
|
||||
export let email: string = ''
|
||||
|
||||
$: email = emailAddress()
|
||||
</script>
|
||||
|
||||
<HighlightTheme />
|
||||
@@ -62,13 +66,13 @@
|
||||
{scopes}
|
||||
/>
|
||||
|
||||
<div class="p-2 flex flex-col w-full gap-4">
|
||||
<div class="flex flex-col w-full gap-4">
|
||||
{#if loading}
|
||||
<Skeleton layout={[[18]]} />
|
||||
{:else}
|
||||
{#if emailDomain}
|
||||
{#if SCRIPT_VIEW_SHOW_CREATE_TOKEN_BUTTON}
|
||||
<div class="my-2">
|
||||
<Label label="Token">
|
||||
<div class="flex flex-row justify-between gap-2">
|
||||
<input
|
||||
bind:value={token}
|
||||
@@ -89,7 +93,7 @@
|
||||
Create/input a valid token before copying the email address below
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Label>
|
||||
{/if}
|
||||
|
||||
{#if !isFlow}
|
||||
@@ -103,20 +107,19 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-col gap-4">
|
||||
{#key requestType}
|
||||
{#key token}
|
||||
<div class="flex flex-col gap-2">
|
||||
<ClipboardPanel title="Email address" content={emailAddress()} />
|
||||
</div>
|
||||
{/key}
|
||||
|
||||
{#key requestType}
|
||||
{#key token}
|
||||
<Label label="Email address">
|
||||
<ClipboardPanel content={email} />
|
||||
</Label>
|
||||
{/key}
|
||||
<Alert title="Email trigger" size="xs">
|
||||
To trigger the job by email, send an email to the address above. The job will receive two
|
||||
arguments: `raw_email` containing the raw email as string, and `parsed_email` containing
|
||||
the parsed email as an object.
|
||||
</Alert>
|
||||
</div>
|
||||
{/key}
|
||||
<Alert title="Email trigger" size="xs">
|
||||
To trigger the job by email, send an email to the address above. The job will receive two
|
||||
arguments: `raw_email` containing the raw email as string, and `parsed_email` containing the
|
||||
parsed email as an object.
|
||||
</Alert>
|
||||
{:else}
|
||||
<div>
|
||||
<Alert title="Email triggers are disabled" size="xs" type="warning">
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<script lang="ts">
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
|
||||
import { FlowService, ScriptService } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
export let kind: 'script' | 'flow'
|
||||
export let scriptOrFlowPath: string
|
||||
export let errorHandlerMuted: boolean | undefined
|
||||
export let textDisabled = false
|
||||
let toggleState = errorHandlerMuted
|
||||
|
||||
export let color: 'nord' | 'red' | 'blue' = 'nord'
|
||||
|
||||
async function toggleErrorHandler(): Promise<void> {
|
||||
toggleState = !toggleState
|
||||
if ($workspaceStore !== undefined) {
|
||||
try {
|
||||
if (kind === 'flow') {
|
||||
await FlowService.toggleWorkspaceErrorHandlerForFlow({
|
||||
workspace: $workspaceStore,
|
||||
path: scriptOrFlowPath,
|
||||
requestBody: {
|
||||
muted: !errorHandlerMuted
|
||||
}
|
||||
})
|
||||
} else {
|
||||
await ScriptService.toggleWorkspaceErrorHandlerForScript({
|
||||
workspace: $workspaceStore,
|
||||
path: scriptOrFlowPath,
|
||||
requestBody: {
|
||||
muted: !errorHandlerMuted
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
sendUserToast(
|
||||
`Error while toggling Workspace Error Handler: ${error.body || error.message}`,
|
||||
true
|
||||
)
|
||||
toggleState = false
|
||||
return
|
||||
}
|
||||
errorHandlerMuted = !errorHandlerMuted
|
||||
toggleState = errorHandlerMuted
|
||||
sendUserToast(
|
||||
errorHandlerMuted ? 'Workspace error handler muted' : 'Workspace error handler active',
|
||||
false
|
||||
)
|
||||
}
|
||||
toggleState = false
|
||||
}
|
||||
|
||||
$: console.log('toggleState', toggleState)
|
||||
</script>
|
||||
|
||||
<Toggle
|
||||
{color}
|
||||
textClass="font-normal text-sm"
|
||||
size="xs"
|
||||
checked={toggleState}
|
||||
on:change={toggleErrorHandler}
|
||||
options={{
|
||||
right: 'Mute',
|
||||
rightTooltip: 'Disable workspace error handler, EE only'
|
||||
}}
|
||||
{textDisabled}
|
||||
/>
|
||||
@@ -19,6 +19,7 @@
|
||||
import { copyToClipboard, generateRandomString } from '$lib/utils'
|
||||
import HighlightTheme from '../HighlightTheme.svelte'
|
||||
import { base } from '$lib/base'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
let userSettings: UserSettings
|
||||
|
||||
export let token: string
|
||||
@@ -27,6 +28,7 @@
|
||||
export let isFlow: boolean = false
|
||||
export let hash: string | undefined = undefined
|
||||
export let path: string
|
||||
export let url: string = ''
|
||||
|
||||
let selectedTab: string = 'rest'
|
||||
|
||||
@@ -232,27 +234,29 @@ done`
|
||||
{scopes}
|
||||
/>
|
||||
|
||||
<div class="p-2 flex flex-col w-full gap-4">
|
||||
<div class="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>
|
||||
<Label label="Token">
|
||||
<div class="flex flex-row justify-between gap-2">
|
||||
<input
|
||||
bind:value={token}
|
||||
placeholder="paste your token here once created to alter examples below"
|
||||
class="!text-xs !font-normal"
|
||||
/>
|
||||
<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>
|
||||
</Label>
|
||||
{/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>
|
||||
<div class="text-sm font-normal text-secondary flex flex-row items-center">Request type</div>
|
||||
<ToggleButtonGroup class="h-[30px] w-auto" bind:selected={webhookType}>
|
||||
<ToggleButton
|
||||
label="Async"
|
||||
@@ -267,7 +271,7 @@ done`
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="text-xs font-semibold flex flex-row items-center">Call method</div>
|
||||
<div class="text-sm font-normal text-secondary flex flex-row items-center">Call method</div>
|
||||
<ToggleButtonGroup class="h-[30px] w-auto" bind:selected={requestType}>
|
||||
<ToggleButton
|
||||
label="POST by path"
|
||||
@@ -294,7 +298,9 @@ done`
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="text-xs font-semibold flex flex-row items-center">Token configuration</div>
|
||||
<div class="text-sm font-normal text-secondary 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" />
|
||||
@@ -314,14 +320,20 @@ done`
|
||||
{#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} />
|
||||
<Label label="Url">
|
||||
<ClipboardPanel content={url} />
|
||||
</Label>
|
||||
|
||||
{#if requestType !== 'get_path'}
|
||||
<ClipboardPanel title="Body" content={JSON.stringify(args, null, 2)} />
|
||||
<Label label="Body">
|
||||
<ClipboardPanel content={JSON.stringify(args, null, 2)} />
|
||||
</Label>
|
||||
{/if}
|
||||
{#key requestType}
|
||||
{#key tokenType}
|
||||
<ClipboardPanel title="Headers" content={JSON.stringify(headers(), null, 2)} />
|
||||
<Label label="Headers">
|
||||
<ClipboardPanel content={JSON.stringify(headers(), null, 2)} />
|
||||
</Label>
|
||||
{/key}
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<FlowEditorPanel enableAi={!disableAi} />
|
||||
<FlowEditorPanel enableAi={!disableAi} {newFlow} />
|
||||
{/if}
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import FlowInput from './FlowInput.svelte'
|
||||
import FlowFailureModule from './FlowFailureModule.svelte'
|
||||
import FlowConstants from './FlowConstants.svelte'
|
||||
import FlowTriggers from './FlowTriggers.svelte'
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import { initFlowStepWarnings } from '../utils'
|
||||
import { dfs } from '../dfs'
|
||||
@@ -14,7 +15,7 @@
|
||||
|
||||
export let noEditor = false
|
||||
export let enableAi = false
|
||||
|
||||
export let newFlow = false
|
||||
const { selectedId, flowStore, flowStateStore, flowInputsStore } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
@@ -66,6 +67,8 @@
|
||||
<FlowFailureModule {noEditor} />
|
||||
{:else if $selectedId === 'preprocessor'}
|
||||
<FlowPreprocessorModule {noEditor} />
|
||||
{:else if $selectedId === 'triggers'}
|
||||
<FlowTriggers {noEditor} {newFlow} />
|
||||
{:else}
|
||||
{@const dup = checkDup($flowStore.value.modules)}
|
||||
{#if dup}
|
||||
|
||||
@@ -1,348 +1,273 @@
|
||||
<script lang="ts">
|
||||
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
|
||||
import Tab from '$lib/components/common/tabs/Tab.svelte'
|
||||
import TabContent from '$lib/components/common/tabs/TabContent.svelte'
|
||||
|
||||
import { BROWSER } from 'esm-env'
|
||||
import { base } from '$lib/base'
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import FlowCard from '../common/FlowCard.svelte'
|
||||
import FlowSchedules from './FlowSchedules.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { Alert, Button, SecondsInput } from '$lib/components/common'
|
||||
import { getContext } from 'svelte'
|
||||
import { getContext, beforeUpdate, afterUpdate } from 'svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import Slider from '$lib/components/Slider.svelte'
|
||||
import { enterpriseLicense, workspaceStore } from '$lib/stores'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { copyToClipboard } from '$lib/utils'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { AlertTriangle, Clipboard } from 'lucide-svelte'
|
||||
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
|
||||
import { schemaToObject } from '$lib/schema'
|
||||
import type { Schema } from '$lib/common'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import ErrorHandlerToggleButton from '$lib/components/details/ErrorHandlerToggleButton.svelte'
|
||||
import ErrorHandlerToggleButtonV2 from '$lib/components/details/ErrorHandlerToggleButtonV2.svelte'
|
||||
import WorkerTagPicker from '$lib/components/WorkerTagPicker.svelte'
|
||||
import MetadataGen from '$lib/components/copilot/MetadataGen.svelte'
|
||||
import Badge from '$lib/components/Badge.svelte'
|
||||
|
||||
export let noEditor: boolean
|
||||
|
||||
const { selectedId, flowStore, initialPath, previewArgs, pathStore, schedule, customUi } =
|
||||
const { flowStore, initialPath, previewArgs, pathStore, customUi } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
let hostname = BROWSER ? window.location.protocol + '//' + window.location.host : 'SSR'
|
||||
$: url = `${hostname}/api/w/${$workspaceStore}/jobs/run/f/${$pathStore}`
|
||||
$: syncedUrl = `${hostname}/api/w/${$workspaceStore}/jobs/run_wait_result/f/${$pathStore}`
|
||||
|
||||
function asSchema(x: any) {
|
||||
return x as Schema
|
||||
}
|
||||
let path: Path | undefined = undefined
|
||||
let dirtyPath = false
|
||||
|
||||
let autoscroll = false
|
||||
|
||||
let scrollableDiv: HTMLDivElement | undefined = undefined
|
||||
|
||||
let displayWorkerTagPicker = false
|
||||
$: $flowStore.tag ? (displayWorkerTagPicker = true) : null
|
||||
|
||||
$: activeAdvancedOptions = [
|
||||
{
|
||||
name: 'High Priority',
|
||||
active: $flowStore.value.priority !== undefined && $flowStore.value.priority > 0
|
||||
},
|
||||
{ name: 'Error Handler Muted', active: Boolean($flowStore.ws_error_handler_muted) },
|
||||
{ name: 'Invisible to Others', active: Boolean($flowStore.visible_to_runner_only) },
|
||||
{ name: 'Shared Directory', active: Boolean($flowStore.value.same_worker) },
|
||||
{ name: 'Cache Results', active: Boolean($flowStore.value.cache_ttl) },
|
||||
{ name: 'Early Stop', active: Boolean($flowStore.value.skip_expr) },
|
||||
{ name: 'Early Return', active: Boolean($flowStore.value.early_return) },
|
||||
{ name: 'Dedicated Worker', active: Boolean($flowStore.dedicated_worker) },
|
||||
{ name: 'Concurrent Limit', active: Boolean($flowStore.value.concurrent_limit) },
|
||||
{ name: 'Worker Tag', active: displayWorkerTagPicker }
|
||||
]
|
||||
|
||||
$: numberOfAdvancedOptionsOn = activeAdvancedOptions.filter((option) => option.active).length
|
||||
$: activeAdvancedOptionNames = activeAdvancedOptions
|
||||
.filter((option) => option.active)
|
||||
.map((option) => option.name)
|
||||
|
||||
beforeUpdate(() => {
|
||||
if (scrollableDiv) {
|
||||
const scrollableDistance = scrollableDiv.scrollHeight - scrollableDiv.offsetHeight
|
||||
autoscroll = scrollableDiv.scrollTop > scrollableDistance - 20
|
||||
console.log('autoscroll', autoscroll)
|
||||
}
|
||||
})
|
||||
|
||||
afterUpdate(() => {
|
||||
if (autoscroll && scrollableDiv) {
|
||||
scrollableDiv.scrollTo(0, scrollableDiv.scrollHeight)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="h-full overflow-hidden">
|
||||
<div class="h-full overflow-y-auto flex flex-col" bind:this={scrollableDiv}>
|
||||
<FlowCard {noEditor} title="Settings">
|
||||
<div class="h-full flex-1">
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<Tabs bind:selected={$selectedId}>
|
||||
<Tab value="settings-metadata">Metadata</Tab>
|
||||
{#if !noEditor && customUi?.settingsTabs?.schedule != false}
|
||||
<Tab value="settings-schedule" active={$schedule.enabled}>Schedule</Tab>
|
||||
{/if}
|
||||
{#if customUi?.settingsTabs?.sharedDiretory != false}
|
||||
<Tab value="settings-same-worker" active={$flowStore.value.same_worker}>
|
||||
Shared Directory
|
||||
</Tab>
|
||||
{/if}
|
||||
{#if customUi?.settingsTabs?.earlyStop != false}
|
||||
<Tab value="settings-early-stop" active={Boolean($flowStore.value.skip_expr)}>
|
||||
Early Stop
|
||||
</Tab>
|
||||
{/if}
|
||||
{#if customUi?.settingsTabs?.earlyReturn != false}
|
||||
<Tab value="settings-early-return" active={Boolean($flowStore.value.early_return)}>
|
||||
Early Return
|
||||
</Tab>
|
||||
{/if}
|
||||
{#if customUi?.settingsTabs?.workerGroup != false}
|
||||
<Tab value="settings-worker-group">Worker Group</Tab>
|
||||
{/if}
|
||||
{#if customUi?.settingsTabs?.concurrency != false}
|
||||
<Tab value="settings-concurrency">Concurrency</Tab>
|
||||
{/if}
|
||||
{#if customUi?.settingsTabs?.cache != false}
|
||||
<Tab value="settings-cache" active={Boolean($flowStore.value.cache_ttl)}>Cache</Tab>
|
||||
{/if}
|
||||
<svelte:fragment slot="content">
|
||||
<TabContent value="settings-metadata" class="p-4 h-full overflow-auto">
|
||||
<Section label="Metadata">
|
||||
<div class="h-full gap-8 flex flex-col">
|
||||
<Label label="Summary">
|
||||
<MetadataGen
|
||||
bind:content={$flowStore.summary}
|
||||
promptConfigName="flowSummary"
|
||||
flow={$flowStore.value}
|
||||
on:change={() => {
|
||||
if (initialPath == '' && $flowStore.summary?.length > 0 && !dirtyPath) {
|
||||
path?.setName(
|
||||
$flowStore.summary
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_]/g, '_')
|
||||
.replace(/-+/g, '_')
|
||||
.replace(/^-|-$/g, '')
|
||||
)
|
||||
}
|
||||
}}
|
||||
elementProps={{
|
||||
type: 'text',
|
||||
id: 'flow-summary',
|
||||
placeholder: 'Short summary to be displayed when listed'
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
<div class="grow min-h-0 p-4 h-full flex flex-col gap-8">
|
||||
<!-- Metadata Section -->
|
||||
<Section label="Metadata" small={true}>
|
||||
<div class="h-full gap-8 flex flex-col">
|
||||
<Label label="Summary">
|
||||
<MetadataGen
|
||||
bind:content={$flowStore.summary}
|
||||
promptConfigName="flowSummary"
|
||||
flow={$flowStore.value}
|
||||
on:change={() => {
|
||||
if (initialPath == '' && $flowStore.summary?.length > 0 && !dirtyPath) {
|
||||
path?.setName(
|
||||
$flowStore.summary
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_]/g, '_')
|
||||
.replace(/-+/g, '_')
|
||||
.replace(/^-|-$/g, '')
|
||||
)
|
||||
}
|
||||
}}
|
||||
elementProps={{
|
||||
type: 'text',
|
||||
id: 'flow-summary',
|
||||
placeholder: 'Short summary to be displayed when listed'
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
{#if !noEditor}
|
||||
<Label label="Path">
|
||||
<Path
|
||||
autofocus={false}
|
||||
bind:this={path}
|
||||
bind:dirty={dirtyPath}
|
||||
bind:path={$pathStore}
|
||||
{initialPath}
|
||||
namePlaceholder="flow"
|
||||
kind="flow"
|
||||
/>
|
||||
</Label>
|
||||
{/if}
|
||||
|
||||
<Label label="Description">
|
||||
<MetadataGen
|
||||
bind:content={$flowStore.description}
|
||||
promptConfigName="flowDescription"
|
||||
flow={$flowStore.value}
|
||||
class="w-full"
|
||||
elementType="textarea"
|
||||
elementProps={{
|
||||
id: 'inp',
|
||||
placeholder: 'What this flow does and how to use it.'
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<!-- TODO: Add EE-only badge when we have it -->
|
||||
<Toggle
|
||||
disabled={!$enterpriseLicense || isCloudHosted()}
|
||||
checked={$flowStore.value.priority !== undefined && $flowStore.value.priority > 0}
|
||||
on:change={() => {
|
||||
if ($flowStore.value.priority) {
|
||||
$flowStore.value.priority = undefined
|
||||
} else {
|
||||
$flowStore.value.priority = 100
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: `Label as high priority`,
|
||||
rightTooltip: `All jobs scheduled by flows labeled as high priority take precedence over the other jobs in the jobs queue. ${
|
||||
!$enterpriseLicense
|
||||
? 'This is a feature only available on enterprise edition.'
|
||||
: ''
|
||||
}`
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="right">
|
||||
<input
|
||||
type="number"
|
||||
class="!w-16 ml-4"
|
||||
disabled={$flowStore.value.priority === undefined}
|
||||
bind:value={$flowStore.value.priority}
|
||||
on:focus
|
||||
on:change={() => {
|
||||
if ($flowStore.value.priority && $flowStore.value.priority > 100) {
|
||||
$flowStore.value.priority = 100
|
||||
} else if ($flowStore.value.priority && $flowStore.value.priority < 0) {
|
||||
$flowStore.value.priority = 0
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</Toggle>
|
||||
|
||||
<Toggle
|
||||
size="sm"
|
||||
checked={Boolean($flowStore.visible_to_runner_only)}
|
||||
on:change={() => {
|
||||
if ($flowStore.visible_to_runner_only) {
|
||||
$flowStore.visible_to_runner_only = undefined
|
||||
} else {
|
||||
$flowStore.visible_to_runner_only = true
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Make runs invisible to others',
|
||||
rightTooltip:
|
||||
'When this option is enabled, manual executions of this script are invisible to users other than the user running it, including the owner(s). This setting can be overridden when this script is run manually from the advanced menu.'
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<ErrorHandlerToggleButton
|
||||
kind="flow"
|
||||
scriptOrFlowPath={$pathStore}
|
||||
bind:errorHandlerMuted={$flowStore.ws_error_handler_muted}
|
||||
iconOnly={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Slider text="How to trigger flows?">
|
||||
<div class="text-sm text-tertiary border p-4 mb-20">
|
||||
On-demand:
|
||||
<ul class="pt-4">
|
||||
<li>
|
||||
1. <a
|
||||
href="https://www.windmill.dev/docs/core_concepts/auto_generated_uis"
|
||||
target="_blank">Auto-generated UIs</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
2. <a href="{base}/apps/add?nodraft=true" target="_blank"> App Editor</a> for
|
||||
customized-UIs
|
||||
</li>
|
||||
<li>
|
||||
3. <a href="{base}/schedules" target="_blank">Scheduling</a>
|
||||
</li>
|
||||
<li>
|
||||
4. <a href="https://www.windmill.dev/docs/advanced/cli" target="_blank"
|
||||
>Windmill CLI</a
|
||||
>
|
||||
</li>
|
||||
<br />
|
||||
<li class="mt-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p> From external events: </p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="mt-2">
|
||||
5. Send a <a
|
||||
href="https://www.windmill.dev/docs/core_concepts/webhooks"
|
||||
target="_blank">webhook</a
|
||||
>
|
||||
after each event:
|
||||
<ul class="list-disc pl-4"
|
||||
><li
|
||||
>Async <Tooltip
|
||||
>Return an uuid instantly that you can use to fetch status and result</Tooltip
|
||||
>:
|
||||
<a
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
copyToClipboard(url)
|
||||
}}
|
||||
href={url}
|
||||
class="whitespace-nowrap text-ellipsis overflow-hidden mr-1"
|
||||
>
|
||||
{url}
|
||||
<span class="text-secondary ml-2">
|
||||
<Clipboard />
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
>Sync <Tooltip>Wait for result within a timeout of 20s</Tooltip>:
|
||||
<a
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
copyToClipboard(syncedUrl)
|
||||
}}
|
||||
href={syncedUrl}
|
||||
class="whitespace-nowrap text-ellipsis overflow-hidden mr-1"
|
||||
>
|
||||
{syncedUrl}
|
||||
<span class="text-secondary ml-2">
|
||||
<Clipboard />
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul></li
|
||||
>
|
||||
<br />
|
||||
<li>
|
||||
6. Use a <a
|
||||
href="https://www.windmill.dev/docs/flows/flow_trigger"
|
||||
target="_blank">trigger script</a
|
||||
>
|
||||
and schedule this flow to run as frequently as needed and compare a state persisted
|
||||
in Windmill to the state of the external system. If a difference is detected,
|
||||
then the rest of the flow is triggered. Oftentimes, the second step of a flow
|
||||
is a for-loop that will iterate over every elements. When using a trigger, a
|
||||
default schedule will be created.
|
||||
<img
|
||||
class="shadow-lg border rounded"
|
||||
alt="static button"
|
||||
src="{base}/trigger_button.png"
|
||||
/>
|
||||
</li></ul
|
||||
>
|
||||
</div>
|
||||
</Slider>
|
||||
</div>
|
||||
</Section>
|
||||
</TabContent>
|
||||
<TabContent value="settings-schedule" class="p-4 h-full overflow-scroll">
|
||||
<Alert
|
||||
type="info"
|
||||
title="Primary Schedule"
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/scheduling"
|
||||
>
|
||||
Flows can be triggered by any schedules, their webhooks or their UI but they have only
|
||||
one primary schedule with which they share the same path. The primary schedule can be
|
||||
set here.
|
||||
</Alert>
|
||||
<div class="mt-4" />
|
||||
<FlowSchedules />
|
||||
</TabContent>
|
||||
|
||||
<TabContent value="settings-same-worker" class="p-4 flex flex-col">
|
||||
<Section label="Shared Directory" class="flex flex-col">
|
||||
<Alert type="info" title="Shared Directory">
|
||||
Steps will share a folder at `./shared` in which they can store heavier data and
|
||||
pass them to the next step. <br /><br />Beware that the `./shared` folder is not
|
||||
preserved across suspends and sleeps. <br /><br />
|
||||
Furthermore, steps' worker groups is not respected and only the flow's worker group will
|
||||
be respected.
|
||||
</Alert>
|
||||
<Toggle
|
||||
bind:checked={$flowStore.value.same_worker}
|
||||
options={{
|
||||
right: 'Shared Directory on `./shared`'
|
||||
}}
|
||||
class="mt-2"
|
||||
{#if !noEditor}
|
||||
<Label label="Path">
|
||||
<Path
|
||||
autofocus={false}
|
||||
bind:this={path}
|
||||
bind:dirty={dirtyPath}
|
||||
bind:path={$pathStore}
|
||||
{initialPath}
|
||||
namePlaceholder="flow"
|
||||
kind="flow"
|
||||
/>
|
||||
</Section>
|
||||
</TabContent>
|
||||
<TabContent value="settings-cache" class="p-4 flex flex-col">
|
||||
<Section label="Cache">
|
||||
<svelte:fragment slot="action">
|
||||
<Toggle
|
||||
size="xs"
|
||||
checked={Boolean($flowStore.value.cache_ttl)}
|
||||
on:change={() => {
|
||||
if ($flowStore.value.cache_ttl && $flowStore.value.cache_ttl != undefined) {
|
||||
$flowStore.value.cache_ttl = undefined
|
||||
} else {
|
||||
$flowStore.value.cache_ttl = 300
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Cache the results for each possible inputs'
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</Label>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-x-4 flex-col gap-2">
|
||||
<div class="text-xs">How long to keep the cache valid</div>
|
||||
<Label label="Description">
|
||||
<MetadataGen
|
||||
bind:content={$flowStore.description}
|
||||
promptConfigName="flowDescription"
|
||||
flow={$flowStore.value}
|
||||
class="w-full"
|
||||
elementType="textarea"
|
||||
elementProps={{
|
||||
id: 'inp',
|
||||
placeholder: 'What this flow does and how to use it.'
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<!-- Deployable Section -->
|
||||
<Section
|
||||
label="Advanced"
|
||||
collapsable={true}
|
||||
small={true}
|
||||
class="h-full grow min-h-0 flex flex-col gap-4"
|
||||
>
|
||||
<!-- Metadata Advanced Section -->
|
||||
<!-- TODO: Add EE-only badge when we have it -->
|
||||
<svelte:fragment slot="badge">
|
||||
{#if numberOfAdvancedOptionsOn > 0}
|
||||
<div class="flex grow min-w-0 w-full flex-wrap gap-1 ps-2">
|
||||
{#each activeAdvancedOptionNames as optionName}
|
||||
<Badge twBgColor="bg-nord-300" twTextColor="text-white">{optionName}</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<!-- Priority Section -->
|
||||
<Toggle
|
||||
textClass="font-normal text-sm"
|
||||
color="nord"
|
||||
size="xs"
|
||||
disabled={!$enterpriseLicense || isCloudHosted()}
|
||||
checked={$flowStore.value.priority !== undefined && $flowStore.value.priority > 0}
|
||||
on:change={() => {
|
||||
if ($flowStore.value.priority) {
|
||||
$flowStore.value.priority = undefined
|
||||
} else {
|
||||
$flowStore.value.priority = 100
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: `Label as high priority`,
|
||||
rightTooltip: `All jobs scheduled by flows labeled as high priority take precedence over the other jobs in the jobs queue. ${
|
||||
!$enterpriseLicense ? 'This is a feature only available on enterprise edition.' : ''
|
||||
}`
|
||||
}}
|
||||
textDisabled={true}
|
||||
class="py-1 relative"
|
||||
>
|
||||
<svelte:fragment slot="right">
|
||||
<input
|
||||
type="number"
|
||||
class="!w-16 text-xs ml-4 absolute left-52"
|
||||
disabled={$flowStore.value.priority === undefined}
|
||||
bind:value={$flowStore.value.priority}
|
||||
on:focus
|
||||
on:change={() => {
|
||||
if ($flowStore.value.priority && $flowStore.value.priority > 100) {
|
||||
$flowStore.value.priority = 100
|
||||
} else if ($flowStore.value.priority && $flowStore.value.priority < 0) {
|
||||
$flowStore.value.priority = 0
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</Toggle>
|
||||
|
||||
<!-- Visibility Section -->
|
||||
<Toggle
|
||||
textClass="font-normal text-sm"
|
||||
color="nord"
|
||||
size="xs"
|
||||
checked={Boolean($flowStore.visible_to_runner_only)}
|
||||
on:change={() => {
|
||||
if ($flowStore.visible_to_runner_only) {
|
||||
$flowStore.visible_to_runner_only = undefined
|
||||
} else {
|
||||
$flowStore.visible_to_runner_only = true
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Make runs invisible to others',
|
||||
rightTooltip:
|
||||
'When this option is enabled, manual executions of this script are invisible to users other than the user running it, including the owner(s). This setting can be overridden when this script is run manually from the advanced menu.'
|
||||
}}
|
||||
class="py-1"
|
||||
textDisabled={true}
|
||||
/>
|
||||
|
||||
<!-- Error Handler Section -->
|
||||
<div class="flex flex-row items-center py-1">
|
||||
<ErrorHandlerToggleButtonV2
|
||||
color="nord"
|
||||
kind="flow"
|
||||
scriptOrFlowPath={$pathStore}
|
||||
bind:errorHandlerMuted={$flowStore.ws_error_handler_muted}
|
||||
textDisabled={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Shared Directory Section -->
|
||||
{#if customUi?.settingsTabs?.sharedDiretory != false}
|
||||
<Toggle
|
||||
textClass="font-normal text-sm"
|
||||
color="nord"
|
||||
size="xs"
|
||||
bind:checked={$flowStore.value.same_worker}
|
||||
options={{
|
||||
right: 'Shared Directory on `./shared`',
|
||||
rightTooltip:
|
||||
'Steps will share a folder at `./shared` in which they can store heavier data and ' +
|
||||
'pass them to the next step. Beware that the `./shared` folder is not ' +
|
||||
'preserved across suspends and sleeps.'
|
||||
}}
|
||||
class="py-1"
|
||||
textDisabled={true}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Cache Section -->
|
||||
{#if customUi?.settingsTabs?.cache != false}
|
||||
<div>
|
||||
<Toggle
|
||||
textClass="font-normal text-sm"
|
||||
color="nord"
|
||||
size="xs"
|
||||
checked={Boolean($flowStore.value.cache_ttl)}
|
||||
on:change={() => {
|
||||
if ($flowStore.value.cache_ttl && $flowStore.value.cache_ttl != undefined) {
|
||||
$flowStore.value.cache_ttl = undefined
|
||||
} else {
|
||||
$flowStore.value.cache_ttl = 300
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Cache the results for each possible inputs'
|
||||
}}
|
||||
class="py-1"
|
||||
textDisabled={true}
|
||||
/>
|
||||
{#if $flowStore.value.cache_ttl}
|
||||
<div class="flex gap-x-4 flex-col gap-1">
|
||||
<div class="text-sm text-secondary">How long to keep the cache valid</div>
|
||||
<div>
|
||||
{#if $flowStore.value.cache_ttl}
|
||||
<SecondsInput bind:seconds={$flowStore.value.cache_ttl} />
|
||||
@@ -351,32 +276,143 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</TabContent>
|
||||
|
||||
<TabContent value="settings-worker-group" class="p-4 flex flex-col">
|
||||
<Alert type="info" title="Worker Group Tag (Queue)">
|
||||
When a worker group tag is defined at the flow level, any steps inside the flow will
|
||||
run on any worker group that listen to that tag, regardless of the steps' tag. If no
|
||||
worker group tags is defined, the flow controls will be executed with the default tag
|
||||
'flow' and the steps will be executed with their respective tag
|
||||
</Alert>
|
||||
<span class="my-4 text-lg font-bold">Worker Group Tag (Queue)</span>
|
||||
<WorkerTagPicker bind:tag={$flowStore.tag} />
|
||||
|
||||
<div class="py-6" />
|
||||
<span class="my-4 text-lg font-bold flex items-baseline gap-8"
|
||||
>Dedicated Worker {#if !$enterpriseLicense}<div
|
||||
class="flex text-xs items-center gap-1 text-yellow-500 whitespace-nowrap"
|
||||
>
|
||||
<AlertTriangle size={16} />
|
||||
EE only
|
||||
</div>{/if}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Early Stop Section -->
|
||||
{#if customUi?.settingsTabs?.earlyStop != false}
|
||||
<div>
|
||||
<!-- documentationLink="https://www.windmill.dev/docs/flows/early_stop -->
|
||||
<Toggle
|
||||
textClass="font-normal text-sm"
|
||||
color="nord"
|
||||
size="xs"
|
||||
checked={Boolean($flowStore.value.skip_expr)}
|
||||
on:change={() => {
|
||||
if (Boolean($flowStore.value.skip_expr) && $flowStore.value.skip_expr) {
|
||||
$flowStore.value.skip_expr = undefined
|
||||
} else {
|
||||
$flowStore.value.skip_expr = 'flow_input.foo == undefined'
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Early stop if condition met',
|
||||
rightTooltip:
|
||||
'If defined, at the beginning of the step the predicate expression will be evaluated' +
|
||||
'to decide if the flow should stop early.'
|
||||
}}
|
||||
class="py-1"
|
||||
textDisabled={true}
|
||||
/>
|
||||
{#if $flowStore.value.skip_expr}
|
||||
<div
|
||||
class="w-full border flex flex-col {$flowStore.value.skip_expr
|
||||
? ''
|
||||
: 'bg-surface-secondary'}"
|
||||
>
|
||||
<div class="border w-full">
|
||||
<SimpleEditor
|
||||
lang="javascript"
|
||||
bind:code={$flowStore.value.skip_expr}
|
||||
class="small-editor"
|
||||
extraLib={`declare const flow_input = ${JSON.stringify(
|
||||
schemaToObject(asSchema($flowStore.schema), $previewArgs)
|
||||
)};
|
||||
declare const WM_SCHEDULED_FOR: string;`}
|
||||
/>
|
||||
<div class="text-xs text-tertiary mt-2">
|
||||
You can use the variable `flow_input` to access the inputs of the flow. <br
|
||||
/>The variable `WM_SCHEDULED_FOR` contains the time the flow was scheduled for
|
||||
which you can use to stop early non fresh jobs:
|
||||
<pre>new Date().getTime() - new Date(WM_SCHEDULED_FOR).getTime() {'>'} X</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Early Return Section -->
|
||||
{#if customUi?.settingsTabs?.earlyReturn != false}
|
||||
<div>
|
||||
<Toggle
|
||||
textClass="font-normal text-sm"
|
||||
color="nord"
|
||||
size="xs"
|
||||
checked={Boolean($flowStore.value.early_return)}
|
||||
on:change={() => {
|
||||
if (Boolean($flowStore.value.early_return) && $flowStore.value.early_return) {
|
||||
$flowStore.value.early_return = undefined
|
||||
} else {
|
||||
$flowStore.value.early_return = $flowStore.value.modules?.[0]?.id ?? 'a'
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Early return sync endpoint at a top-level step',
|
||||
rightTooltip:
|
||||
'If defined, sync endpoints will return early at the node defined here while the rest of the flow continue asynchronously.'
|
||||
}}
|
||||
class="py-1"
|
||||
textDisabled={true}
|
||||
/>
|
||||
{#if $flowStore.value.early_return}
|
||||
<div
|
||||
class="max-w-[120px] flex flex-col {$flowStore.value.early_return
|
||||
? ''
|
||||
: 'bg-surface-secondary'}"
|
||||
>
|
||||
<select
|
||||
name="oauth_name"
|
||||
id="oauth_name"
|
||||
bind:value={$flowStore.value.early_return}
|
||||
class="text-xs"
|
||||
>
|
||||
<option value={undefined}>Node's id</option>
|
||||
{#each $flowStore.value?.modules?.map((x) => x.id) as name}
|
||||
<option value={name}>{name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Worker Group Section -->
|
||||
{#if customUi?.settingsTabs?.workerGroup != false}
|
||||
<div>
|
||||
<Toggle
|
||||
textClass="font-normal text-sm"
|
||||
color="nord"
|
||||
size="xs"
|
||||
disabled={!$enterpriseLicense}
|
||||
checked={displayWorkerTagPicker}
|
||||
on:change={() => {
|
||||
displayWorkerTagPicker = !displayWorkerTagPicker
|
||||
if (!displayWorkerTagPicker) {
|
||||
$flowStore.tag = undefined
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Worker Group Tag (Queue)',
|
||||
rightTooltip:
|
||||
"When a worker group tag is defined at the flow level, any steps inside the flow will run on any worker group that listen to that tag, regardless of the steps tag. If no worker group tags is defined, the flow controls will be executed with the default tag 'flow' and the steps will be executed with their respective tag"
|
||||
}}
|
||||
class="py-1"
|
||||
textDisabled={true}
|
||||
/>
|
||||
|
||||
{#if displayWorkerTagPicker}
|
||||
<WorkerTagPicker bind:tag={$flowStore.tag} popupPlacement="top-end" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Toggle
|
||||
textClass="font-normal text-sm"
|
||||
color="nord"
|
||||
size="xs"
|
||||
disabled={!$enterpriseLicense || isCloudHosted()}
|
||||
size="sm"
|
||||
checked={Boolean($flowStore.dedicated_worker)}
|
||||
on:change={() => {
|
||||
if ($flowStore.dedicated_worker) {
|
||||
@@ -388,9 +424,11 @@
|
||||
options={{
|
||||
right: 'Flow is run on dedicated workers'
|
||||
}}
|
||||
class="py-1"
|
||||
textDisabled={true}
|
||||
/>
|
||||
{#if $flowStore.dedicated_worker}
|
||||
<div class="py-2">
|
||||
<div>
|
||||
<Alert type="info" title="Require dedicated workers">
|
||||
One worker in a worker group needs to be configured with dedicated worker set to: <pre
|
||||
>{$workspaceStore}:flow/{$pathStore}</pre
|
||||
@@ -398,111 +436,40 @@
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip
|
||||
>In this mode, every scripts of this flow is run on the workers dedicated to this
|
||||
flow that keep the scripts "hot" so that there is not cold start cost incurred.
|
||||
Steps can run at >1500 rps in this mode.</Tooltip
|
||||
>
|
||||
</svelte:fragment>
|
||||
</TabContent>
|
||||
<TabContent value="settings-early-stop" class="p-4">
|
||||
<Section label="Early stop">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip documentationLink="https://www.windmill.dev/docs/flows/early_stop">
|
||||
If defined, at the beginning of the step the predicate expression will be
|
||||
evaluated to decide if the flow should stop early.
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
<Toggle
|
||||
checked={Boolean($flowStore.value.skip_expr)}
|
||||
on:change={() => {
|
||||
if (Boolean($flowStore.value.skip_expr) && $flowStore.value.skip_expr) {
|
||||
$flowStore.value.skip_expr = undefined
|
||||
} else {
|
||||
$flowStore.value.skip_expr = 'flow_input.foo == undefined'
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Early stop if condition met'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="w-full border mt-2 p-2 flex flex-col {$flowStore.value.skip_expr
|
||||
? ''
|
||||
: 'bg-surface-secondary'}"
|
||||
>
|
||||
{#if $flowStore.value.skip_expr}
|
||||
<div class="border w-full">
|
||||
<SimpleEditor
|
||||
lang="javascript"
|
||||
bind:code={$flowStore.value.skip_expr}
|
||||
class="small-editor"
|
||||
extraLib={`declare const flow_input = ${JSON.stringify(
|
||||
schemaToObject(asSchema($flowStore.schema), $previewArgs)
|
||||
)};
|
||||
declare const WM_SCHEDULED_FOR: string;`}
|
||||
/>
|
||||
<div class="text-xs text-tertiary mt-2">
|
||||
You can use the variable `flow_input` to access the inputs of the flow. <br
|
||||
/>The variable `WM_SCHEDULED_FOR` contains the time the flow was scheduled for
|
||||
which you can use to stop early non fresh jobs:
|
||||
<pre>new Date().getTime() - new Date(WM_SCHEDULED_FOR).getTime() {'>'} X</pre>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<textarea disabled rows="3" class="min-h-[80px]" />
|
||||
{/if}
|
||||
</div>
|
||||
</Section>
|
||||
</TabContent>
|
||||
<TabContent value="settings-early-return" class="p-4">
|
||||
<Section label="Early Return">
|
||||
<div class="py-2">
|
||||
<Alert type="info" title="Return sync endpoints early">
|
||||
If defined, sync endpoints will return early at the node defined here while the
|
||||
rest of the flow continue asynchronously.
|
||||
</Alert>
|
||||
</div>
|
||||
<Toggle
|
||||
checked={Boolean($flowStore.value.early_return)}
|
||||
on:change={() => {
|
||||
if (Boolean($flowStore.value.early_return) && $flowStore.value.early_return) {
|
||||
$flowStore.value.early_return = undefined
|
||||
} else {
|
||||
$flowStore.value.early_return = $flowStore.value.modules?.[0]?.id ?? 'a'
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Early return sync endpoint at a top-level step'
|
||||
}}
|
||||
/>
|
||||
<!-- <Tooltip
|
||||
>In this mode, every scripts of this flow is run on the workers dedicated to this flow
|
||||
that keep the scripts "hot" so that there is not cold start cost incurred. Steps can run
|
||||
at >1500 rps in this mode.</Tooltip
|
||||
> -->
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="border max-w-[120px] mt-2 p-2 flex flex-col {$flowStore.value.early_return
|
||||
? ''
|
||||
: 'bg-surface-secondary'}"
|
||||
>
|
||||
<select
|
||||
name="oauth_name"
|
||||
id="oauth_name"
|
||||
bind:value={$flowStore.value.early_return}
|
||||
>
|
||||
<option value={undefined}>Node's id</option>
|
||||
{#each $flowStore.value?.modules?.map((x) => x.id) as name}
|
||||
<option value={name}>{name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</Section>
|
||||
</TabContent>
|
||||
<!-- Concurrency Section -->
|
||||
{#if customUi?.settingsTabs?.concurrency != false}
|
||||
<div>
|
||||
<Toggle
|
||||
textClass="font-normal text-sm"
|
||||
color="nord"
|
||||
size="xs"
|
||||
disabled={!$enterpriseLicense}
|
||||
checked={Boolean($flowStore.value.concurrent_limit)}
|
||||
on:change={() => {
|
||||
if ($flowStore.value.concurrent_limit) {
|
||||
$flowStore.value.concurrent_limit = undefined
|
||||
} else {
|
||||
$flowStore.value.concurrent_limit = 1
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Concurrency Limits',
|
||||
rightTooltip: 'Allowed concurrency within a given timeframe'
|
||||
}}
|
||||
class="py-1"
|
||||
textDisabled={true}
|
||||
/>
|
||||
|
||||
<TabContent value="settings-concurrency" class="p-4 flex flex-col">
|
||||
<Section label="Concurrency Limits" eeOnly>
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip>Allowed concurrency within a given timeframe</Tooltip>
|
||||
</svelte:fragment>
|
||||
{#if $flowStore.value.concurrent_limit}
|
||||
<div class="flex flex-col gap-4">
|
||||
<Label label="Max number of executions within the time window">
|
||||
<div class="flex flex-row gap-2 max-w-sm">
|
||||
@@ -544,10 +511,10 @@
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
</Section>
|
||||
</TabContent>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</Section>
|
||||
</div>
|
||||
</FlowCard>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
<script lang="ts">
|
||||
import Tab from '$lib/components/common/tabs/Tab.svelte'
|
||||
import { Tabs } from '$lib/components/common'
|
||||
import WebhooksPanel from '$lib/components/details/WebhooksPanel.svelte'
|
||||
import EmailTriggerPanel from '$lib/components/details/EmailTriggerPanel.svelte'
|
||||
import RoutesPanel from '$lib/components/triggers/RoutesPanel.svelte'
|
||||
import RunPageSchedules from '$lib/components/RunPageSchedules.svelte'
|
||||
import { HttpTriggerService, ScheduleService, type HttpTrigger, type Schedule } from '$lib/gen'
|
||||
import { canWrite } from '$lib/utils'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import FlowCard from '../common/FlowCard.svelte'
|
||||
import { onMount, getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
let schedules: Schedule[] | undefined = undefined
|
||||
|
||||
export let noEditor: boolean
|
||||
export let newFlow = false
|
||||
let triggers: (HttpTrigger & { canWrite: boolean })[] | undefined = undefined
|
||||
let path = ''
|
||||
|
||||
const { pathStore, selectedTrigger } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
$: path = $pathStore
|
||||
|
||||
async function loadSchedules() {
|
||||
if (!path) return
|
||||
try {
|
||||
schedules = (
|
||||
await ScheduleService.listSchedules({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: path,
|
||||
isFlow: true
|
||||
})
|
||||
).filter((s) => s.path != path)
|
||||
} catch (e) {
|
||||
console.error('impossible to load schedules')
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTriggers() {
|
||||
try {
|
||||
triggers = (
|
||||
await HttpTriggerService.listHttpTriggers({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path,
|
||||
isFlow: true
|
||||
})
|
||||
).map((x) => {
|
||||
return { canWrite: canWrite(x.path, x.extra_perms!, $userStore), ...x }
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('impossible to load http routes')
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!newFlow) {
|
||||
loadSchedules()
|
||||
loadTriggers()
|
||||
}
|
||||
})
|
||||
|
||||
let selectedTab = $selectedTrigger ?? 'webhooks'
|
||||
</script>
|
||||
|
||||
<FlowCard {noEditor} title="Flow Triggers">
|
||||
<div class="pt-4">
|
||||
<Tabs bind:selected={selectedTab}>
|
||||
<Tab value="webhooks" disabled={newFlow} selectedClass="text-primary font-semibold"
|
||||
>Webhooks</Tab
|
||||
>
|
||||
<Tab value="mail" disabled={newFlow} selectedClass="text-primary text-sm font-semibold"
|
||||
>Mail</Tab
|
||||
>
|
||||
<Tab value="routes" disabled={newFlow} selectedClass="text-primary text-sm font-semibold"
|
||||
>Routes</Tab
|
||||
>
|
||||
<Tab value="schedules" disabled={newFlow} selectedClass="text-primary text-sm font-semibold"
|
||||
>Schedules</Tab
|
||||
>
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
{#if selectedTab === 'webhooks'}
|
||||
<div class="p-4">
|
||||
<WebhooksPanel scopes={[`run:flow/${path}`]} {path} isFlow={true} args={{}} token="" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if selectedTab === 'mail'}
|
||||
<div class="p-4">
|
||||
<EmailTriggerPanel token="" scopes={[`run:flow/${path}`]} {path} isFlow={true} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !newFlow && selectedTab === 'routes'}
|
||||
<div class="p-4">
|
||||
<RoutesPanel path={path ?? ''} isFlow={true} bind:triggers />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !newFlow && selectedTab === 'schedules'}
|
||||
<div class="p-4">
|
||||
<RunPageSchedules
|
||||
isFlow={true}
|
||||
path={path ?? ''}
|
||||
can_write={canWrite(path, {}, $userStore)}
|
||||
bind:schedules
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
</div>
|
||||
</FlowCard>
|
||||
@@ -45,7 +45,8 @@
|
||||
'preprocessor',
|
||||
'constants',
|
||||
'Result',
|
||||
'Input'
|
||||
'Input',
|
||||
'triggers'
|
||||
].includes($selectedId) ||
|
||||
$selectedId?.includes('branch')
|
||||
</script>
|
||||
|
||||
@@ -28,6 +28,7 @@ export type ExtendedOpenFlow = OpenFlow & {
|
||||
|
||||
export type FlowEditorContext = {
|
||||
selectedId: Writable<string>
|
||||
selectedTrigger: Writable<string>
|
||||
moving: Writable<{ module: FlowModule; modules: FlowModule[] } | undefined>
|
||||
schedule: Writable<Schedule>
|
||||
previewArgs: Writable<Record<string, any>>
|
||||
|
||||
@@ -241,7 +241,8 @@
|
||||
!$selectedId.startsWith('settings') &&
|
||||
$selectedId !== 'failure' &&
|
||||
$selectedId !== 'preprocessor' &&
|
||||
$selectedId !== 'Result'
|
||||
$selectedId !== 'Result' &&
|
||||
$selectedId !== 'triggers'
|
||||
|
||||
const viewport = writable<Viewport>({
|
||||
x: 0,
|
||||
|
||||
@@ -5,18 +5,13 @@
|
||||
import { HttpTriggerService, ScheduleService, type HttpTrigger, type Schedule } from '$lib/gen'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
|
||||
import { DrawerContent, Tabs } from '$lib/components/common'
|
||||
import WebhooksPanel from '$lib/components/details/WebhooksPanel.svelte'
|
||||
import Tab from '$lib/components/common/tabs/Tab.svelte'
|
||||
import EmailTriggerPanel from '$lib/components/details/EmailTriggerPanel.svelte'
|
||||
import TriggerCount from './TriggerCount.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import { Route } from 'lucide-svelte'
|
||||
import { canWrite } from '$lib/utils'
|
||||
import RoutesPanel from '$lib/components/triggers/RoutesPanel.svelte'
|
||||
import RunPageSchedules from '$lib/components/RunPageSchedules.svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from '../../../flows/types'
|
||||
|
||||
let schedules: Schedule[] | undefined = undefined
|
||||
let triggers: (HttpTrigger & { canWrite: boolean })[] | undefined = undefined
|
||||
@@ -69,62 +64,28 @@
|
||||
if (!newFlow) {
|
||||
loadSchedules()
|
||||
loadTriggers()
|
||||
console.log('triggerWrapper mounted')
|
||||
}
|
||||
})
|
||||
|
||||
let drawer: Drawer | undefined = undefined
|
||||
let selectedTab: 'webhooks' | 'mail' | 'routes' | 'schedules' = 'webhooks'
|
||||
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const selectedId = flowEditorContext?.selectedId ?? { subscribe: () => () => {} }
|
||||
const selectedTrigger = flowEditorContext?.selectedTrigger ?? { subscribe: () => () => {} }
|
||||
</script>
|
||||
|
||||
{#if isEditor}
|
||||
{#if !newFlow}
|
||||
<Drawer bind:this={drawer} size="600px">
|
||||
<DrawerContent title="Triggers" noPadding on:close={drawer.closeDrawer}>
|
||||
<Tabs bind:selected={selectedTab}>
|
||||
<Tab value="webhooks" disabled={newFlow}>Webhooks</Tab>
|
||||
<Tab value="mail" disabled={newFlow}>Mail</Tab>
|
||||
<Tab value="routes" disabled={newFlow}>Routes</Tab>
|
||||
<Tab value="schedules" disabled={newFlow}>Schedules</Tab>
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
{#if selectedTab === 'webhooks'}
|
||||
<WebhooksPanel
|
||||
scopes={[`run:flow/${path}`]}
|
||||
{path}
|
||||
isFlow={true}
|
||||
args={{}}
|
||||
token=""
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if selectedTab === 'mail'}
|
||||
<EmailTriggerPanel token="" scopes={[`run:flow/${path}`]} {path} isFlow={true} />
|
||||
{/if}
|
||||
|
||||
{#if !newFlow && selectedTab === 'routes'}
|
||||
<RoutesPanel path={path ?? ''} isFlow={true} bind:triggers />
|
||||
{/if}
|
||||
|
||||
{#if !newFlow && selectedTab === 'schedules'}
|
||||
<RunPageSchedules
|
||||
isFlow={true}
|
||||
path={path ?? ''}
|
||||
can_write={canWrite(path, {}, $userStore)}
|
||||
bind:schedules
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
{/if}
|
||||
<div style={`width: ${NODE.width}px; margin-top: -24px;`}>
|
||||
<div class="flex flex-row mx-auto w-min">
|
||||
<div
|
||||
class="flex flex-row gap-2 px-2 border p-1 rounded-md border-surface-selected bg-surface shadow-md items-center"
|
||||
<button
|
||||
class="flex flex-row gap-2 px-2 border p-1 rounded-md bg-surface shadow-md items-center {$selectedId?.startsWith(
|
||||
'triggers'
|
||||
)
|
||||
? 'outline outline-offset-1 outline-2 outline-slate-900 dark:bg-white/5 dark:outline-slate-800/60 dark:border-gray-400'
|
||||
: ''}"
|
||||
on:click={() => ($selectedId = 'triggers')}
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-row items-center text-2xs">
|
||||
<div class="flex flex-row items-center text-2xs font-normal">
|
||||
Triggers
|
||||
|
||||
<Tooltip small wrapperClass="center-center">
|
||||
@@ -144,8 +105,8 @@
|
||||
<TriggerButton
|
||||
on:click={() => {
|
||||
if (isEditor) {
|
||||
selectedTab = 'webhooks'
|
||||
drawer?.openDrawer()
|
||||
$selectedTrigger = 'webhooks'
|
||||
$selectedId = 'webhooks'
|
||||
} else {
|
||||
dispatch('triggerDetail', 'webhooks')
|
||||
}
|
||||
@@ -166,8 +127,8 @@
|
||||
<TriggerButton
|
||||
on:click={() => {
|
||||
if (isEditor) {
|
||||
selectedTab = 'mail'
|
||||
drawer?.openDrawer()
|
||||
$selectedTrigger = 'mail'
|
||||
$selectedId = 'mail'
|
||||
} else {
|
||||
dispatch('triggerDetail', 'mail')
|
||||
}
|
||||
@@ -188,8 +149,8 @@
|
||||
<TriggerButton
|
||||
on:click={() => {
|
||||
if (isEditor) {
|
||||
selectedTab = 'routes'
|
||||
drawer?.openDrawer()
|
||||
$selectedTrigger = 'routes'
|
||||
$selectedId = 'routes'
|
||||
} else {
|
||||
dispatch('triggerDetail', 'routes')
|
||||
}
|
||||
@@ -211,8 +172,8 @@
|
||||
<TriggerButton
|
||||
on:click={() => {
|
||||
if (isEditor) {
|
||||
selectedTab = 'schedules'
|
||||
drawer?.openDrawer()
|
||||
$selectedTrigger = 'schedules'
|
||||
$selectedId = 'schedules'
|
||||
} else {
|
||||
dispatch('triggerDetail', 'schedule')
|
||||
}
|
||||
@@ -223,7 +184,7 @@
|
||||
<Calendar size={12} />
|
||||
</TriggerButton>
|
||||
</Popover>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
bind:this={routeEditor}
|
||||
/>
|
||||
|
||||
<div class="p-2 flex flex-col">
|
||||
<div class="flex flex-col">
|
||||
{#if $userStore?.is_admin || $userStore?.is_super_admin}
|
||||
<Button
|
||||
on:click={() => routeEditor?.openNew(isFlow, path)}
|
||||
@@ -56,22 +56,24 @@
|
||||
|
||||
{#if triggers}
|
||||
{#if triggers.length == 0}
|
||||
<div class="text-xs text-secondary px-2"> No http routes </div>
|
||||
<div class="text-xs text-secondary"> No http routes </div>
|
||||
{:else}
|
||||
<div class="flex flex-col divide-y px-2 pt-2">
|
||||
<div class="flex flex-col divide-y pt-2">
|
||||
{#each triggers as trigger (trigger.path)}
|
||||
<div class="grid grid-cols-5 text-2xs items-center py-2">
|
||||
<div class="col-span-2 truncate">{trigger.path}</div>
|
||||
<div class="col-span-2 truncate">
|
||||
{trigger.http_method.toUpperCase()} /{trigger.route_path}
|
||||
</div>
|
||||
<button on:click={() => routeEditor?.openEdit(trigger.path, isFlow)}>
|
||||
{#if trigger.canWrite}
|
||||
Edit
|
||||
{:else}
|
||||
View
|
||||
{/if}
|
||||
</button>
|
||||
<div class="flex justify-end">
|
||||
<button on:click={() => routeEditor?.openEdit(trigger.path, isFlow)} class="px-2">
|
||||
{#if trigger.canWrite}
|
||||
Edit
|
||||
{:else}
|
||||
View
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -490,27 +490,35 @@
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="webhooks">
|
||||
<WebhooksPanel
|
||||
bind:token
|
||||
scopes={[`run:flow/${flow?.path}`]}
|
||||
path={flow?.path}
|
||||
isFlow={true}
|
||||
{args}
|
||||
/>
|
||||
<div class="p-2">
|
||||
<WebhooksPanel
|
||||
bind:token
|
||||
scopes={[`run:flow/${flow?.path}`]}
|
||||
path={flow?.path}
|
||||
isFlow={true}
|
||||
{args}
|
||||
/>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="routes">
|
||||
<RoutesPanel path={flow.path ?? ''} isFlow />
|
||||
<div class="p-2">
|
||||
<RoutesPanel path={flow.path ?? ''} isFlow />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="email">
|
||||
<EmailTriggerPanel
|
||||
bind:token
|
||||
scopes={[`run:flow/${flow?.path}`]}
|
||||
path={flow?.path}
|
||||
isFlow={true}
|
||||
/>
|
||||
<div class="p-2">
|
||||
<EmailTriggerPanel
|
||||
bind:token
|
||||
scopes={[`run:flow/${flow?.path}`]}
|
||||
path={flow?.path}
|
||||
isFlow={true}
|
||||
/>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="schedule">
|
||||
<RunPageSchedules isFlow={true} path={flow.path ?? ''} {can_write} />
|
||||
<div class="p-2">
|
||||
<RunPageSchedules isFlow={true} path={flow.path ?? ''} {can_write} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="cli">
|
||||
<div class="p-2">
|
||||
|
||||
@@ -695,27 +695,35 @@
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="webhooks">
|
||||
<WebhooksPanel
|
||||
bind:token
|
||||
scopes={[`run:script/${script?.path}`]}
|
||||
hash={script.hash}
|
||||
path={script.path}
|
||||
{args}
|
||||
/>
|
||||
<div class="p-2">
|
||||
<WebhooksPanel
|
||||
bind:token
|
||||
scopes={[`run:script/${script?.path}`]}
|
||||
hash={script.hash}
|
||||
path={script.path}
|
||||
{args}
|
||||
/>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="routes">
|
||||
<RoutesPanel path={script.path ?? ''} isFlow={false} />
|
||||
<div class="p-2">
|
||||
<RoutesPanel path={script.path ?? ''} isFlow={false} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="email">
|
||||
<EmailTriggerPanel
|
||||
bind:token
|
||||
scopes={[`run:script/${script?.path}`]}
|
||||
hash={script.hash}
|
||||
path={script.path}
|
||||
/>
|
||||
<div class="p-2">
|
||||
<EmailTriggerPanel
|
||||
bind:token
|
||||
scopes={[`run:script/${script?.path}`]}
|
||||
hash={script.hash}
|
||||
path={script.path}
|
||||
/>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="schedule">
|
||||
<RunPageSchedules isFlow={false} path={script.path ?? ''} {can_write} />
|
||||
<div class="p-2">
|
||||
<RunPageSchedules isFlow={false} path={script.path ?? ''} {can_write} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="details">
|
||||
<div>
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
|
||||
const testStepStore = writable<Record<string, any>>({})
|
||||
const selectedIdStore = writable('settings-metadata')
|
||||
const selectedTriggerStore = writable('webhooks')
|
||||
|
||||
// function select(selectedId: string) {
|
||||
// selectedIdStore.set(selectedId)
|
||||
@@ -85,6 +86,7 @@
|
||||
|
||||
setContext<FlowEditorContext>('FlowEditorContext', {
|
||||
selectedId: selectedIdStore,
|
||||
selectedTrigger: selectedTriggerStore,
|
||||
schedule: scheduleStore,
|
||||
previewArgs: previewArgsStore,
|
||||
scriptEditorDrawer,
|
||||
|
||||
@@ -12,8 +12,8 @@ const lightTheme = {
|
||||
textTertiary: '#505c70',
|
||||
textDisabled: '#a0aec0',
|
||||
|
||||
border: '#ddd',
|
||||
borderHover: '#ccc'
|
||||
border: '#dddddd',
|
||||
borderHover: '#cccccc'
|
||||
}
|
||||
|
||||
const lightThemeRgb = makeRgb(lightTheme)
|
||||
@@ -384,6 +384,20 @@ const config = {
|
||||
400: '#394A6D',
|
||||
500: '#323F5B'
|
||||
},
|
||||
nord: {
|
||||
0: '#2E3440',
|
||||
100: '#3B4252',
|
||||
200: '#434C5E',
|
||||
300: '#4C566A',
|
||||
400: '#D8DEE9',
|
||||
500: '#E5E9F0',
|
||||
600: '#ECEFF4',
|
||||
700: '#8FBCBB',
|
||||
800: '#88C0D0',
|
||||
900: '#81A1C1',
|
||||
950: '#5E81AC'
|
||||
},
|
||||
|
||||
surface: 'rgb(var(--color-surface) / <alpha-value>)',
|
||||
'surface-secondary': 'rgb(var(--color-surface-secondary) / <alpha-value>)',
|
||||
'surface-hover': 'rgb(var(--color-surface-hover) / <alpha-value>)',
|
||||
|
||||
Reference in New Issue
Block a user