mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 16:02:28 +00:00
fix: flow step missing input warnings (#5916)
* fix: flow step missing input warnings * nit
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import { VariableService, type InputTransform } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { allTrue } from '$lib/utils'
|
||||
import { createEventDispatcher, untrack } from 'svelte'
|
||||
import { untrack } from 'svelte'
|
||||
import { Button } from './common'
|
||||
import StepInputsGen from './copilot/StepInputsGen.svelte'
|
||||
import type { PickableProperties } from './flows/previousResults'
|
||||
@@ -40,8 +40,6 @@
|
||||
|
||||
let inputCheck: { [id: string]: boolean } = $state({})
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$effect(() => {
|
||||
isValid = allTrue(inputCheck) ?? false
|
||||
})
|
||||
@@ -117,10 +115,6 @@
|
||||
{noDynamicToggle}
|
||||
{pickableProperties}
|
||||
{enableAi}
|
||||
on:change={(e) => {
|
||||
const { argName } = e.detail
|
||||
dispatch('changeArg', { argName })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
flowModuleSchemaMap: FlowModuleSchemaMap | undefined
|
||||
} = $props()
|
||||
|
||||
const { flowStore, flowStateStore, selectedId, currentEditor, flowInputsStore } =
|
||||
const { flowStore, flowStateStore, selectedId, currentEditor } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
const { exprsToSet } = getContext<FlowCopilotContext | undefined>('FlowCopilotContext') ?? {}
|
||||
@@ -185,10 +185,6 @@
|
||||
flowModuleSchemaMap?.removeAtId(modules, id)
|
||||
}
|
||||
|
||||
if ($flowInputsStore) {
|
||||
delete $flowInputsStore[id]
|
||||
}
|
||||
|
||||
refreshStateStore(flowStore)
|
||||
|
||||
flowModuleSchemaMap?.updateFlowInputsStore()
|
||||
|
||||
@@ -73,9 +73,9 @@ function getModelSpecificConfig(
|
||||
}
|
||||
: {
|
||||
model: modelProvider.model,
|
||||
temperature: 0,
|
||||
...(tools && tools.length > 0 ? { tools } : {})
|
||||
temperature: 0
|
||||
}),
|
||||
...(tools && tools.length > 0 ? { tools } : {}),
|
||||
max_completion_tokens: getModelMaxTokens(modelProvider.model)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import FlowModuleWrapper from './FlowModuleWrapper.svelte'
|
||||
import FlowSettings from './FlowSettings.svelte'
|
||||
@@ -7,24 +7,35 @@
|
||||
import FlowFailureModule from './FlowFailureModule.svelte'
|
||||
import FlowConstants from './FlowConstants.svelte'
|
||||
import type { FlowModule, Flow } from '$lib/gen'
|
||||
import { initFlowStepWarnings } from '../utils'
|
||||
import { dfs } from '../dfs'
|
||||
import FlowPreprocessorModule from './FlowPreprocessorModule.svelte'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
import { insertNewPreprocessorModule } from '../flowStateUtils.svelte'
|
||||
import TriggersEditor from '../../triggers/TriggersEditor.svelte'
|
||||
import { handleSelectTriggerFromKind, type Trigger } from '$lib/components/triggers/utils'
|
||||
import { computeMissingInputWarnings } from '../missingInputWarnings'
|
||||
|
||||
interface Props {
|
||||
noEditor?: boolean
|
||||
enableAi?: boolean
|
||||
newFlow?: boolean
|
||||
disabledFlowInputs?: boolean
|
||||
savedFlow?:
|
||||
| (Flow & {
|
||||
draft?: Flow | undefined
|
||||
})
|
||||
| undefined
|
||||
onDeployTrigger?: (trigger: Trigger) => void
|
||||
}
|
||||
|
||||
let {
|
||||
noEditor = false,
|
||||
enableAi = false,
|
||||
newFlow = false,
|
||||
disabledFlowInputs = false,
|
||||
savedFlow = undefined,
|
||||
onDeployTrigger = () => {}
|
||||
}: Props = $props()
|
||||
|
||||
export let noEditor = false
|
||||
export let enableAi = false
|
||||
export let newFlow = false
|
||||
export let disabledFlowInputs = false
|
||||
export let savedFlow:
|
||||
| (Flow & {
|
||||
draft?: Flow | undefined
|
||||
})
|
||||
| undefined = undefined
|
||||
export let onDeployTrigger: (trigger: Trigger) => void = () => {}
|
||||
const {
|
||||
selectedId,
|
||||
flowStore,
|
||||
@@ -50,28 +61,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function initWarnings() {
|
||||
for (const module of flowStore.val?.value?.modules) {
|
||||
if (!module) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!$flowInputsStore) {
|
||||
$flowInputsStore = {}
|
||||
}
|
||||
|
||||
$flowInputsStore[module?.id] = {
|
||||
flowStepWarnings: await initFlowStepWarnings(
|
||||
module.value,
|
||||
$flowStateStore?.[module?.id]?.schema,
|
||||
dfs(flowStore.val.value.modules, (fm) => fm.id)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
initWarnings()
|
||||
$effect(() => {
|
||||
computeMissingInputWarnings(flowStore, $flowStateStore, flowInputsStore)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -46,9 +46,6 @@
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { loadSchemaFromModule } from '../flowInfers'
|
||||
import { computeFlowStepWarning, initFlowStepWarnings } from '../utils'
|
||||
import { debounce } from '$lib/utils'
|
||||
import { dfs } from '../dfs'
|
||||
import FlowModuleSkip from './FlowModuleSkip.svelte'
|
||||
import { type Job, JobService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
@@ -65,7 +62,6 @@
|
||||
flowStore,
|
||||
pathStore,
|
||||
saveDraft,
|
||||
flowInputsStore,
|
||||
customUi,
|
||||
executionCount
|
||||
} = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
@@ -147,15 +143,6 @@
|
||||
|
||||
if (inputTransformSchemaForm) {
|
||||
inputTransformSchemaForm.setArgs(input_transforms)
|
||||
if (!deepEqual(schema, $flowStateStore[flowModule.id]?.schema)) {
|
||||
$flowInputsStore[flowModule?.id] = {
|
||||
flowStepWarnings: await initFlowStepWarnings(
|
||||
flowModule.value,
|
||||
schema ?? {},
|
||||
dfs(flowStore.val.value.modules, (fm) => fm.id)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
flowModule.value.type == 'rawscript' ||
|
||||
@@ -202,27 +189,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
let debouncedWarning = debounce((argName: string) => {
|
||||
if ($flowInputsStore) {
|
||||
computeFlowStepWarning(
|
||||
argName,
|
||||
flowModule.value,
|
||||
$flowInputsStore[flowModule.id].flowStepWarnings ?? {},
|
||||
$flowStateStore[$selectedId]?.schema,
|
||||
dfs(flowStore.val?.value?.modules ?? [], (fm) => fm.id) ?? []
|
||||
).then((flowStepWarnings) => {
|
||||
$flowInputsStore[flowModule.id].flowStepWarnings = flowStepWarnings
|
||||
})
|
||||
}
|
||||
}, 100)
|
||||
|
||||
function setFlowInput(argName: string) {
|
||||
if ($flowInputsStore && flowModule.id && $flowInputsStore?.[flowModule.id] === undefined) {
|
||||
$flowInputsStore[flowModule.id] = {}
|
||||
}
|
||||
debouncedWarning(argName)
|
||||
}
|
||||
|
||||
async function getLastJob() {
|
||||
if (
|
||||
!$flowStateStore ||
|
||||
@@ -545,10 +511,6 @@
|
||||
}
|
||||
extraLib={stepPropPicker.extraLib}
|
||||
{enableAi}
|
||||
on:changeArg={(e) => {
|
||||
const { argName } = e.detail
|
||||
setFlowInput(argName)
|
||||
}}
|
||||
/>
|
||||
</PropPickerWrapper>
|
||||
</div>
|
||||
|
||||
@@ -19,13 +19,10 @@
|
||||
import FlowBranchesAllWrapper from './FlowBranchesAllWrapper.svelte'
|
||||
import FlowBranchesOneWrapper from './FlowBranchesOneWrapper.svelte'
|
||||
import FlowWhileLoop from './FlowWhileLoop.svelte'
|
||||
import { initFlowStepWarnings } from '../utils'
|
||||
import { dfs } from '../dfs'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
import { formatCron } from '$lib/utils'
|
||||
|
||||
const { selectedId, flowStateStore, flowInputsStore, flowStore } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const { selectedId, flowStateStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
const { triggersState, triggersCount } = getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
@@ -106,16 +103,6 @@
|
||||
|
||||
flowModule = module
|
||||
$flowStateStore[module.id] = state
|
||||
|
||||
if ($flowInputsStore) {
|
||||
$flowInputsStore[module?.id] = {
|
||||
flowStepWarnings: await initFlowStepWarnings(
|
||||
module?.value,
|
||||
$flowStateStore[module?.id]?.schema,
|
||||
dfs(flowStore.val.value.modules, (fm) => fm.id)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -192,16 +179,6 @@
|
||||
|
||||
flowModule = module
|
||||
$flowStateStore[module.id] = state
|
||||
|
||||
if ($flowInputsStore) {
|
||||
$flowInputsStore[module.id] = {
|
||||
flowStepWarnings: await initFlowStepWarnings(
|
||||
module.value,
|
||||
$flowStateStore[module.id].schema,
|
||||
dfs(flowStore.val.value.modules, (fm) => fm.id)
|
||||
)
|
||||
}
|
||||
}
|
||||
}}
|
||||
failureModule={$selectedId === 'failure'}
|
||||
preprocessorModule={$selectedId === 'preprocessor'}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
insertNewPreprocessorModule
|
||||
} from '$lib/components/flows/flowStateUtils.svelte'
|
||||
import type { FlowModule, ScriptLang } from '$lib/gen'
|
||||
import { emptyFlowModuleState, initFlowStepWarnings } from '../utils'
|
||||
import { emptyFlowModuleState } from '../utils'
|
||||
import FlowSettingsItem from './FlowSettingsItem.svelte'
|
||||
import FlowConstantsItem from './FlowConstantsItem.svelte'
|
||||
|
||||
@@ -242,30 +242,6 @@
|
||||
change: void
|
||||
}>()
|
||||
|
||||
export async function updateFlowInputsStore() {
|
||||
const keys = Object.keys(dependents ?? {})
|
||||
|
||||
for (const key of keys) {
|
||||
const module = flowStore.val.value.modules.find((m) => m.id === key)
|
||||
|
||||
if (!module) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!$flowInputsStore) {
|
||||
$flowInputsStore = {}
|
||||
}
|
||||
|
||||
$flowInputsStore[module.id] = {
|
||||
flowStepWarnings: await initFlowStepWarnings(
|
||||
module.value,
|
||||
$flowStateStore?.[module.id]?.schema,
|
||||
dfs(flowStore.val.value.modules, (fm) => fm.id)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setExpr(module: FlowModule, expr: string) {
|
||||
if (module.value.type == 'forloopflow') {
|
||||
module.value.iterator = { type: 'javascript', expr }
|
||||
@@ -377,13 +353,8 @@
|
||||
} else {
|
||||
selectNextId(id)
|
||||
removeAtId(flowStore.val.value.modules, id)
|
||||
if ($flowInputsStore) {
|
||||
delete $flowInputsStore[id]
|
||||
}
|
||||
}
|
||||
refreshStateStore(flowStore)
|
||||
|
||||
updateFlowInputsStore()
|
||||
}
|
||||
|
||||
if (Object.keys(dependents).length > 0) {
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import type { Schema } from '$lib/common'
|
||||
import type { InputTransform, OpenFlow } from '$lib/gen'
|
||||
import { parseOutputs } from '$lib/infer'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import type { FlowInput } from './types'
|
||||
import type { StateStore } from '$lib/utils'
|
||||
import type { FlowState } from './flowState'
|
||||
import { dfs } from './dfs'
|
||||
|
||||
function isInputFilled(
|
||||
inputTransforms: Record<string, InputTransform>,
|
||||
key: string,
|
||||
schema: Schema | undefined
|
||||
): boolean {
|
||||
const required = schema?.required?.includes(key) ?? false
|
||||
|
||||
if (!required) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (inputTransforms.hasOwnProperty(key)) {
|
||||
const transform = inputTransforms[key]
|
||||
if (
|
||||
transform?.type === 'static' &&
|
||||
(transform?.value === undefined || transform?.value === '' || transform?.value === null)
|
||||
) {
|
||||
return false
|
||||
} else if (
|
||||
transform?.type === 'javascript' &&
|
||||
(transform?.expr === undefined || transform?.expr === '' || transform?.expr === null)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async function isConnectedToMissingModule(
|
||||
argName: string,
|
||||
input_transform: InputTransform,
|
||||
moduleIds: string[]
|
||||
): Promise<string | undefined> {
|
||||
const val: string =
|
||||
input_transform.type === 'static' ? String(input_transform.value) : input_transform.expr
|
||||
|
||||
try {
|
||||
const outputs = await parseOutputs(val, true)
|
||||
let error: string = ''
|
||||
|
||||
outputs?.forEach(([componentId, id]) => {
|
||||
if (componentId === 'results') {
|
||||
if (!moduleIds.includes(id)) {
|
||||
error += `Input ${argName} is connected to a missing module with id ${id}\n`
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return error
|
||||
} catch (e) {
|
||||
return `Input ${argName} expression is invalid`
|
||||
}
|
||||
}
|
||||
|
||||
type FlowStepWarnings = Record<string, { message: string; type: 'error' | 'warning' }>
|
||||
|
||||
async function computeFlowStepWarnings(
|
||||
input: {
|
||||
input_transforms: Record<string, InputTransform>
|
||||
id: string
|
||||
schema: Schema | undefined
|
||||
},
|
||||
moduleIds: string[] = []
|
||||
) {
|
||||
const messages: FlowStepWarnings = {}
|
||||
const { input_transforms, schema } = input
|
||||
const keys = Object.keys(input_transforms ?? {})
|
||||
const promises = keys.map(async (key) => {
|
||||
if (!isInputFilled(input_transforms, key, schema)) {
|
||||
messages[key] = {
|
||||
message: `Input ${key} is required but not filled`,
|
||||
type: 'warning'
|
||||
}
|
||||
} else {
|
||||
const errorMessage = await isConnectedToMissingModule(key, input_transforms[key], moduleIds)
|
||||
if (errorMessage) {
|
||||
messages[key] = {
|
||||
message: errorMessage,
|
||||
type: 'error'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
await Promise.all(promises)
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
export async function computeMissingInputWarnings(
|
||||
flowStore: StateStore<OpenFlow>,
|
||||
flowState: FlowState,
|
||||
flowInputsStore: Writable<FlowInput>
|
||||
) {
|
||||
const inputs = dfs(flowStore.val.value.modules, (module) => {
|
||||
if (
|
||||
module.value.type === 'script' ||
|
||||
module.value.type === 'rawscript' ||
|
||||
module.value.type === 'flow'
|
||||
) {
|
||||
const schema = flowState[module.id]?.schema
|
||||
return {
|
||||
input_transforms: module.value.input_transforms,
|
||||
id: module.id,
|
||||
schema
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}).filter((x) => x !== undefined)
|
||||
|
||||
const moduleIds = dfs(flowStore.val.value.modules, (module) => module.id)
|
||||
|
||||
const promises = inputs.map(async (input) => {
|
||||
const warnings = await computeFlowStepWarnings(input, moduleIds)
|
||||
return [input.id, warnings] as const
|
||||
})
|
||||
const warnings = Object.fromEntries(await Promise.all(promises))
|
||||
for (const key in warnings) {
|
||||
flowInputsStore.update((fi) => {
|
||||
return {
|
||||
...fi,
|
||||
[key]: {
|
||||
flowStepWarnings: warnings[key]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,7 @@ import {
|
||||
type InputTransform,
|
||||
type Job,
|
||||
type RestartedFrom,
|
||||
type OpenFlow,
|
||||
type FlowModuleValue
|
||||
type OpenFlow
|
||||
} from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { cleanExpr, emptySchema } from '$lib/utils'
|
||||
@@ -15,8 +14,6 @@ import type { FlowModuleState } from './flowState'
|
||||
import { type PickableProperties, dfs } from './previousResults'
|
||||
import { NEVER_TESTED_THIS_FAR } from './models'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import type { Schema } from '$lib/common'
|
||||
import { parseOutputs } from '$lib/infer'
|
||||
import type { ExtendedOpenFlow } from './types'
|
||||
|
||||
function create_context_function_template(eval_string: string, context: Record<string, any>) {
|
||||
@@ -188,135 +185,6 @@ export function emptyFlowModuleState(): FlowModuleState {
|
||||
}
|
||||
}
|
||||
|
||||
export function isInputFilled(
|
||||
inputTransforms: Record<string, InputTransform>,
|
||||
key: string,
|
||||
schema: Schema | undefined
|
||||
): boolean {
|
||||
const required = schema?.required?.includes(key) ?? false
|
||||
|
||||
if (!required) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (inputTransforms.hasOwnProperty(key)) {
|
||||
const transform = inputTransforms[key]
|
||||
if (
|
||||
transform?.type === 'static' &&
|
||||
(transform?.value === undefined || transform?.value === '' || transform?.value === null)
|
||||
) {
|
||||
return false
|
||||
} else if (
|
||||
transform?.type === 'javascript' &&
|
||||
(transform?.expr === undefined || transform?.expr === '' || transform?.expr === null)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async function isConnectedToMissingModule(
|
||||
argName: string,
|
||||
flowModuleValue: FlowModuleValue,
|
||||
moduleIds: string[]
|
||||
): Promise<string | undefined> {
|
||||
const type = flowModuleValue.type
|
||||
|
||||
if (type === 'rawscript' || type === 'script' || type === 'flow') {
|
||||
const input = flowModuleValue?.input_transforms[argName]
|
||||
const val: string = input.type === 'static' ? String(input.value) : input.expr
|
||||
|
||||
try {
|
||||
const outputs = await parseOutputs(val, true)
|
||||
let error: string = ''
|
||||
|
||||
outputs?.forEach(([componentId, id]) => {
|
||||
if (componentId === 'results') {
|
||||
if (!moduleIds.includes(id)) {
|
||||
error += `Input ${argName} is connected to a missing module with id ${id}\n`
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return error
|
||||
} catch (e) {
|
||||
return `Input ${argName} expression is invalid`
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
export async function computeFlowStepWarning(
|
||||
argName: string,
|
||||
flowModuleValue: FlowModuleValue,
|
||||
messages: Record<
|
||||
string,
|
||||
{
|
||||
message: string
|
||||
type: 'error' | 'warning'
|
||||
}
|
||||
>,
|
||||
schema: Schema | undefined,
|
||||
moduleIds: string[] = []
|
||||
) {
|
||||
if (messages[argName]) {
|
||||
delete messages[argName]
|
||||
}
|
||||
|
||||
const type = flowModuleValue.type
|
||||
if (type == 'rawscript' || type == 'script' || type == 'flow') {
|
||||
if (!isInputFilled(flowModuleValue.input_transforms, argName, schema)) {
|
||||
messages[argName] = {
|
||||
message: `Input ${argName} is required but not filled`,
|
||||
type: 'warning'
|
||||
}
|
||||
}
|
||||
|
||||
const errorMessage = await isConnectedToMissingModule(argName, flowModuleValue, moduleIds)
|
||||
|
||||
if (errorMessage) {
|
||||
messages[argName] = {
|
||||
message: errorMessage,
|
||||
type: 'error'
|
||||
}
|
||||
} else {
|
||||
if (messages[argName]?.type === 'error') {
|
||||
delete messages[argName]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
export async function initFlowStepWarnings(
|
||||
flowModuleValue: FlowModuleValue,
|
||||
schema: Schema | undefined,
|
||||
moduleIds: string[] = []
|
||||
) {
|
||||
const messages: Record<
|
||||
string,
|
||||
{
|
||||
message: string
|
||||
type: 'error' | 'warning'
|
||||
}
|
||||
> = {}
|
||||
const type = flowModuleValue.type
|
||||
|
||||
if (type == 'rawscript' || type == 'script' || type == 'flow') {
|
||||
const keys = Object.keys(flowModuleValue.input_transforms ?? {})
|
||||
const promises = keys.map(async (key) => {
|
||||
await computeFlowStepWarning(key, flowModuleValue, messages, schema, moduleIds)
|
||||
})
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
export function checkIfParentLoop(
|
||||
flowStore: ExtendedOpenFlow,
|
||||
modId: string
|
||||
|
||||
Reference in New Issue
Block a user