mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-04 16:03:06 +00:00
feat: step mocking for flows
This commit is contained in:
+16
-7
@@ -1015,7 +1015,8 @@ async fn test_deno_flow(db: Pool<Postgres>) {
|
||||
suspend: Default::default(),
|
||||
retry: None,
|
||||
sleep: None,
|
||||
cache_ttl: None
|
||||
cache_ttl: None,
|
||||
mock: None,
|
||||
},
|
||||
FlowModule {
|
||||
id: "b".to_string(),
|
||||
@@ -1044,7 +1045,8 @@ async fn test_deno_flow(db: Pool<Postgres>) {
|
||||
suspend: Default::default(),
|
||||
retry: None,
|
||||
sleep: None,
|
||||
cache_ttl: None
|
||||
cache_ttl: None,
|
||||
mock: None,
|
||||
}],
|
||||
},
|
||||
stop_after_if: Default::default(),
|
||||
@@ -1052,7 +1054,8 @@ async fn test_deno_flow(db: Pool<Postgres>) {
|
||||
suspend: Default::default(),
|
||||
retry: None,
|
||||
sleep: None,
|
||||
cache_ttl: None
|
||||
cache_ttl: None,
|
||||
mock: None,
|
||||
},
|
||||
],
|
||||
same_worker: false,
|
||||
@@ -1147,7 +1150,8 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
|
||||
suspend: Default::default(),
|
||||
retry: None,
|
||||
sleep: None,
|
||||
cache_ttl: None
|
||||
cache_ttl: None,
|
||||
mock: None,
|
||||
},
|
||||
FlowModule {
|
||||
id: "b".to_string(),
|
||||
@@ -1187,7 +1191,8 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
|
||||
suspend: Default::default(),
|
||||
retry: None,
|
||||
sleep: None,
|
||||
cache_ttl: None
|
||||
cache_ttl: None,
|
||||
mock: None,
|
||||
},
|
||||
FlowModule {
|
||||
id: "e".to_string(),
|
||||
@@ -1214,7 +1219,8 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
|
||||
suspend: Default::default(),
|
||||
retry: None,
|
||||
sleep: None,
|
||||
cache_ttl: None
|
||||
cache_ttl: None,
|
||||
mock: None
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1224,6 +1230,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
|
||||
retry: None,
|
||||
sleep: None,
|
||||
cache_ttl: None,
|
||||
mock: None,
|
||||
},
|
||||
FlowModule {
|
||||
id: "c".to_string(),
|
||||
@@ -1258,7 +1265,9 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
|
||||
suspend: Default::default(),
|
||||
retry: None,
|
||||
sleep: None,
|
||||
cache_ttl: None
|
||||
cache_ttl: None,
|
||||
mock: None,
|
||||
|
||||
},
|
||||
],
|
||||
same_worker: true,
|
||||
|
||||
@@ -652,7 +652,8 @@ mod tests {
|
||||
suspend: Default::default(),
|
||||
retry: None,
|
||||
sleep: None,
|
||||
cache_ttl: None
|
||||
cache_ttl: None,
|
||||
mock: None,
|
||||
},
|
||||
FlowModule {
|
||||
id: "b".to_string(),
|
||||
@@ -672,7 +673,8 @@ mod tests {
|
||||
suspend: Default::default(),
|
||||
retry: None,
|
||||
sleep: None,
|
||||
cache_ttl: None
|
||||
cache_ttl: None,
|
||||
mock: None,
|
||||
},
|
||||
FlowModule {
|
||||
id: "c".to_string(),
|
||||
@@ -690,7 +692,8 @@ mod tests {
|
||||
suspend: Default::default(),
|
||||
retry: None,
|
||||
sleep: None,
|
||||
cache_ttl: None
|
||||
cache_ttl: None,
|
||||
mock: None,
|
||||
},
|
||||
],
|
||||
failure_module: Some(FlowModule {
|
||||
@@ -708,7 +711,8 @@ mod tests {
|
||||
suspend: Default::default(),
|
||||
retry: None,
|
||||
sleep: None,
|
||||
cache_ttl: None
|
||||
cache_ttl: None,
|
||||
mock: None,
|
||||
}),
|
||||
same_worker: false,
|
||||
};
|
||||
|
||||
@@ -156,6 +156,13 @@ pub struct Suspend {
|
||||
pub resume_form: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct Mock {
|
||||
pub enabled: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub return_value: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct FlowModule {
|
||||
#[serde(default = "default_id")]
|
||||
@@ -168,6 +175,8 @@ pub struct FlowModule {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub suspend: Option<Suspend>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mock: Option<Mock>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub retry: Option<Retry>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sleep: Option<InputTransform>,
|
||||
|
||||
@@ -589,6 +589,7 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||
summary: Some(
|
||||
"Virtual module needed for suspend/sleep when last module".to_string(),
|
||||
),
|
||||
mock: None,
|
||||
retry: None,
|
||||
sleep: None,
|
||||
suspend: None,
|
||||
|
||||
@@ -1225,43 +1225,64 @@ async fn push_next_flow_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
|
||||
|
||||
let mut transform_context: Option<IdContext> = None;
|
||||
|
||||
let args: windmill_common::error::Result<_> = match &module.value {
|
||||
FlowModuleValue::Script { input_transforms, .. }
|
||||
| FlowModuleValue::RawScript { input_transforms, .. }
|
||||
| FlowModuleValue::Flow { input_transforms, .. } => {
|
||||
let ctx = get_transform_context(&flow_job, previous_id.clone(), &status).await?;
|
||||
transform_context = Some(ctx);
|
||||
let by_id = transform_context.as_ref().unwrap();
|
||||
transform_input(
|
||||
&flow_job.args,
|
||||
last_result.clone(),
|
||||
input_transforms,
|
||||
resume_messages.as_slice(),
|
||||
approvers,
|
||||
by_id,
|
||||
client,
|
||||
)
|
||||
.await
|
||||
}
|
||||
FlowModuleValue::Identity => match last_result.clone() {
|
||||
Value::Object(m) => Ok(m),
|
||||
v @ _ => {
|
||||
let mut m = Map::new();
|
||||
m.insert("previous_result".to_string(), v);
|
||||
Ok(m)
|
||||
let args: windmill_common::error::Result<_> = if module.mock.is_some()
|
||||
&& module.mock.as_ref().unwrap().enabled
|
||||
{
|
||||
let mut m = Map::new();
|
||||
let v = module
|
||||
.mock
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.return_value
|
||||
.clone()
|
||||
.ok_or_else(|| {
|
||||
Error::BadRequest(format!(
|
||||
"mock enabled but no return_value specified for module {}",
|
||||
module.id
|
||||
))
|
||||
})?;
|
||||
m.insert("previous_result".to_string(), v);
|
||||
Ok(m)
|
||||
} else {
|
||||
match &module.value {
|
||||
FlowModuleValue::Script { input_transforms, .. }
|
||||
| FlowModuleValue::RawScript { input_transforms, .. }
|
||||
| FlowModuleValue::Flow { input_transforms, .. } => {
|
||||
let ctx = get_transform_context(&flow_job, previous_id.clone(), &status).await?;
|
||||
transform_context = Some(ctx);
|
||||
let by_id = transform_context.as_ref().unwrap();
|
||||
transform_input(
|
||||
&flow_job.args,
|
||||
last_result.clone(),
|
||||
input_transforms,
|
||||
resume_messages.as_slice(),
|
||||
approvers,
|
||||
by_id,
|
||||
client,
|
||||
)
|
||||
.await
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
/* embedded flow input is augmented with embedding flow input */
|
||||
if let Some(value) = &flow_job.args {
|
||||
Ok(value
|
||||
.as_object()
|
||||
.ok_or_else(|| {
|
||||
Error::BadRequest(format!("Expected an object value, found: {value:?}"))
|
||||
})?
|
||||
.clone())
|
||||
} else {
|
||||
Ok(Map::new())
|
||||
FlowModuleValue::Identity => match last_result.clone() {
|
||||
Value::Object(m) => Ok(m),
|
||||
v @ _ => {
|
||||
let mut m = Map::new();
|
||||
m.insert("previous_result".to_string(), v);
|
||||
Ok(m)
|
||||
}
|
||||
},
|
||||
|
||||
_ => {
|
||||
/* embedded flow input is augmented with embedding flow input */
|
||||
if let Some(value) = &flow_job.args {
|
||||
Ok(value
|
||||
.as_object()
|
||||
.ok_or_else(|| {
|
||||
Error::BadRequest(format!("Expected an object value, found: {value:?}"))
|
||||
})?
|
||||
.clone())
|
||||
} else {
|
||||
Ok(Map::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1648,6 +1669,18 @@ async fn compute_next_flow_transform<'c>(
|
||||
previous_id: String,
|
||||
client: &AuthedClient,
|
||||
) -> error::Result<(sqlx::Transaction<'c, sqlx::Postgres>, NextFlowTransform)> {
|
||||
if module.mock.is_some() && module.mock.as_ref().unwrap().enabled {
|
||||
return Ok((
|
||||
tx,
|
||||
NextFlowTransform::Continue(
|
||||
ContinuePayload::SingleJob(JobPayloadWithTag {
|
||||
payload: JobPayload::Identity,
|
||||
tag: None,
|
||||
}),
|
||||
NextStatus::NextStep,
|
||||
),
|
||||
));
|
||||
}
|
||||
match &module.value {
|
||||
FlowModuleValue::Identity => Ok((
|
||||
tx,
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import FlowModuleEarlyStop from './FlowModuleEarlyStop.svelte'
|
||||
import FlowModuleSleep from './FlowModuleSleep.svelte'
|
||||
import FlowModuleSuspend from './FlowModuleSuspend.svelte'
|
||||
import FlowModuleMock from './FlowModuleMock.svelte'
|
||||
|
||||
export let flowModule: FlowModule
|
||||
export let previousModule: FlowModule | undefined
|
||||
@@ -67,6 +68,7 @@
|
||||
<Tab value="early-stop">Early Stop/Break</Tab>
|
||||
<Tab value="suspend">Suspend/Approval</Tab>
|
||||
<Tab value="sleep">Sleep</Tab>
|
||||
<Tab value="mock">Mock</Tab>
|
||||
<svelte:fragment slot="content">
|
||||
<div class="overflow-hidden bg-white">
|
||||
<TabContent value="early-stop" class="flex flex-col flex-1 h-full">
|
||||
@@ -84,6 +86,11 @@
|
||||
<FlowModuleSleep previousModuleId={previousModule?.id} bind:flowModule />
|
||||
</div>
|
||||
</TabContent>
|
||||
<TabContent value="mock" class="flex flex-col flex-1 h-full">
|
||||
<div class="p-4 overflow-y-auto">
|
||||
<FlowModuleMock bind:flowModule />
|
||||
</div>
|
||||
</TabContent>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import FlowModuleSleep from './FlowModuleSleep.svelte'
|
||||
import FlowModuleSuspend from './FlowModuleSuspend.svelte'
|
||||
import SplitPanesWrapper from '../../splitPanes/SplitPanesWrapper.svelte'
|
||||
import FlowModuleMock from './FlowModuleMock.svelte'
|
||||
// import FlowRetries from './FlowRetries.svelte'
|
||||
|
||||
export let flowModule: FlowModule
|
||||
@@ -67,6 +68,7 @@
|
||||
<Tab value="early-stop">Early Stop/Break</Tab>
|
||||
<Tab value="suspend">Suspend/Approval</Tab>
|
||||
<Tab value="sleep">Sleep</Tab>
|
||||
<Tab value="mock">Mock</Tab>
|
||||
<svelte:fragment slot="content">
|
||||
<div class="overflow-hidden bg-white">
|
||||
<TabContent value="early-stop" class="flex flex-col flex-1 h-full">
|
||||
@@ -84,6 +86,11 @@
|
||||
<FlowModuleSleep previousModuleId={previousModule?.id} bind:flowModule />
|
||||
</div>
|
||||
</TabContent>
|
||||
<TabContent value="mock" class="flex flex-col flex-1 h-full">
|
||||
<div class="p-4 overflow-y-auto">
|
||||
<FlowModuleMock bind:flowModule />
|
||||
</div>
|
||||
</TabContent>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import { getStepPropPicker } from '../previousResults'
|
||||
|
||||
import FlowModuleSleep from './FlowModuleSleep.svelte'
|
||||
import FlowModuleMock from './FlowModuleMock.svelte'
|
||||
|
||||
const { previewArgs, flowStateStore, flowStore } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
@@ -113,6 +114,7 @@
|
||||
<Tab value="early-stop">Early Stop/Break</Tab>
|
||||
<Tab value="suspend">Suspend/Approval</Tab>
|
||||
<Tab value="sleep">Sleep</Tab>
|
||||
<Tab value="mock">Mock</Tab>
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
<div class="overflow-hidden bg-white" style="height:calc(100% - 32px);">
|
||||
@@ -138,6 +140,11 @@
|
||||
<FlowModuleSleep previousModuleId={previousModule?.id} bind:flowModule={mod} />
|
||||
</div>
|
||||
</TabContent>
|
||||
<TabContent value="mock" class="flex flex-col flex-1 h-full">
|
||||
<div class="p-4 overflow-y-auto">
|
||||
<FlowModuleMock bind:flowModule={mod} />
|
||||
</div>
|
||||
</TabContent>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
import FlowPathViewer from './FlowPathViewer.svelte'
|
||||
import InputTransformSchemaForm from '$lib/components/InputTransformSchemaForm.svelte'
|
||||
import { schemaToObject } from '$lib/schema'
|
||||
import FlowModuleMock from './FlowModuleMock.svelte'
|
||||
|
||||
const { selectedId, previewArgs, flowStateStore, flowStore, saveDraft } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
@@ -133,6 +134,7 @@
|
||||
bind:module={flowModule}
|
||||
on:toggleSuspend={() => selectAdvanced('suspend')}
|
||||
on:toggleSleep={() => selectAdvanced('sleep')}
|
||||
on:toggleMock={() => selectAdvanced('mock')}
|
||||
on:toggleRetry={() => selectAdvanced('retries')}
|
||||
on:toggleCache={() => selectAdvanced('cache')}
|
||||
on:toggleStopAfterIf={() => selectAdvanced('early-stop')}
|
||||
@@ -265,6 +267,7 @@
|
||||
<Tab value="early-stop">Early Stop/Break</Tab>
|
||||
<Tab value="suspend">Suspend/Approval</Tab>
|
||||
<Tab value="sleep">Sleep</Tab>
|
||||
<Tab value="mock">Mock</Tab>
|
||||
<Tab value="same_worker">Shared Directory</Tab>
|
||||
{/if}
|
||||
</Tabs>
|
||||
@@ -285,6 +288,10 @@
|
||||
<div>
|
||||
<FlowModuleCache bind:flowModule />
|
||||
</div>
|
||||
{:else if advancedSelected === 'mock'}
|
||||
<div>
|
||||
<FlowModuleMock bind:flowModule />
|
||||
</div>
|
||||
{:else if advancedSelected === 'same_worker'}
|
||||
<div>
|
||||
<Alert type="info" title="Share a directory between steps">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { WorkerService, type FlowModule } from '$lib/gen'
|
||||
import { faCodeBranch, faPen, faSave } from '@fortawesome/free-solid-svg-icons'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { Bed, Database, PhoneIncoming, Repeat, Square } from 'lucide-svelte'
|
||||
import { Bed, Database, PhoneIncoming, Repeat, Square, Voicemail } from 'lucide-svelte'
|
||||
import Popover from '../../Popover.svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import { sendUserToast } from '$lib/utils'
|
||||
@@ -83,6 +83,17 @@
|
||||
<Bed size={14} />
|
||||
<svelte:fragment slot="text">Sleep</svelte:fragment>
|
||||
</Popover>
|
||||
<Popover
|
||||
placement="bottom"
|
||||
class="center-center rounded p-2
|
||||
{module.mock?.enabled
|
||||
? 'bg-blue-100 text-blue-800 border border-blue-300 hover:bg-blue-200'
|
||||
: 'bg-white text-gray-800 hover:bg-gray-100'}"
|
||||
on:click={() => dispatch('toggleMock')}
|
||||
>
|
||||
<Voicemail size={14} />
|
||||
<svelte:fragment slot="text">Mock</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
{#if module.value.type === 'script'}
|
||||
<div class="w-2" />
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
|
||||
import JsonEditor from '$lib/components/apps/editor/settingsPanel/inputEditor/JsonEditor.svelte'
|
||||
|
||||
export let flowModule: FlowModule
|
||||
|
||||
let code: string | undefined = flowModule.mock?.return_value
|
||||
? JSON.stringify(flowModule.mock?.return_value, null, 2)
|
||||
: undefined
|
||||
|
||||
$: isMockEnabled = Boolean(flowModule.mock?.enabled)
|
||||
</script>
|
||||
|
||||
<h2 class="pb-4">
|
||||
Mock
|
||||
<Tooltip>
|
||||
If defined and enabled, the step will immediately return the mock value instead of being
|
||||
executed.
|
||||
</Tooltip>
|
||||
</h2>
|
||||
<Toggle
|
||||
checked={isMockEnabled}
|
||||
on:change={() => {
|
||||
if (isMockEnabled) {
|
||||
flowModule.mock = {
|
||||
enabled: false,
|
||||
return_value: flowModule.mock?.return_value
|
||||
}
|
||||
} else {
|
||||
flowModule.mock = {
|
||||
enabled: true,
|
||||
return_value: flowModule.mock?.return_value ?? { example: 'value' }
|
||||
}
|
||||
code = JSON.stringify(flowModule.mock?.return_value, null, 2)
|
||||
}
|
||||
console.log(isMockEnabled, flowModule.mock)
|
||||
}}
|
||||
options={{
|
||||
right: 'Enable step mocking'
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<span class="text-xs font-bold">Mocked Return value</span>
|
||||
{#if flowModule?.mock?.return_value != undefined}
|
||||
<JsonEditor {code} bind:value={flowModule.mock.return_value} />
|
||||
{:else}
|
||||
<input
|
||||
type="text"
|
||||
disabled
|
||||
value={flowModule.mock?.return_value ? code : ''}
|
||||
class="w-full p-2 border rounded-md"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -2,7 +2,7 @@
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { Bed, Database, Move, PhoneIncoming, Repeat, Square, X } from 'lucide-svelte'
|
||||
import { Bed, Database, Move, PhoneIncoming, Repeat, Square, Voicemail, X } from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
export let earlyStop: boolean = false
|
||||
export let suspend: boolean = false
|
||||
export let sleep: boolean = false
|
||||
export let mock: boolean = false
|
||||
export let bold: boolean = false
|
||||
export let id: string | undefined = undefined
|
||||
export let label: string
|
||||
@@ -88,6 +89,17 @@
|
||||
<svelte:fragment slot="text">Sleep</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
{#if mock}
|
||||
<Popover notClickable>
|
||||
<div
|
||||
transition:fade|local={{ duration: 200 }}
|
||||
class="center-center bg-white rounded border border-gray-400 text-gray-700 px-1 py-0.5"
|
||||
>
|
||||
<Voicemail size={14} />
|
||||
</div>
|
||||
<svelte:fragment slot="text">Mocked</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
class="flex gap-1 justify-between items-center w-full overflow-hidden rounded-sm
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
earlyStop: mod.stop_after_if != undefined,
|
||||
suspend: Boolean(mod.suspend),
|
||||
sleep: Boolean(mod.sleep),
|
||||
cache: Boolean(mod.cache_ttl)
|
||||
cache: Boolean(mod.cache_ttl),
|
||||
mock: Boolean(mod.mock?.enabled)
|
||||
}
|
||||
|
||||
function onDelete(event: CustomEvent<MouseEvent>) {
|
||||
|
||||
@@ -92,6 +92,12 @@ components:
|
||||
type: number
|
||||
summary:
|
||||
type: string
|
||||
mock:
|
||||
type: object
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
return_value: {}
|
||||
suspend:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
Reference in New Issue
Block a user