feat(frontend): improve and simplify scheduled poll flows (#4560)

This commit is contained in:
Guilhem
2024-11-04 23:18:09 +01:00
committed by GitHub
parent 3741fe60cf
commit cc1d3f2193
46 changed files with 1011 additions and 540 deletions
+8
View File
@@ -1112,6 +1112,7 @@ async fn test_deno_flow(db: Pool<Postgres>) {
custom_concurrency_key: None,
concurrent_limit: None,
concurrency_time_window_s: None,
is_trigger: None,
}
.into(),
stop_after_if: Default::default(),
@@ -1153,6 +1154,7 @@ async fn test_deno_flow(db: Pool<Postgres>) {
custom_concurrency_key: None,
concurrent_limit: None,
concurrency_time_window_s: None,
is_trigger: None,
}
.into(),
stop_after_if: Default::default(),
@@ -1276,6 +1278,8 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
custom_concurrency_key: None,
concurrent_limit: None,
concurrency_time_window_s: None,
is_trigger: None,
}.into(),
stop_after_if: Default::default(),
stop_after_all_iters_if: Default::default(),
@@ -1327,6 +1331,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
custom_concurrency_key: None,
concurrent_limit: None,
concurrency_time_window_s: None,
is_trigger: None,
}.into(),
stop_after_if: Default::default(),
stop_after_all_iters_if: Default::default(),
@@ -1364,6 +1369,8 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
custom_concurrency_key: None,
concurrent_limit: None,
concurrency_time_window_s: None,
is_trigger: None,
}.into(),
stop_after_if: Default::default(),
stop_after_all_iters_if: Default::default(),
@@ -1424,6 +1431,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
custom_concurrency_key: None,
concurrent_limit: None,
concurrency_time_window_s: None,
is_trigger: None,
}.into(),
stop_after_if: Default::default(),
stop_after_all_iters_if: Default::default(),
+3
View File
@@ -1209,6 +1209,7 @@ mod tests {
.into(),
hash: None,
tag_override: None,
is_trigger: None,
}),
stop_after_if: None,
stop_after_all_iters_if: None,
@@ -1236,6 +1237,7 @@ mod tests {
custom_concurrency_key: None,
concurrent_limit: None,
concurrency_time_window_s: None,
is_trigger: None,
}),
stop_after_if: Some(StopAfterIf {
expr: "foo = 'bar'".to_string(),
@@ -1290,6 +1292,7 @@ mod tests {
input_transforms: HashMap::new(),
hash: None,
tag_override: None,
is_trigger: None,
}
.into(),
stop_after_if: Some(StopAfterIf {
+7
View File
@@ -426,6 +426,8 @@ pub enum FlowModuleValue {
hash: Option<ScriptHash>,
#[serde(skip_serializing_if = "Option::is_none")]
tag_override: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
is_trigger: Option<bool>,
},
Flow {
#[serde(default)]
@@ -474,6 +476,8 @@ pub enum FlowModuleValue {
concurrent_limit: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
concurrency_time_window_s: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
is_trigger: Option<bool>,
},
Identity,
}
@@ -505,6 +509,7 @@ struct UntaggedFlowModuleValue {
custom_concurrency_key: Option<String>,
concurrent_limit: Option<i32>,
concurrency_time_window_s: Option<i32>,
is_trigger: Option<bool>,
}
impl<'de> Deserialize<'de> for FlowModuleValue {
@@ -522,6 +527,7 @@ impl<'de> Deserialize<'de> for FlowModuleValue {
.ok_or_else(|| serde::de::Error::missing_field("path"))?,
hash: untagged.hash,
tag_override: untagged.tag_override,
is_trigger: untagged.is_trigger,
}),
"flow" => Ok(FlowModuleValue::Flow {
input_transforms: untagged.input_transforms.unwrap_or_default(),
@@ -574,6 +580,7 @@ impl<'de> Deserialize<'de> for FlowModuleValue {
custom_concurrency_key: untagged.custom_concurrency_key,
concurrent_limit: untagged.concurrent_limit,
concurrency_time_window_s: untagged.concurrency_time_window_s,
is_trigger: untagged.is_trigger,
}),
"identity" => Ok(FlowModuleValue::Identity),
other => Err(serde::de::Error::unknown_variant(
+1
View File
@@ -3522,6 +3522,7 @@ pub async fn push<'c, 'd, R: rsmq_async::RsmqConnection + Send + 'c>(
path: path.clone(),
hash: Some(hash),
tag_override: tag_override,
is_trigger: None,
},
),
stop_after_if: None,
@@ -735,6 +735,7 @@ async fn lock_modules<'c>(
custom_concurrency_key,
concurrent_limit,
concurrency_time_window_s,
is_trigger,
} = e.get_value()?
else {
match e.get_value()? {
@@ -970,6 +971,7 @@ async fn lock_modules<'c>(
custom_concurrency_key,
concurrent_limit,
concurrency_time_window_s,
is_trigger,
});
new_flow_modules.push(e);
continue;
@@ -992,6 +994,7 @@ async fn lock_modules<'c>(
custom_concurrency_key,
concurrent_limit,
concurrency_time_window_s,
is_trigger,
});
new_flow_modules.push(e);
continue;
+5
View File
@@ -182,6 +182,11 @@
"svelte": "./package/components/icons/WindmillIcon2.svelte",
"default": "./package/components/icons/WindmillIcon2.svelte"
},
"./components/icons/SchedulePollIcon.svelte": {
"types": "./package/components/icons/SchedulePollIcon.d.ts",
"svelte": "./package/components/icons/SchedulePollIcon.svelte",
"default": "./package/components/icons/SchedulePollIcon.svelte"
},
"./components/IconedResourceType.svelte": {
"types": "./package/components/IconedResourceType.svelte.d.ts",
"svelte": "./package/components/IconedResourceType.svelte",
+3 -2
View File
@@ -476,7 +476,7 @@
const testStepStore = writable<Record<string, any>>({})
const selectedIdStore = writable('settings-metadata')
const selectedTriggerStore = writable<
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets'
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets' | 'scheduledPoll'
>('webhooks')
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(undefined)
@@ -484,7 +484,8 @@
setContext<TriggerContext>('TriggerContext', {
primarySchedule: primaryScheduleStore,
selectedTrigger: selectedTriggerStore,
triggersCount: triggersCount
triggersCount: triggersCount,
simplifiedPoll: writable(false)
})
setContext<FlowEditorContext>('FlowEditorContext', {
selectedId: selectedIdStore,
+12 -5
View File
@@ -144,7 +144,7 @@
? { schedule_count: 1, primary_schedule: { schedule: savedPrimarySchedule.cron } }
: undefined
)
const simplifiedPoll = writable(false)
export function setPrimarySchedule(schedule: ScheduleTrigger | undefined | false) {
primaryScheduleStore.set(schedule)
loadTriggers()
@@ -468,7 +468,7 @@
const selectedIdStore = writable<string>(selectedId ?? 'settings-metadata')
const selectedTriggerStore = writable<
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets'
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets' | 'scheduledPoll'
>('webhooks')
export function getSelectedId() {
@@ -490,7 +490,14 @@
}
function selectTrigger(
selectedTrigger: 'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets'
selectedTrigger:
| 'webhooks'
| 'emails'
| 'schedules'
| 'cli'
| 'routes'
| 'websockets'
| 'scheduledPoll'
) {
selectedTriggerStore.set(selectedTrigger)
}
@@ -517,7 +524,8 @@
setContext<TriggerContext>('TriggerContext', {
selectedTrigger: selectedTriggerStore,
primarySchedule: primaryScheduleStore,
triggersCount
triggersCount,
simplifiedPoll
})
async function loadTriggers() {
@@ -876,7 +884,6 @@
await tick()
select(module.id)
await tick()
await tick()
focusCopilot()
let isFirstInLoop = false
@@ -1008,6 +1008,7 @@
modules={job.raw_flow?.modules ?? []}
failureModule={job.raw_flow?.failure_module}
preprocessorModule={job.raw_flow?.preprocessor_module}
allowSimplifiedPoll={false}
/>
</div>
<div
@@ -12,6 +12,8 @@
import SchemaForm from './SchemaForm.svelte'
import { emptyString, sendUserToast } from '$lib/utils'
import Toggle from './Toggle.svelte'
import { loadSchedules, saveSchedule } from './flows/scheduleUtils'
import { type Writable, writable } from 'svelte/store'
export let schema: any
export let isFlow: boolean
@@ -22,103 +24,33 @@
const { primarySchedule, triggersCount } = getContext<TriggerContext>('TriggerContext')
let scheduleEditor: ScheduleEditor
let schedules: Writable<Schedule[] | undefined> = writable(undefined)
let initialPrimarySchedule: Writable<ScheduleTrigger | false | undefined> = writable(undefined)
$: loadSchedules(false) || path
let schedules: Schedule[] | undefined = undefined
let initialPrimarySchedule: ScheduleTrigger | false | undefined = undefined
async function loadSchedules(forceRefresh: boolean) {
if (!path || path == '') {
schedules = []
if ($primarySchedule == undefined) {
$primarySchedule = false
}
initialPrimarySchedule = structuredClone($primarySchedule)
return
}
try {
const allSchedules = await ScheduleService.listSchedules({
workspace: $workspaceStore ?? '',
path: path,
isFlow
})
const primary = allSchedules.find((s) => s.path == path)
let remotePrimarySchedule = primary
? {
summary: primary.summary,
args: primary.args ?? {},
cron: primary.schedule,
timezone: primary.timezone,
enabled: primary.enabled
}
: false
if ($primarySchedule == undefined || forceRefresh) {
$primarySchedule = remotePrimarySchedule
}
initialPrimarySchedule = structuredClone(remotePrimarySchedule)
$triggersCount = {
...($triggersCount ?? {}),
schedule_count: allSchedules.length,
primary_schedule: $primarySchedule ? { schedule: $primarySchedule.cron } : undefined
}
schedules = allSchedules.filter((s) => s.path != path)
} catch (e) {
console.error('impossible to load schedules', e)
}
async function updateSchedules(forceRefresh: boolean) {
loadSchedules(
forceRefresh,
path,
isFlow,
schedules,
primarySchedule,
initialPrimarySchedule,
$workspaceStore ?? '',
triggersCount
)
}
$: updateSchedules(false) || path
async function save() {
const scheduleExists =
path != '' &&
!newItem &&
(await ScheduleService.existsSchedule({
workspace: $workspaceStore!,
path
}))
if (scheduleExists) {
console.log('primary schedule exists')
if ($primarySchedule) {
await ScheduleService.updateSchedule({
workspace: $workspaceStore!,
path,
requestBody: {
summary: $primarySchedule.summary,
args: $primarySchedule.args,
schedule: $primarySchedule.cron,
timezone: $primarySchedule.timezone
}
})
sendUserToast(`Primary schedule updated`)
} else {
await ScheduleService.deleteSchedule({ workspace: $workspaceStore!, path })
sendUserToast(`Primary schedule deleted`)
}
} else {
if ($primarySchedule) {
await ScheduleService.createSchedule({
workspace: $workspaceStore!,
requestBody: {
path,
script_path: path,
is_flow: isFlow,
summary: $primarySchedule.summary,
args: $primarySchedule.args,
schedule: $primarySchedule.cron,
timezone: $primarySchedule.timezone,
enabled: $primarySchedule.enabled
}
})
sendUserToast(`Primary schedule created`)
}
}
loadSchedules(true)
await saveSchedule(path, newItem, $workspaceStore ?? '', primarySchedule, isFlow)
updateSchedules(true)
}
</script>
<ScheduleEditor
on:update={() => {
loadSchedules(true)
updateSchedules(true)
}}
bind:this={scheduleEditor}
/>
@@ -204,7 +136,7 @@
<p class="text-xs text-tertiary mt-10">Define a schedule frequency first</p>
{/if}
{#if initialPrimarySchedule != false}
{#if $initialPrimarySchedule != false}
<div class="flex">
<Button size="xs" color="light" on:click={() => scheduleEditor?.openEdit(path, isFlow)}
>Advanced</Button
@@ -213,29 +145,31 @@
{/if}
{:else}
<div class="flex flex-row gap-4 mt-2">
<Button
on:click={() => {
$primarySchedule = {
summary: '',
args: {},
cron: '0 0 */1 * * *',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
enabled: true
}
$triggersCount = {
...($triggersCount ?? {}),
schedule_count: ($triggersCount?.schedule_count ?? 0) + 1,
primary_schedule: { schedule: $primarySchedule.cron }
}
}}
variant="contained"
color="dark"
size="md"
startIcon={{ icon: Calendar }}
>
Set Primary Schedule
</Button>
{#if initialPrimarySchedule != undefined && initialPrimarySchedule != false && !newItem}
<div class="flex items-center">
<Button
on:click={() => {
$primarySchedule = {
summary: '',
args: {},
cron: '0 0 */1 * * *',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
enabled: true
}
$triggersCount = {
...($triggersCount ?? {}),
schedule_count: ($triggersCount?.schedule_count ?? 0) + 1,
primary_schedule: { schedule: $primarySchedule.cron }
}
}}
variant="contained"
color="dark"
size="md"
startIcon={{ icon: Calendar }}
>
Set Primary Schedule
</Button>
</div>
{#if $initialPrimarySchedule != undefined && $initialPrimarySchedule != false && !newItem}
<Button on:click={save} color="dark" size="md" startIcon={{ icon: Save }}>
Apply changes now
</Button>
@@ -274,12 +208,12 @@
{/if}
<Label label="Other schedules">
{#if schedules}
{#if schedules?.length == 0 || schedules == undefined}
{#if $schedules}
{#if $schedules.length == 0 || $schedules == undefined}
<div class="text-xs text-tertiary"> No other schedules </div>
{:else}
<div class="flex flex-col divide-y">
{#each schedules as schedule (schedule.path)}
{#each $schedules as schedule (schedule.path)}
<div class="grid grid-cols-6 text-xs items-center py-2">
<div class="col-span-3 truncate">{schedule.path}</div>
<div class="col-span-2 flex flex-row gap-4 flex-nowrap">
@@ -106,8 +106,9 @@
? { schedule_count: 1, primary_schedule: { schedule: savedPrimarySchedule.cron } }
: undefined
)
const simplifiedPoll = writable(false)
const selectedTriggerStore = writable<
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets'
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets' | 'scheduledPoll'
>('webhooks')
export function setPrimarySchedule(schedule: ScheduleTrigger | undefined | false) {
@@ -138,7 +139,8 @@
setContext<TriggerContext>('TriggerContext', {
selectedTrigger: selectedTriggerStore,
primarySchedule: primaryScheduleStore,
triggersCount
triggersCount,
simplifiedPoll
})
const enterpriseLangs = ['bigquery', 'snowflake', 'mssql']
+4 -2
View File
@@ -20,7 +20,7 @@
export let lightMode: boolean = false
export let eeOnly: boolean = false
export let size: 'sm' | 'xs' = 'sm'
export let size: 'sm' | 'xs' | '2xs' = 'sm'
const dispatch = createEventDispatcher()
const bothOptions = Boolean(options.left) && Boolean(options.right)
@@ -39,7 +39,7 @@
class={twMerge(
'mr-2 font-medium duration-50 select-none',
bothOptions || textDisabled ? (checked ? 'text-disabled' : 'text-primary') : 'text-primary',
size === 'xs' ? 'text-xs' : 'text-sm',
size === 'xs' ? 'text-xs' : size === '2xs' ? 'text-[0.5rem]' : 'text-sm',
textClass
)}
style={textStyle}
@@ -76,6 +76,8 @@
: 'peer-checked:bg-nord-950 dark:peer-checked:bg-nord-400',
size === 'sm'
? 'w-11 h-6 after:top-0.5 after:left-[2px] after:h-5 after:w-5'
: size === '2xs'
? 'w-5 h-3 after:top-0.5 after:left-[2px] after:h-2 after:w-2'
: 'w-7 h-4 after:top-0.5 after:left-[2px] after:h-3 after:w-3'
)}
/>
@@ -265,7 +265,7 @@ Generate a description for the flow below:
</div>
{#if elementType === 'textarea'}
<div>
<div class="flex flex-row-reverse text-2xs text-tertiary -mt-4">GH Markdown</div>
<div class="flex flex-row-reverse !text-3xs text-tertiary -mt-4">GH Markdown</div>
<textarea
bind:this={el}
bind:value={content}
@@ -11,8 +11,10 @@
| 'schedules'
| 'cli'
| 'routes'
| 'websockets' = 'webhooks'
| 'websockets'
| 'scheduledPoll' = 'webhooks'
export let flow_json: any | undefined = undefined
export let simplfiedPoll: boolean = false
export let isOperator: boolean = false
@@ -45,7 +47,7 @@
<slot name="script" />
</TabContent>
<TabContent value="triggers" class="h-full pt-2">
<DetailPageTriggerPanel bind:triggerSelected>
<DetailPageTriggerPanel {simplfiedPoll} bind:triggerSelected>
<slot slot="webhooks" name="webhooks" />
<slot slot="routes" name="routes" />
<slot slot="websockets" name="websockets" />
@@ -20,13 +20,15 @@
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(undefined)
const selectedTriggerStore = writable<
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets'
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets' | 'scheduledPoll'
>('webhooks')
const simplifiedPoll = writable(false)
setContext<TriggerContext>('TriggerContext', {
selectedTrigger: selectedTriggerStore,
primarySchedule: primaryScheduleStore,
triggersCount
triggersCount,
simplifiedPoll
})
</script>
@@ -41,6 +43,7 @@
</Pane>
<Pane size={35} minSize={15}>
<DetailPageDetailPanel
simplfiedPoll={$simplifiedPoll}
bind:triggerSelected={$selectedTriggerStore}
bind:selected
{isOperator}
@@ -83,7 +86,10 @@
<slot name="save_inputs" />
</TabContent>
<TabContent value="triggers" class="flex flex-col flex-1 h-full">
<DetailPageTriggerPanel bind:triggerSelected={$selectedTriggerStore}>
<DetailPageTriggerPanel
simplfiedPoll={$simplifiedPoll}
bind:triggerSelected={$selectedTriggerStore}
>
<slot slot="webhooks" name="webhooks" />
<slot slot="routes" name="routes" />
<slot slot="script" name="script" />
@@ -10,64 +10,70 @@
| 'schedules'
| 'cli'
| 'routes'
| 'websockets' = 'webhooks'
| 'websockets'
| 'scheduledPoll' = 'webhooks'
export let simplfiedPoll: boolean = false
</script>
<HighlightTheme />
<Tabs bind:selected={triggerSelected}>
<Tab value="webhooks">
<span class="flex flex-row gap-2 items-center text-xs">
<Webhook size={12} />
Webhooks
</span>
</Tab>
<Tab value="schedules">
<span class="flex flex-row gap-2 items-center text-xs">
<CalendarCheck2 size={12} />
Schedules
</span>
</Tab>
<Tab value="routes">
<span class="flex flex-row gap-2 items-center text-xs">
<Route size={12} />
HTTP
</span>
</Tab>
<Tab value="websockets">
<span class="flex flex-row gap-2 items-center text-xs">
<Unplug size={12} />
Websockets
</span>
</Tab>
<Tab value="emails">
<span class="flex flex-row gap-2 items-center text-xs">
<MailIcon size={12} />
Email
</span>
</Tab>
<Tab value="cli">
<span class="flex flex-row gap-2 items-center text-xs">
<Terminal size={12} />
CLI
</span>
</Tab>
</Tabs>
{#if !simplfiedPoll}
<Tabs bind:selected={triggerSelected}>
<Tab value="webhooks">
<span class="flex flex-row gap-2 items-center text-xs">
<Webhook size={12} />
Webhooks
</span>
</Tab>
<Tab value="schedules">
<span class="flex flex-row gap-2 items-center text-xs">
<CalendarCheck2 size={12} />
Schedules
</span>
</Tab>
<Tab value="routes">
<span class="flex flex-row gap-2 items-center text-xs">
<Route size={12} />
HTTP
</span>
</Tab>
<Tab value="websockets">
<span class="flex flex-row gap-2 items-center text-xs">
<Unplug size={12} />
Websockets
</span>
</Tab>
<Tab value="emails">
<span class="flex flex-row gap-2 items-center text-xs">
<MailIcon size={12} />
Email
</span>
</Tab>
<Tab value="cli">
<span class="flex flex-row gap-2 items-center text-xs">
<Terminal size={12} />
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 === 'routes'}
<slot name="routes" />
{:else if triggerSelected === 'emails'}
<slot name="emails" />
{:else if triggerSelected === 'schedules'}
<slot name="schedules" />
{:else if triggerSelected === 'websockets'}
<slot name="websockets" />
{:else if triggerSelected === 'cli'}
<slot name="cli" />
{/if}
<div class="h-[calc(100%-32px)]">
<div class="h-full overflow-auto">
{#if triggerSelected === 'webhooks'}
<slot name="webhooks" />
{:else if triggerSelected === 'routes'}
<slot name="routes" />
{:else if triggerSelected === 'emails'}
<slot name="emails" />
{:else if triggerSelected === 'schedules'}
<slot name="schedules" />
{:else if triggerSelected === 'websockets'}
<slot name="websockets" />
{:else if triggerSelected === 'cli'}
<slot name="cli" />
{/if}
</div>
</div>
</div>
{:else}
<slot name="schedules" />
{/if}
@@ -12,15 +12,16 @@
let flow: Flow | undefined = undefined
const selectedTriggerStore = writable<'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes'>(
'webhooks'
)
const selectedTriggerStore = writable<
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'scheduledPoll'
>('webhooks')
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(undefined)
const triggersCount = writable<TriggersCount | undefined>(undefined)
setContext<TriggerContext>('TriggerContext', {
primarySchedule: primaryScheduleStore,
selectedTrigger: selectedTriggerStore,
triggersCount: triggersCount
triggersCount: triggersCount,
simplifiedPoll: writable(false)
})
async function loadFlow(path: string) {
@@ -45,11 +45,12 @@ export async function pickScript(
path: string,
summary: string,
id: string,
hash?: string
hash?: string,
kind?: string
): Promise<[FlowModule & { value: PathScript }, FlowModuleState]> {
const flowModule: FlowModule & { value: PathScript } = {
id,
value: { type: 'script', path, hash, input_transforms: {} },
value: { type: 'script', path, hash, input_transforms: {}, is_trigger: kind === 'trigger' },
summary
}
@@ -82,7 +83,13 @@ export async function createInlineScriptModule(
const flowModule: FlowModule = {
id,
summary,
value: { type: 'rawscript', content: code, language, input_transforms: {} }
value: {
type: 'rawscript',
content: code,
language,
input_transforms: {},
is_trigger: kind === 'trigger'
}
}
return [flowModule, await loadFlowModuleState(flowModule)]
@@ -46,6 +46,7 @@
export let concurrency: boolean = false
export let retries: number | undefined = undefined
export let warningMessage: string | undefined = undefined
export let isTrigger: boolean = false
const { flowInputsStore } = getContext<{ flowInputsStore: Writable<FlowInput | undefined> }>(
'FlowGraphContext'
@@ -131,7 +132,7 @@
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class={classNames(
'w-full module flex rounded-sm cursor-pointer',
'w-full module flex rounded-sm cursor-pointer max-w-full',
selected ? 'outline outline-offset-0 outline-2 outline-slate-500 dark:outline-gray-400' : '',
'flex relative',
$copilotCurrentStepStore === id ? 'z-[901]' : ''
@@ -139,7 +140,7 @@
style="width: 275px; height: 34px; background-color: {bgColor};"
on:mouseenter={() => (hover = true)}
on:mouseleave={() => (hover = false)}
on:click
on:click|preventDefault|stopPropagation
>
<div class="absolute text-sm right-12 -bottom-3 flex flex-row gap-1 z-10">
{#if retry}
@@ -185,7 +186,11 @@
>
<Square size={12} />
</div>
<svelte:fragment slot="text">Early stop/break</svelte:fragment>
<svelte:fragment slot="text"
>{isTrigger
? 'Stop early if there are no new events'
: 'Early stop/break'}</svelte:fragment
>
</Popover>
{/if}
{#if skip}
@@ -33,6 +33,7 @@
import { tutorialInProgress } from '$lib/tutorialUtils'
import FlowGraphV2 from '$lib/components/graph/FlowGraphV2.svelte'
import { replaceId } from '../flowStore'
import { setScheduledPollSchedule, type TriggerContext } from '$lib/components/triggers'
export let modules: FlowModule[] | undefined
export let sidebarSize: number | undefined = undefined
@@ -47,7 +48,7 @@
const { selectedId, moving, history, flowStateStore, flowStore, flowInputsStore, pathStore } =
getContext<FlowEditorContext>('FlowEditorContext')
const { primarySchedule, triggersCount } = getContext<TriggerContext>('TriggerContext')
async function insertNewModuleAtIndex(
modules: FlowModule[],
index: number,
@@ -74,10 +75,17 @@
push(history, $flowStore)
var module = emptyModule($flowStateStore, $flowStore, kind == 'flow')
var state = emptyFlowModuleState()
$flowStateStore[module.id] = state
if (wsFlow) {
;[module, state] = await pickFlow(wsFlow.path, wsFlow.summary, module.id)
} else if (wsScript) {
;[module, state] = await pickScript(wsScript.path, wsScript.summary, module.id, wsScript.hash)
;[module, state] = await pickScript(
wsScript.path,
wsScript.summary,
module.id,
wsScript.hash,
kind
)
} else if (kind == 'forloop') {
;[module, state] = await createLoop(
module.id,
@@ -89,10 +97,7 @@
;[module, state] = await createBranches(module.id)
} else if (kind == 'branchall') {
;[module, state] = await createBranchAll(module.id)
}
$flowStateStore[module.id] = state
if (inlineScript) {
} else if (inlineScript) {
const { language, kind, subkind } = inlineScript
;[module, state] = await createInlineScriptModule(
language,
@@ -108,6 +113,7 @@
module.summary = 'Approval'
}
}
$flowStateStore[module.id] = state
if (kind == 'approval') {
module.suspend = { required_events: 1, timeout: 1800 }
@@ -116,6 +122,9 @@
expr: '!result || (Array.isArray(result) && result.length == 0)',
skip_if_stopped: true
}
} else if (kind == 'end') {
module.summary = 'Terminate flow'
module.stop_after_if = { skip_if_stopped: false, expr: 'true' }
}
if (!modules) return [module]
@@ -403,6 +412,7 @@
undefined
)
setExpr(detail.modules[index + 1], `results.${id}`)
setScheduledPollSchedule(primarySchedule, triggersCount)
}
}
}
@@ -1,6 +1,6 @@
<script lang="ts">
import { createEventDispatcher, getContext } from 'svelte'
import { Cross, Zap } from 'lucide-svelte'
import { Cross } from 'lucide-svelte'
import StepGenQuick from '$lib/components/copilot/StepGenQuick.svelte'
import FlowInputsQuick from '../content/FlowInputsQuick.svelte'
import type { FlowModule } from '$lib/gen'
@@ -11,6 +11,7 @@
import TopLevelNode from '../pickers/TopLevelNode.svelte'
import PopupV2 from '$lib/components/common/popup/PopupV2.svelte'
import { flip, offset } from 'svelte-floating-ui/dom'
import { SchedulePollIcon } from '$lib/components/icons'
// import type { Writable } from 'svelte/store'
@@ -22,6 +23,7 @@
export let disableAi = false
export let kind: 'script' | 'trigger' | 'preprocessor' | 'failure' = 'script'
export let allowTrigger = true
export let iconSize = 12
type Alignment = 'start' | 'end' | 'center'
type Side = 'top' | 'bottom'
@@ -70,22 +72,22 @@ shouldUsePortal={true} -->
id={`flow-editor-add-step-${index}`}
type="button"
class={twMerge(
'w-5 h-5 flex items-center justify-center',
'outline-[1px] outline dark:outline-gray-500 outline-gray-300',
'text-secondary',
'bg-surface focus:outline-none hover:bg-surface-hover rounded'
'w-[17.5px] h-[17.5px] flex items-center justify-center outline-[1px] outline dark:outline-gray-500 outline-gray-300 text-secondary bg-surface focus:outline-none hover:bg-surface-hover rounded',
$$props.class
)}
on:pointerdown|preventDefault|stopPropagation={pointerdown}
on:pointerdown|preventDefault|stopPropagation={() => {
dispatch('open')
pointerdown()
}}
on:pointerup={pointerup}
>
{#if kind === 'trigger'}
<Zap size={12} />
<SchedulePollIcon size={14} />
{:else}
<Cross size={12} />
<Cross size={iconSize} />
{/if}
</button>
</svelte:fragment>
<!-- FOO -->
<div
id="flow-editor-insert-module"
class="flex flex-col h-[400px] {small ? 'w-[450px]' : 'w-[650px]'} pt-1 pr-1 pl-1 gap-1.5"
@@ -109,7 +111,7 @@ shouldUsePortal={true} -->
</div>
<div class="flex flex-row grow min-h-0">
{#if kind === 'script' || kind == 'trigger'}
{#if kind === 'script'}
<div class="flex-none flex flex-col text-xs text-primary">
<TopLevelNode
label="Action"
@@ -11,6 +11,7 @@
import { msToSec } from '$lib/utils'
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
import FlowJobsMenu from './FlowJobsMenu.svelte'
import { isTriggerStep } from '$lib/components/graph/graphBuilder'
export let mod: FlowModule
export let insertable: boolean
@@ -19,6 +20,7 @@
export let modules: FlowModule[]
export let moving: string | undefined = undefined
export let duration_ms: number | undefined = undefined
export let isTrigger: boolean = false
export let retries: number | undefined = undefined
export let flowJobs:
@@ -173,6 +175,7 @@
(mod.value.type === 'rawscript'
? `Inline ${prettyLanguage(mod.value.language)}`
: 'To be defined')}
isTrigger={isTriggerStep(mod)}
>
<div slot="icon">
{#if mod.value.type === 'rawscript'}
@@ -1,9 +1,9 @@
<script lang="ts">
import { Badge } from '$lib/components/common'
import type { FlowModule } from '$lib/gen'
import { classNames } from '$lib/utils'
import { createEventDispatcher, getContext } from 'svelte'
import { getContext } from 'svelte'
import type { FlowCopilotContext } from '$lib/components/copilot/flow'
import VirtualItemWrapper from './VirtualItemWrapper.svelte'
export let label: string | undefined = undefined
export let bgColor: string = ''
@@ -15,41 +15,18 @@
export let hideId: boolean = false
export let preLabel: string | undefined = undefined
const dispatch = createEventDispatcher<{
insert: {
script?: { path: string; summary: string; hash: string | undefined }
detail: 'script' | 'forloop' | 'branchone' | 'branchall' | 'trigger' | 'move'
modules: FlowModule[]
index: number
}
select: string
}>()
const { currentStepStore: copilotCurrentStepStore } =
getContext<FlowCopilotContext | undefined>('FlowCopilotContext') || {}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class={classNames(
'w-full flex relative overflow-hidden rounded-sm',
selectable ? 'cursor-pointer' : '',
selected ? 'outline outline-offset-1 outline-2 outline-gray-600' : '',
label === 'Input' && $copilotCurrentStepStore === 'Input' ? 'z-[901]' : ''
)}
style="width: 275px; max-height: 34px; background-color: {bgColor} !important;"
on:click={() => {
if (selectable) {
if (id) {
dispatch('select', id)
} else {
dispatch('select', label || label || '')
}
}
}}
title={(label ? label + ' ' : '') + (label ?? '')}
id={`flow-editor-virtual-${encodeURIComponent(label || label || '')}`}
<VirtualItemWrapper
{label}
{bgColor}
{selected}
{selectable}
{id}
onTop={label === 'Input' && $copilotCurrentStepStore === 'Input'}
on:select
>
<div
style={borderColor ? `border-color: ${borderColor};` : 'border: 0'}
@@ -76,4 +53,4 @@
{/if}
</div>
</div>
</div>
</VirtualItemWrapper>
@@ -0,0 +1,45 @@
<script lang="ts">
import type { FlowModule } from '$lib/gen'
import { classNames } from '$lib/utils'
import { createEventDispatcher } from 'svelte'
export let label: string | undefined
export let selectable: boolean
export let selected: boolean
export let id: string | undefined
export let onTop: boolean = false
export let bgColor: string
const dispatch = createEventDispatcher<{
insert: {
script?: { path: string; summary: string; hash: string | undefined }
detail: 'script' | 'forloop' | 'branchone' | 'branchall' | 'trigger' | 'move'
modules: FlowModule[]
index: number
}
select: string
}>()
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class={classNames(
'w-full flex relative overflow-hidden rounded-sm',
selectable ? 'cursor-pointer' : '',
selected ? 'outline outline-offset-1 outline-2 outline-gray-600 dark:outline-gray-400' : '',
onTop ? 'z-[901]' : ''
)}
style="width: 275px; max-height: 34px; background-color: {bgColor} !important;"
on:click={() => {
if (selectable) {
if (id) {
dispatch('select', id)
} else {
dispatch('select', label || label || '')
}
}
}}
title={label ? label + ' ' : ''}
id={`flow-editor-virtual-${encodeURIComponent(label || label || '')}`}><slot /></div
>
@@ -1,5 +1,8 @@
import { ScheduleService } from '$lib/gen'
import { ScheduleService, type Schedule, type TriggersCount } from '$lib/gen'
import type { ScheduleTrigger } from '../triggers'
import type { Writable } from 'svelte/store'
import { get } from 'svelte/store'
import { sendUserToast } from '$lib/utils'
// Load the schedule of a flow given its path and the workspace
export async function loadFlowSchedule(path: string, workspace: string): Promise<ScheduleTrigger> {
@@ -25,3 +28,108 @@ export async function loadFlowSchedule(path: string, workspace: string): Promise
args: schedule.args ?? {}
}
}
export async function loadSchedules(
forceRefresh: boolean,
path: string,
isFlow: boolean,
schedules: Writable<Schedule[] | undefined>,
primarySchedule: Writable<ScheduleTrigger | false | undefined>,
initialPrimarySchedule: Writable<ScheduleTrigger | false | undefined>,
workspace: string,
triggersCount: Writable<TriggersCount | undefined>
) {
console.log('loading schedules for path', path)
if (!path || path == '') {
schedules.set([])
primarySchedule.update((ps) => (ps === undefined ? false : ps))
initialPrimarySchedule.set(structuredClone(get(primarySchedule)))
return
}
console.log('loading schedules for path', path)
try {
const allSchedules = await ScheduleService.listSchedules({
workspace,
path: path,
isFlow
})
const primary = allSchedules.find((s) => s.path == path)
let remotePrimarySchedule: ScheduleTrigger | false | undefined = primary
? {
summary: primary.summary,
args: primary.args ?? {},
cron: primary.schedule,
timezone: primary.timezone,
enabled: primary.enabled
}
: false
primarySchedule.update((ps) => (ps === undefined || forceRefresh ? remotePrimarySchedule : ps))
initialPrimarySchedule.set(structuredClone(remotePrimarySchedule))
triggersCount.update((tc) => {
const primary = get(primarySchedule)
return {
...(tc ?? {}),
schedule_count: allSchedules.length,
primary_schedule: primary ? { schedule: primary.cron } : undefined
}
})
schedules.set(allSchedules.filter((s) => s.path != path))
} catch (e) {
console.error('impossible to load schedules', e)
}
}
export async function saveSchedule(
path: string,
newItem: boolean,
workspace: string,
primarySchedule: Writable<ScheduleTrigger | false | undefined>,
isFlow: boolean
) {
const scheduleExists =
path != '' &&
!newItem &&
(await ScheduleService.existsSchedule({
workspace,
path
}))
if (scheduleExists) {
console.log('primary schedule exists')
const primary = get(primarySchedule)
if (primary) {
await ScheduleService.updateSchedule({
workspace,
path,
requestBody: {
summary: primary.summary,
args: primary.args,
schedule: primary.cron,
timezone: primary.timezone
}
})
sendUserToast(`Primary schedule updated`)
} else {
await ScheduleService.deleteSchedule({ workspace, path })
sendUserToast(`Primary schedule deleted`)
}
} else {
const primary = get(primarySchedule)
if (primary) {
await ScheduleService.createSchedule({
workspace,
requestBody: {
path,
script_path: path,
is_flow: isFlow,
summary: primary.summary,
args: primary.args,
schedule: primary.cron,
timezone: primary.timezone,
enabled: primary.enabled
}
})
sendUserToast(`Primary schedule created`)
}
}
}
@@ -1,9 +1,9 @@
<script lang="ts">
import { type FlowModule } from '../../gen'
import { NODE, type GraphModuleState } from '.'
import { createEventDispatcher, onMount, setContext } from 'svelte'
import { createEventDispatcher, getContext, onDestroy, onMount, setContext } from 'svelte'
import { writable, type Writable } from 'svelte/store'
import { get, writable, type Writable } from 'svelte/store'
import '@xyflow/svelte/dist/style.css'
import type { FlowInput } from '../flows/types'
import {
@@ -15,7 +15,7 @@
ControlButton,
type Viewport
} from '@xyflow/svelte'
import graphBuilder from './graphBuilder'
import { graphBuilder, isTriggerStep, type SimplifiableFlow } from './graphBuilder'
import ModuleNode from './renderers/nodes/ModuleNode.svelte'
import InputNode from './renderers/nodes/InputNode.svelte'
import BranchAllStart from './renderers/nodes/BranchAllStart.svelte'
@@ -38,6 +38,8 @@
import Button from '../common/button/Button.svelte'
import FlowYamlEditor from '../flows/header/FlowYamlEditor.svelte'
import BranchOneEndNode from './renderers/nodes/branchOneEndNode.svelte'
import type { TriggerContext } from '../triggers'
export let success: boolean | undefined = undefined
export let modules: FlowModule[] | undefined = []
export let failureModule: FlowModule | undefined = undefined
@@ -72,11 +74,46 @@
useDataflow: Writable<boolean | undefined>
}>('FlowGraphContext', { selectedId, flowInputsStore, useDataflow })
const triggerContext = getContext<TriggerContext>('TriggerContext')
const dispatch = createEventDispatcher()
let fullWidth = 0
let width = 0
export let allowSimplifiedPoll: boolean = true
let simplifiableFlow: SimplifiableFlow | undefined = undefined
if (triggerContext && allowSimplifiedPoll) {
if (isSimplifiable(modules)) {
triggerContext?.simplifiedPoll?.set(true)
}
triggerContext?.simplifiedPoll.subscribe((value) => {
computeSimplifiableFlow(modules ?? [], value ?? false)
})
}
function computeSimplifiableFlow(modules: FlowModule[], simplifiedFlow: boolean) {
const isSimplif = isSimplifiable(modules)
simplifiableFlow = isSimplif ? { simplifiedFlow } : undefined
}
onDestroy(() => {
if (isSimplifiable(modules)) {
triggerContext?.simplifiedPoll?.set(undefined)
}
})
function onModulesChange(modules: FlowModule[]) {
computeSimplifiableFlow(
modules,
triggerContext?.simplifiedPoll ? get(triggerContext.simplifiedPoll) ?? false : false
)
}
$: allowSimplifiedPoll && onModulesChange(modules ?? [])
function layoutNodes(nodes: Node[]): Node[] {
let seenId: string[] = []
for (const n of nodes) {
@@ -129,6 +166,44 @@
return newNodes
}
let eventHandler = {
deleteBranch: (detail, label) => {
$selectedId = label
dispatch('deleteBranch', detail)
},
insert: (detail) => {
dispatch('insert', detail)
},
select: (modId) => {
if (!notSelectable) {
if ($selectedId != modId) {
$selectedId = modId
}
dispatch('select', modId)
}
},
changeId: (detail) => {
dispatch('changeId', detail)
},
delete: (detail, label) => {
$selectedId = label
dispatch('delete', detail)
},
newBranch: (module) => {
dispatch('newBranch', { module })
},
move: (module, modules) => {
dispatch('move', { module, modules })
},
selectedIteration: (detail, moduleId) => {
dispatch('selectedIteration', { ...detail, moduleId: moduleId })
},
simplifyFlow: (detail) => {
triggerContext?.simplifiedPoll.set(detail)
}
}
$: graph = graphBuilder(
modules,
{
@@ -141,50 +216,13 @@
},
failureModule,
preprocessorModule,
{
deleteBranch: (detail, label) => {
$selectedId = label
dispatch('deleteBranch', detail)
},
insert: (detail) => {
dispatch('insert', detail)
},
select: (modId) => {
if (!notSelectable) {
if ($selectedId != modId) {
$selectedId = modId
}
dispatch('select', modId)
}
},
changeId: (detail) => {
dispatch('changeId', detail)
},
delete: (detail, label) => {
$selectedId = label
dispatch('delete', detail)
},
newBranch: (module) => {
dispatch('newBranch', { module })
},
move: (module, modules) => {
dispatch('move', { module, modules })
},
selectedIteration: (detail, moduleId) => {
dispatch('selectedIteration', { ...detail, moduleId: moduleId })
}
},
eventHandler,
success,
$useDataflow,
$selectedId,
moving,
triggerNode
? {
path
}
: undefined
simplifiableFlow,
triggerNode ? path : undefined
)
const nodes = writable<Node[]>([])
@@ -192,18 +230,93 @@
let height = 0
// function removeInputNode(nodes, edges, id) {
// const inputNode = nodes.find((node) => node.id === id)
// if (!inputNode) return { nodes, edges }
// // Find edges connected to the input node
// const connectedEdges = edges.filter((edge) => edge.source === id || edge.target === id)
// // Remove the input node
// let updatedNodes = nodes.filter((node) => node.id !== id)
// // Remove edges connected to the input node
// let updatedEdges = edges.filter((edge) => edge.source !== id && edge.target !== id)
// // Create new edges from the input node's parent to its children
// const inputEdges = connectedEdges.filter((edge) => edge.target === id)
// const outputEdges = connectedEdges.filter((edge) => edge.source === id)
// inputEdges.forEach((inputEdge) => {
// outputEdges.forEach((outputEdge) => {
// const newEdge = {
// id: `edge:${inputEdge.source}->${outputEdge.target}`,
// source: inputEdge.source,
// target: outputEdge.target,
// type: 'empty',
// data: {
// ...outputEdge.data,
// sourceId: inputEdge.source,
// targetId: outputEdge.target
// }
// }
// updatedEdges.push(newEdge)
// })
// })
// // Update parent ids of the nodes
// updatedNodes = updatedNodes.map((node) => {
// if (node.data && node.data.parentIds && node.data.parentIds.includes(id)) {
// const updatedParentIds = node.data.parentIds.filter((parentId) => parentId !== id)
// if (inputNode.data && inputNode.data.parentIds) {
// updatedParentIds.push(...inputNode.data.parentIds)
// }
// return {
// ...node,
// data: {
// ...node.data,
// parentIds: [...new Set(updatedParentIds)] // Remove duplicates
// }
// }
// }
// return node
// })
// return { nodes: updatedNodes, edges: updatedEdges }
// }
// function processGraph(graph, simplifiable) {
// let newGraph = { nodes: graph.nodes, edges: graph.edges }
// newGraph = removeInputNode(newGraph.nodes, newGraph.edges, 'Input')
// newGraph = removeInputNode(newGraph.nodes, newGraph.edges, simplifiable.forLoopNode.id)
// newGraph = removeInputNode(newGraph.nodes, newGraph.edges, simplifiable.triggerNode.id)
// return newGraph
// }
function isSimplifiable(modules: FlowModule[] | undefined): boolean {
if (!modules || modules?.length !== 2) {
return false
}
if (isTriggerStep(modules?.[0])) {
let secondValue = modules?.[1].value
return secondValue.type == 'forloopflow'
}
return false
}
function updateStores() {
if (graph.error) {
return
}
let newGraph = graph
$nodes = layoutNodes(graph?.nodes)
$edges = graph.edges
$nodes = layoutNodes(newGraph.nodes)
$edges = newGraph.edges
height = Math.max(...$nodes.map((n) => n.position.y + NODE.height + 40), minHeight)
}
$: graph && updateStores()
$: (graph || allowSimplifiedPoll) && updateStores()
const nodeTypes = {
input2: InputNode,
@@ -14,9 +14,19 @@ export type GraphEventHandlers = {
move: (module: FlowModule, modules: FlowModule[]) => void
selectedIteration: (detail, moduleId: string) => void
changeId: (newId: string) => void
simplifyFlow: (detail: boolean) => void
}
export default function graphBuilder(
export type SimplifiableFlow = { simplifiedFlow: boolean }
export function isTriggerStep(module: FlowModule | undefined): boolean {
return (
module?.value != undefined &&
(module.value.type === 'script' || module.value.type === 'rawscript') &&
module.value.is_trigger === true
)
}
export function graphBuilder(
modules: FlowModule[] | undefined,
extra: Record<string, any>,
failureModule: FlowModule | undefined,
@@ -26,9 +36,12 @@ export default function graphBuilder(
useDataflow: boolean | undefined,
selectedId: string | undefined,
moving: string | undefined,
triggerProps?: {
path?: string
}
simplifiableFlow: SimplifiableFlow | undefined,
flowPathForTriggerNode: string | undefined
// triggerProps?: {
// path?: string
// flowIsSimplifiable?: boolean
// }
): {
nodes: Node[]
edges: Edge[]
@@ -102,6 +115,7 @@ export default function graphBuilder(
sourceId: string,
targetId: string,
options?: {
disableInsert?: boolean
customId?: string
type?: string
subModules?: FlowModule[]
@@ -129,17 +143,18 @@ export default function graphBuilder(
target: targetId,
type: options?.type ?? 'edge',
data: {
insertable: extra.insertable,
modules: options?.subModules ?? modules,
sourceId,
targetId,
moving,
eventHandlers,
simplifiedTriggerView: simplifiableFlow?.simplifiedFlow,
disableMoveIds: options?.disableMoveIds,
enableTrigger: sourceId === 'Input',
// If the index is -1, it means that the target module is not in the modules array, so we set it to the length of the array
index: index >= 0 ? index : mods?.length ?? 0,
...extra
...extra,
insertable: extra.insertable && !options?.disableInsert
}
})
}
@@ -156,15 +171,19 @@ export default function graphBuilder(
}
}
if (extra.path && triggerProps != undefined) {
const triggerNode: Node = {
let triggerNode: Node | undefined = undefined
if (flowPathForTriggerNode) {
triggerNode = {
id: 'Trigger',
position: { x: -1, y: -1 },
type: 'trigger',
data: {
path: triggerProps?.path,
simplifiableFlow: simplifiableFlow,
path: flowPathForTriggerNode,
newFlow: extra.newFlow,
eventHandlers: eventHandlers
eventHandlers: eventHandlers,
modules: modules,
isEditor: extra.insertable
}
}
nodes.push(triggerNode)
@@ -191,13 +210,16 @@ export default function graphBuilder(
type: 'result'
}
nodes.push(inputNode)
nodes.push(resultNode)
if (simplifiableFlow?.simplifiedFlow !== true) {
nodes.push(inputNode)
nodes.push(resultNode)
}
function processModules(
modules: FlowModule[],
beforeNode: Node,
nextNode: Node,
nextNode: Node | undefined,
simplifiedTriggerView: boolean,
currentOffset = 0,
disableMoveIds: string[] = [],
parentIndex?: string,
@@ -206,10 +228,12 @@ export default function graphBuilder(
let previousId: string | undefined = undefined
if (modules.length === 0) {
addEdge(beforeNode.id, nextNode.id, {
subModules: modules,
disableMoveIds
})
if (nextNode) {
addEdge(beforeNode.id, nextNode.id, {
subModules: modules,
disableMoveIds
})
}
} else {
modules.forEach((module, index) => {
const localDisableMoveIds = [...disableMoveIds, module.id]
@@ -295,6 +319,7 @@ export default function graphBuilder(
branch.modules,
startNode,
endNode,
false,
currentOffset,
localDisableMoveIds,
parentIndex ? `${parentIndex}-${index}-${branchIndex}` : `${index}-${branchIndex}`
@@ -304,7 +329,9 @@ export default function graphBuilder(
previousId = endNode.id
} else if (module.value.type === 'forloopflow') {
addNode(module, currentOffset, 'module', modules)
if (!simplifiedTriggerView) {
addNode(module, currentOffset, 'module', modules)
}
const startNode = {
id: `${module.id}-start`,
@@ -313,6 +340,7 @@ export default function graphBuilder(
id: module.id,
module: module,
modules: modules,
simplifiedTriggerView,
eventHandlers: eventHandlers,
...extra
},
@@ -320,9 +348,15 @@ export default function graphBuilder(
type: 'forLoopStart'
}
addEdge(module.id, startNode.id, {
type: 'empty'
})
if (!simplifiedTriggerView) {
addEdge(module.id, startNode.id, {
type: 'empty'
})
} else if (previousId) {
addEdge(previousId, startNode.id, {
type: 'empty'
})
}
const endNode = {
id: `${module.id}-end`,
@@ -332,6 +366,7 @@ export default function graphBuilder(
module: module,
modules: modules,
eventHandlers: eventHandlers,
simplifiedTriggerView,
...extra
},
position: { x: -1, y: -1 },
@@ -347,6 +382,7 @@ export default function graphBuilder(
module.value.modules,
startNode,
endNode,
false,
currentOffset + 25,
localDisableMoveIds,
parentIndex
@@ -390,6 +426,7 @@ export default function graphBuilder(
module.value.modules,
startNode,
endNode,
false,
currentOffset + 25,
localDisableMoveIds,
parentIndex
@@ -435,6 +472,7 @@ export default function graphBuilder(
module.value.default,
defaultBranch,
endNode,
false,
currentOffset,
localDisableMoveIds,
parentIndex ? `${parentIndex}-${index}` : index.toString(),
@@ -468,6 +506,7 @@ export default function graphBuilder(
branch.modules,
startNode,
endNode,
false,
currentOffset,
localDisableMoveIds,
parentIndex ? `${parentIndex}-${index}` : index.toString(),
@@ -480,30 +519,31 @@ export default function graphBuilder(
addNode(module, currentOffset, 'module', modules)
previousId = module.id
}
if (index === 0) {
addEdge(beforeNode.id, module.id, {
subModules: modules,
disableMoveIds
disableMoveIds,
disableInsert: simplifiedTriggerView
})
}
if (index === modules.length - 1 && previousId) {
if (index === modules.length - 1 && previousId && nextNode) {
addEdge(previousId, nextNode.id, {
subModules: modules,
disableMoveIds
})
}
})
}
}
processModules(modules, inputNode, resultNode)
if (simplifiableFlow?.simplifiedFlow === true && triggerNode) {
processModules(modules, triggerNode, undefined, true)
} else {
processModules(modules, inputNode, resultNode, false)
}
if (failureModule) {
let toAdd: Record<string, string> = {}
@@ -86,45 +86,6 @@
}}
/>
</div>
{#if data.enableTrigger}
<div
class="edgeButtonContainer nodrag nopan"
style:transform="translate(100%, 50%) translate({sourceX}px,{sourceY + 2}px)"
>
<InsertModuleButton
disableAi={data.disableAi}
on:new={(e) => {
// console.log('new', e)
data?.eventHandlers.insert({
modules: data.modules,
index: data.index,
kind: e.detail.kind,
inlineScript: e.detail.inlineScript
})
}}
on:pickScript={(e) => {
// console.log('pickScript', e)
data?.eventHandlers.insert({
modules: data.modules,
index: data.index,
script: e.detail,
kind: e.detail.kind
})
}}
on:pickFlow={(e) => {
// console.log('pickFlow', e)
data?.eventHandlers.insert({
modules: data.modules,
index: data.index,
flow: e.detail
})
}}
kind="trigger"
index={data?.index ?? 0}
modules={data?.modules ?? []}
/>
</div>
{/if}
{/if}
{#if data?.moving}
@@ -11,19 +11,35 @@
insertable: boolean
flowModuleStates: Record<string, GraphModuleState> | undefined
eventHandlers: GraphEventHandlers
simplifiedTriggerView: boolean
}
</script>
<NodeWrapper offset={data.offset} let:darkMode>
<VirtualItem
label={'Collect result of each iteration'}
selectable={true}
selected={false}
id={data.id}
bgColor={getStateColor(undefined, darkMode)}
borderColor={getStateColor(data.flowModuleStates?.[data.id]?.type, darkMode)}
on:select={(e) => {
data?.eventHandlers?.select(e.detail)
}}
/>
{#if data.simplifiedTriggerView}
<VirtualItem
label={'Each event is processed'}
selectable={false}
selected={false}
id={data.id}
hideId
bgColor={getStateColor(undefined, darkMode)}
borderColor={getStateColor(data.flowModuleStates?.[data.id]?.type, darkMode)}
on:select={(e) => {
data?.eventHandlers?.select(e.detail)
}}
/>
{:else}
<VirtualItem
label={'Collect result of each iteration'}
selectable={true}
selected={false}
id={data.id}
bgColor={getStateColor(undefined, darkMode)}
borderColor={getStateColor(data.flowModuleStates?.[data.id]?.type, darkMode)}
on:select={(e) => {
data?.eventHandlers?.select(e.detail)
}}
/>
{/if}
</NodeWrapper>
@@ -12,12 +12,13 @@
modules: FlowModule[]
flowModuleStates: Record<string, GraphModuleState> | undefined
eventHandlers: GraphEventHandlers
simplifiedTriggerView: boolean
}
</script>
<NodeWrapper let:darkMode offset={data.offset}>
<VirtualItem
label={'Do one iteration'}
label={data.simplifiedTriggerView ? 'For each new event' : 'Do one iteration'}
selectable={false}
selected={false}
id={data.id}
@@ -15,7 +15,6 @@
moving: string | undefined
eventHandlers: GraphEventHandlers
index: number
enableTrigger: boolean
disableAi: boolean
disableMoveIds: string[]
}
@@ -50,6 +49,8 @@
detail: 'preprocessor'
})
}}
class="w-[14px] h-[14px]"
iconSize={10}
/>
</div>
{/if}
@@ -1,9 +1,14 @@
<script lang="ts">
import NodeWrapper from './NodeWrapper.svelte'
import TriggersWrapper from '../triggers/TriggersWrapper.svelte'
import { type GraphEventHandlers } from '../../graphBuilder'
import { type GraphEventHandlers, type SimplifiableFlow } from '../../graphBuilder'
import type { FlowModule } from '$lib/gen'
import { getContext } from 'svelte'
import type { Writable } from 'svelte/store'
import { Maximize2, Minimize2, Calendar } from 'lucide-svelte'
import { getStateColor } from '../../util'
import { setScheduledPollSchedule, type TriggerContext } from '$lib/components/triggers'
import VirtualItemWrapper from '$lib/components/flows/map/VirtualItemWrapper.svelte'
export let data: {
path: string
@@ -11,21 +16,107 @@
newFlow: boolean
extra_perms: Record<string, any>
eventHandlers: GraphEventHandlers
modules: FlowModule[]
index: number
disableAi: boolean
simplifiableFlow: SimplifiableFlow
}
const { selectedId } = getContext<{
selectedId: Writable<string | undefined>
}>('FlowGraphContext')
const { primarySchedule, triggersCount, selectedTrigger } =
getContext<TriggerContext>('TriggerContext')
</script>
<NodeWrapper wrapperClass="shadow-none">
<TriggersWrapper
path={data.path}
on:select={() => {
data?.eventHandlers?.select('triggers')
}}
isFlow={true}
selected={$selectedId == 'triggers'}
newItem={data.newFlow}
/>
<NodeWrapper wrapperClass="shadow-md" let:darkMode>
{#if data.simplifiableFlow?.simplifiedFlow != true}
<TriggersWrapper
disableAi={data.disableAi}
isEditor={data.isEditor}
path={data.path}
bgColor={getStateColor(undefined, darkMode)}
on:new={(e) => {
data?.eventHandlers.insert({
modules: data.modules,
index: 0,
kind: 'trigger',
inlineScript: e.detail.inlineScript
})
data?.eventHandlers?.simplifyFlow(true)
}}
on:pickScript={(e) => {
data?.eventHandlers.insert({
modules: data.modules,
index: 0,
kind: 'trigger',
script: e.detail
})
data?.eventHandlers?.simplifyFlow(true)
}}
on:openScheduledPoll={(e) => {
$selectedTrigger = 'scheduledPoll'
}}
on:select={(e) => {
data?.eventHandlers?.select('triggers')
}}
on:delete={(e) => {
data.eventHandlers.delete(e, '')
}}
selected={$selectedId == 'triggers'}
newItem={data.newFlow}
modules={data.modules}
/>
{:else}
<VirtualItemWrapper
label="Check for new events"
selectable={true}
selected={$selectedId == 'triggers'}
id={'triggers'}
bgColor={getStateColor(undefined, darkMode)}
on:select={(e) => {
data?.eventHandlers?.select(e.detail)
}}
>
{#if $primarySchedule || ($primarySchedule == undefined && $triggersCount?.primary_schedule?.schedule)}
<div class="text-2xs text-primary p-2 flex gap-2 items-center">
<Calendar size={12} />
<div>
Schedule every {$primarySchedule?.cron ?? $triggersCount?.primary_schedule?.schedule}
{$primarySchedule?.enabled ||
($primarySchedule == undefined && $triggersCount?.primary_schedule?.schedule)
? ''
: ' (disabled)'}
</div>
</div>
{:else}
<button
class="px-2 py-1 hover:bg-surface-inverse w-full hover:text-primary-inverse"
on:click={() => {
setScheduledPollSchedule(primarySchedule, triggersCount)
}}
>
Set primary schedule
</button>
{/if}
</VirtualItemWrapper>
{/if}
{#if data.simplifiableFlow != undefined}
<button
class="absolute -top-[10px] -right-[10px] rounded-full h-[20px] w-[20px] trash center-center text-secondary
outline-[1px] outline dark:outline-gray-500 outline-gray-300 bg-surface duration-150 hover:bg-nord-950 hover:text-white"
on:click|preventDefault|stopPropagation={() =>
data?.eventHandlers?.simplifyFlow(!data.simplifiableFlow?.simplifiedFlow)}
title={data.simplifiableFlow?.simplifiedFlow
? 'Expand to full flow view'
: 'Simplify flow view for scheduled poll'}
>
{#if data.simplifiableFlow?.simplifiedFlow}
<Maximize2 size={12} strokeWidth={2} />
{:else}
<Minimize2 size={12} strokeWidth={2} />
{/if}
</button>
{/if}
</NodeWrapper>
@@ -7,7 +7,7 @@
<button
on:click
class={twMerge(
'hover:bg-surface-hover rounded-md border text-xs w-6 h-6 relative center-center cursor-pointer bg-surface',
'hover:bg-surface-hover rounded-md border text-xs w-[23px] h-[23px] relative center-center cursor-pointer bg-surface',
selected ? 'outline-1 outline-tertiary outline' : 'outline-0'
)}
>
@@ -18,6 +18,8 @@
export let isFlow: boolean
export let selected: boolean
export let showOnlyWithCount: boolean
export let triggersToDisplay: ('webhooks' | 'schedules' | 'routes' | 'websockets' | 'emails')[] =
['webhooks', 'schedules', 'routes', 'websockets', 'emails']
const dispatch = createEventDispatcher()
onMount(() => {
@@ -39,84 +41,31 @@
})
}
}
const triggerTypeConfig = {
webhooks: { icon: Webhook, countKey: 'webhook_count' },
schedules: { icon: Calendar, countKey: 'schedule_count' },
routes: { icon: Route, countKey: 'http_routes_count' },
websockets: { icon: Unplug, countKey: 'websocket_count' },
emails: { icon: Mail, countKey: 'email_count' }
}
</script>
{#if !showOnlyWithCount || ($triggersCount?.webhook_count ?? 0) > 0}
<Popover>
<svelte:fragment slot="text">Webhooks</svelte:fragment>
<TriggerButton
on:click={() => {
$selectedTrigger = 'webhooks'
dispatch('select')
}}
selected={selected && $selectedTrigger === 'webhooks'}
>
<TriggerCount count={$triggersCount?.webhook_count} />
<Webhook size={12} />
</TriggerButton>
</Popover>
{/if}
{#if !showOnlyWithCount || ($triggersCount?.schedule_count ?? 0) > 0}
<Popover>
<svelte:fragment slot="text">Schedules</svelte:fragment>
<TriggerButton
on:click={() => {
$selectedTrigger = 'schedules'
dispatch('select')
}}
selected={selected && $selectedTrigger === 'schedules'}
>
<TriggerCount count={$triggersCount?.schedule_count} />
<Calendar size={12} />
</TriggerButton>
</Popover>
{/if}
{#if !showOnlyWithCount || ($triggersCount?.http_routes_count ?? 0) > 0}
<Popover>
<svelte:fragment slot="text">HTTP Routes</svelte:fragment>
<TriggerButton
on:click={() => {
$selectedTrigger = 'routes'
dispatch('select')
}}
selected={selected && $selectedTrigger === 'routes'}
>
<TriggerCount count={$triggersCount?.http_routes_count} />
<Route size={12} />
</TriggerButton>
</Popover>
{/if}
{#if !showOnlyWithCount || ($triggersCount?.websocket_count ?? 0) > 0}
<Popover>
<svelte:fragment slot="text">Websockets</svelte:fragment>
<TriggerButton
on:click={() => {
$selectedTrigger = 'websockets'
dispatch('select')
}}
selected={selected && $selectedTrigger === 'websockets'}
>
<TriggerCount count={$triggersCount?.websocket_count} />
<Unplug size={12} />
</TriggerButton>
</Popover>
{/if}
{#if !showOnlyWithCount || ($triggersCount?.email_count ?? 0) > 0}
<Popover>
<svelte:fragment slot="text">Emails</svelte:fragment>
<TriggerButton
on:click={() => {
$selectedTrigger = 'emails'
dispatch('select')
}}
selected={selected && $selectedTrigger === 'emails'}
>
<TriggerCount count={$triggersCount?.email_count} />
<Mail size={12} />
</TriggerButton>
</Popover>
{/if}
{#each triggersToDisplay as type}
{@const { icon, countKey } = triggerTypeConfig[type]}
{#if !showOnlyWithCount || ($triggersCount?.[countKey] ?? 0) > 0}
<Popover>
<svelte:fragment slot="text">{type.charAt(0).toUpperCase() + type.slice(1)}</svelte:fragment>
<TriggerButton
on:click={() => {
$selectedTrigger = type
dispatch('select')
}}
selected={selected && $selectedTrigger === type}
>
<TriggerCount count={$triggersCount?.[countKey]} />
<svelte:component this={icon} size={12} />
</TriggerButton>
</Popover>
{/if}
{/each}
@@ -4,29 +4,51 @@
import { createEventDispatcher } from 'svelte'
import TriggersBadge from './TriggersBadge.svelte'
import InsertModuleButton from '$lib/components/flows/map/InsertModuleButton.svelte'
import type { FlowModule } from '$lib/gen'
import { twMerge } from 'tailwind-merge'
export let path: string
export let newItem: boolean
export let isFlow: boolean
export let selected: boolean
export let isEditor: boolean = false
export let disableAi: boolean = false
export let modules: FlowModule[] = []
export let bgColor: string
const dispatch = createEventDispatcher()
</script>
<div style={`width: ${NODE.width}px;`}>
<div class="flex flex-row mx-auto w-min">
<button
class="flex flex-row gap-2 px-2 border p-1 rounded-md bg-surface shadow-md items-center {selected
? 'outline outline-offset-1 outline-1 outline-slate-900 dark:bg-white/5 dark:outline-slate-800/60 dark:border-gray-400'
: ''}"
on:click={() => {
dispatch('select')
}}
>
<div class="flex flex-col">
<div class="flex flex-row items-center text-2xs font-normal"> Triggers </div>
</div>
<TriggersBadge showOnlyWithCount={false} {path} {newItem} {isFlow} {selected} on:select />
</button>
</div>
<button
style="background-color: {bgColor} !important;"
class="flex w-full flex-row gap-2 px-2 p-1 items-center {selected
? 'outline outline-2 outline-gray-600 rounded-sm dark:bg-white/5 dark:outline-gray-400'
: ''}"
on:click={() => {
dispatch('select')
}}
>
<div class="flex flex-col mr-1 ml-1">
<div class="flex flex-row items-center text-2xs font-normal"> Triggers </div>
</div>
<TriggersBadge showOnlyWithCount={false} {path} {newItem} isFlow {selected} on:select />
{#if isEditor}
<InsertModuleButton
{disableAi}
on:new
on:pickScript
on:select
on:open={() => {
dispatch('openScheduledPoll')
}}
kind="trigger"
index={0}
{modules}
class={twMerge(
'hover:bg-surface-hover rounded-md border text-xs w-[23px] h-[23px] relative center-center cursor-pointer bg-surface outline-0'
)}
/>
{/if}
</button>
</div>
@@ -0,0 +1,36 @@
<script>
// You can pass props for flexibility (e.g., size, color)
export let size = 24
</script>
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M4.96046 2.04913L6.96046 2.04913" />
<path d="M5.96046 8.94553L4.96046 7.94553" />
<path d="M13.1611 13.8974L17.2173 13.8974" />
<path
d="M20.4622 12.2749V9.84124C20.4622 9.62608 20.3767 9.41975 20.2246 9.26761C20.0725 9.11547 19.8661 9.03001 19.651 9.03001H18.0285C17.8134 9.03001 17.607 9.11547 17.4549 9.26761C17.3028 9.41975 17.2173 9.62608 17.2173 9.84124V12.2749"
/>
<path
d="M20.8678 22.0097C21.1906 22.0097 21.5001 21.8632 21.7283 21.6024C21.9565 21.3415 22.0847 20.9878 22.0847 20.619V17.9412C22.0847 16.9747 20.8678 15.8816 20.8678 14.5834V12.9703C20.8678 12.7858 20.8037 12.609 20.6896 12.4786C20.5755 12.3482 20.4208 12.2749 20.2594 12.2749H17.8257C17.6644 12.2749 17.5096 12.3482 17.3955 12.4786C17.2814 12.609 17.2173 12.7858 17.2173 12.9703V20.619C17.2173 20.9878 17.3455 21.3415 17.5737 21.6024C17.8019 21.8632 18.1114 22.0097 18.4341 22.0097H20.8678Z"
/>
<path d="M22.0847 18.7648H8.29377" />
<path
d="M9.51061 22.0097C9.18789 22.0097 8.87838 21.8632 8.65018 21.6024C8.42197 21.3415 8.29377 20.9878 8.29377 20.619V17.9412C8.29377 16.9747 9.51061 15.8816 9.51061 14.5834V12.9703C9.51061 12.7858 9.57472 12.609 9.68882 12.4786C9.80292 12.3482 9.95767 12.2749 10.119 12.2749H12.5527C12.7141 12.2749 12.8688 12.3482 12.9829 12.4786C13.097 12.609 13.1611 12.7858 13.1611 12.9703V20.619C13.1611 20.9878 13.0329 21.3415 12.8047 21.6024C12.5765 21.8632 12.267 22.0097 11.9443 22.0097H9.51061Z"
/>
<path
d="M13.1611 12.2749V9.84124C13.1611 9.62608 13.0757 9.41975 12.9235 9.26761C12.7714 9.11547 12.5651 9.03001 12.3499 9.03001H10.7275C10.5123 9.03001 10.306 9.11547 10.1538 9.26761C10.0017 9.41975 9.91622 9.62608 9.91622 9.84124V12.2749"
/>
<path
d="M8.78889 6.11711C8.30848 5.63669 7.71362 5.28656 7.0604 5.09974C6.40719 4.91291 5.71715 4.89555 5.05536 5.04928C4.39358 5.20301 3.78186 5.52278 3.27789 5.97842C2.77392 6.43406 2.39431 7.01056 2.17486 7.65355C1.95542 8.29654 1.90337 8.98483 2.02362 9.65351C2.14387 10.3222 2.43246 10.9492 2.86217 11.4755C3.29188 12.0017 3.84856 12.4098 4.47969 12.6614C5.11083 12.9129 5.79563 12.9995 6.46951 12.913"
/>
</svg>
@@ -19,6 +19,7 @@ import Slack from './Slack.svelte'
import TogglIcon from './TogglIcon.svelte'
import WindmillIcon from './WindmillIcon.svelte'
import WindmillIcon2 from './WindmillIcon2.svelte'
import SchedulePollIcon from './SchedulePollIcon.svelte'
import MailchimpIcon from './MailchimpIcon.svelte'
import SendgridIcon from './SendgridIcon.svelte'
import SendflakeIcon from './SendflakeIcon.svelte'
@@ -215,6 +216,7 @@ export {
TogglIcon,
WindmillIcon,
WindmillIcon2,
SchedulePollIcon,
MailchimpIcon,
SendgridIcon,
LinkedinIcon,
+25 -1
View File
@@ -10,7 +10,31 @@ export type ScheduleTrigger = {
}
export type TriggerContext = {
selectedTrigger: Writable<'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets'>
selectedTrigger: Writable<
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets' | 'scheduledPoll'
>
primarySchedule: Writable<ScheduleTrigger | undefined | false>
triggersCount: Writable<TriggersCount | undefined>
simplifiedPoll: Writable<boolean | undefined>
}
export function setScheduledPollSchedule(
primarySchedule: Writable<ScheduleTrigger | undefined | false>,
triggersCount: Writable<TriggersCount | undefined>
) {
const cron = '0 */5 * * * *'
primarySchedule.set({
enabled: true,
summary: 'Check for new events every 5 minutes',
cron: cron,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
args: {}
})
triggersCount.update((triggersCount) => {
return {
...(triggersCount ?? {}),
schedule_count: (triggersCount?.schedule_count ?? 0) + 1,
primary_schedule: { schedule: cron }
}
})
}
@@ -0,0 +1,41 @@
<script lang="ts">
import Alert from '$lib/components/common/alert/Alert.svelte'
import Label from '$lib/components/Label.svelte'
import { base } from '$lib/base'
$: console.log('dbg base', base)
</script>
<div class="flex flex-col w-full gap-4 text-sm">
<Alert
title="Scheduled Poll"
size="xs"
documentationLink="https://www.windmill.dev/docs/flows/flow_trigger"
>
Sheduled Poll is a trigger that polls an external API at a given interval. It is useful for
triggering jobs based on the change of external states. Under the hood, it is a script triggered
by a schedule, and returning a list of the changed states so we can iterate over them.
</Alert>
<Label label="Setting a scheduled poll" primary>
<ul class="pt-4 space-y-2 text-sm">
<li>
1. Chose a trigger script using the script picker that will poll for new events regularly <img
class="shadow-lg rounded"
alt="script picker"
src="{base}/script-picker.png"
width="400"
/>
</li>
<li>
2. Add logic to process each new event <img
class="shadow-lg rounded"
alt="create action"
src="{base}/create_action.png"
width="500"
/>
</li>
</ul>
</Label>
</div>
@@ -11,6 +11,7 @@
import { getContext } from 'svelte'
import type { TriggerContext } from '$lib/components/triggers'
import WebsocketTriggersPanel from './WebsocketTriggersPanel.svelte'
import ScheduledPollPanel from './ScheduledPollPanel.svelte'
export let noEditor: boolean
export let newItem = false
@@ -19,67 +20,88 @@
export let schema: any
export let isFlow: boolean
const { selectedTrigger } = getContext<TriggerContext>('TriggerContext')
const { selectedTrigger, simplifiedPoll } = getContext<TriggerContext>('TriggerContext')
</script>
<FlowCard {noEditor} title="Triggers">
<div class="pt-4">
<Tabs bind:selected={$selectedTrigger}>
<Tab value="webhooks" selectedClass="text-primary font-semibold">Webhooks</Tab>
<Tab value="schedules" selectedClass="text-primary text-sm font-semibold">Schedules</Tab>
<Tab value="routes" selectedClass="text-primary text-sm font-semibold">Routes</Tab>
<Tab value="websockets" selectedClass="text-primary text-sm font-semibold">Websockets</Tab>
<Tab value="emails" selectedClass="text-primary text-sm font-semibold">Email</Tab>
{#if !$simplifiedPoll}
<Tabs bind:selected={$selectedTrigger}>
<Tab value="webhooks" selectedClass="text-primary font-semibold">Webhooks</Tab>
<Tab value="schedules" selectedClass="text-primary text-sm font-semibold">Schedules</Tab>
<Tab value="routes" selectedClass="text-primary text-sm font-semibold">Routes</Tab>
<Tab value="websockets" selectedClass="text-primary text-sm font-semibold">Websockets</Tab>
<Tab value="emails" selectedClass="text-primary text-sm font-semibold">Email</Tab>
<Tab value="scheduledPoll" selectedClass="text-primary text-sm font-semibold"
>Scheduled Poll</Tab
>
<svelte:fragment slot="content">
{#if $selectedTrigger === 'webhooks'}
<div class="p-4">
<WebhooksPanel
scopes={isFlow ? [`run:flow/${currentPath}`] : [`run:script/${currentPath}`]}
path={currentPath}
{isFlow}
args={{}}
token=""
{newItem}
/>
</div>
{/if}
<svelte:fragment slot="content">
{#if $selectedTrigger === 'webhooks'}
<div class="p-4">
<WebhooksPanel
scopes={isFlow ? [`run:flow/${currentPath}`] : [`run:script/${currentPath}`]}
path={currentPath}
{isFlow}
args={{}}
token=""
{newItem}
/>
</div>
{/if}
{#if $selectedTrigger === 'emails'}
<div class="p-4">
<EmailTriggerPanel
token=""
scopes={isFlow ? [`run:flow/${currentPath}`] : [`run:script/${currentPath}`]}
path={currentPath}
{isFlow}
/>
</div>
{/if}
{#if $selectedTrigger === 'emails'}
<div class="p-4">
<EmailTriggerPanel
token=""
scopes={isFlow ? [`run:flow/${currentPath}`] : [`run:script/${currentPath}`]}
path={currentPath}
{isFlow}
/>
</div>
{/if}
{#if $selectedTrigger === 'routes'}
<div class="p-4">
<RoutesPanel {newItem} path={currentPath} {isFlow} />
</div>
{/if}
{#if $selectedTrigger === 'routes'}
<div class="p-4">
<RoutesPanel {newItem} path={currentPath} {isFlow} />
</div>
{/if}
{#if $selectedTrigger === 'websockets'}
<div class="p-4">
<WebsocketTriggersPanel {newItem} path={currentPath} {isFlow} />
</div>
{/if}
{#if $selectedTrigger === 'websockets'}
<div class="p-4">
<WebsocketTriggersPanel {newItem} path={currentPath} {isFlow} />
</div>
{/if}
{#if $selectedTrigger === 'schedules'}
<div class="p-2">
<RunPageSchedules
{schema}
{isFlow}
path={initialPath}
{newItem}
can_write={canWrite(currentPath, {}, $userStore)}
/>
</div>
{/if}
</svelte:fragment>
</Tabs>
{#if $selectedTrigger === 'schedules'}
<div class="p-4">
<RunPageSchedules
{schema}
{isFlow}
path={initialPath}
{newItem}
can_write={canWrite(currentPath, {}, $userStore)}
/>
</div>
{/if}
{#if $selectedTrigger === 'scheduledPoll'}
<div class="p-4">
<ScheduledPollPanel />
</div>
{/if}
</svelte:fragment>
</Tabs>
{:else}
<div class="px-4 pb-2">
<RunPageSchedules
{schema}
{isFlow}
path={initialPath}
{newItem}
can_write={canWrite(currentPath, {}, $userStore)}
/>
</div>
{/if}
</div>
</FlowCard>
+3 -2
View File
@@ -74,12 +74,13 @@
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(undefined)
const triggersCount = writable<TriggersCount | undefined>(undefined)
const selectedTriggerStore = writable<
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets'
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets' | 'scheduledPoll'
>('webhooks')
setContext<TriggerContext>('TriggerContext', {
primarySchedule: primaryScheduleStore,
selectedTrigger: selectedTriggerStore,
triggersCount: triggersCount
triggersCount: triggersCount,
simplifiedPoll: writable(false)
})
setContext<FlowEditorContext>('FlowEditorContext', {
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

+2 -1
View File
@@ -463,7 +463,8 @@ const config = {
height: 'height'
},
fontSize: {
'2xs': '0.7rem'
'2xs': '0.7rem',
'3xs': '0.65rem'
},
screens: {
fhd: '1900px',
+4
View File
@@ -269,6 +269,8 @@ components:
type: number
custom_concurrency_key:
type: string
is_trigger:
type: boolean
required:
- type
- content
@@ -292,6 +294,8 @@ components:
- script
tag_override:
type: string
is_trigger:
type: boolean
required:
- type
- path