fix(frontend): flow editor improvements (#4008)

* fix(frontend): wip

* fix(frontend): wip

* fix(frontend): add missing iter input for while loops

* fix(frontend): improve code

* fix(frontend): improve code

* fix(frontend): failure status for fake modules on hold

* fix(frontend): wip

* fix(frontend): improve flow step warnings

* fix(frontend): add debounce

* fix(frontend): improve rpath detection

* feat(frontend): improve code

* fix(frontend): use the code parser to detect missing dependencies

* fix(frontend): improve code

* fix(frontend): improve code
This commit is contained in:
Faton Ramadani
2024-07-15 09:46:35 +02:00
committed by GitHub
parent 546c343811
commit e6dfa390bb
10 changed files with 240 additions and 70 deletions
@@ -216,6 +216,19 @@
true
)
}
/**
* else if (mod.type === 'Failure' || mod.type === 'WaitingForPriorSteps') {
if (job?.type === 'CompletedJob') {
setModuleState('b', {
type: 'Failure',
args: job?.args,
job_id: job?.id,
result: job?.result
})
}
}
*/
})
}
}
@@ -8,7 +8,7 @@
import FlowFailureModule from './FlowFailureModule.svelte'
import FlowConstants from './FlowConstants.svelte'
import type { FlowModule } from '$lib/gen'
import { initRequiredInputFilled } from '../utils'
import { initFlowStepWarnings } from '../utils'
export let noEditor = false
export let enableAi = false
@@ -27,10 +27,10 @@
}
}
onMount(() => {
$flowStore?.value?.modules?.forEach((module) => {
async function initWarnings() {
for (const module of $flowStore?.value?.modules) {
if (!module) {
return
continue
}
if (!$flowInputsStore) {
@@ -38,12 +38,17 @@
}
$flowInputsStore[module?.id] = {
requiredInputsFilled: initRequiredInputFilled(
flowStepWarnings: await initFlowStepWarnings(
module.value,
$flowStateStore?.[module?.id]?.schema ?? {}
$flowStateStore?.[module?.id]?.schema ?? {},
$flowStore?.value?.modules?.map((m) => m.id) ?? []
)
}
})
}
}
onMount(() => {
initWarnings()
})
</script>
@@ -44,7 +44,8 @@
import { enterpriseLicense } from '$lib/stores'
import { isCloudHosted } from '$lib/cloud'
import { loadSchemaFromModule } from '../flowInfers'
import { initRequiredInputFilled, setRequiredInputFilled } from '../utils'
import { computeFlowStepWarning, initFlowStepWarnings } from '../utils'
import { debounce } from '$lib/utils'
const {
selectedId,
@@ -128,8 +129,12 @@
if (inputTransformSchemaForm) {
inputTransformSchemaForm.setArgs(input_transforms)
if (!deepEqual(schema, $flowStateStore[flowModule.id]?.schema)) {
$flowInputsStore![flowModule?.id] = {
requiredInputsFilled: initRequiredInputFilled(flowModule.value, schema ?? {})
$flowInputsStore[flowModule?.id] = {
flowStepWarnings: await initFlowStepWarnings(
flowModule.value,
schema ?? {},
$flowStore?.value?.modules?.map((m) => m?.id) ?? []
)
}
}
} else {
@@ -176,21 +181,25 @@
let editorPanelSize = noEditor ? 0 : flowModule.value.type == 'script' ? 30 : 50
let editorSettingsPanelSize = 100 - editorPanelSize
function setFlowInput(argName: string) {
if ($flowInputsStore && $flowInputsStore?.[flowModule.id] === undefined) {
$flowInputsStore[flowModule.id] = {}
}
let debouncedWarning = debounce((argName: string) => {
if ($flowInputsStore) {
const requiredInputsFilled = setRequiredInputFilled(
computeFlowStepWarning(
argName,
flowModule.value,
$flowInputsStore[flowModule.id].requiredInputsFilled ?? {},
$flowStateStore[$selectedId]?.schema
)
$flowInputsStore[flowModule.id].requiredInputsFilled = requiredInputsFilled
$flowInputsStore[flowModule.id].flowStepWarnings ?? {},
$flowStateStore[$selectedId]?.schema ?? {},
$flowStore?.value?.modules?.map((m) => m?.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)
}
</script>
@@ -18,13 +18,13 @@
import FlowBranchesAllWrapper from './FlowBranchesAllWrapper.svelte'
import FlowBranchesOneWrapper from './FlowBranchesOneWrapper.svelte'
import FlowWhileLoop from './FlowWhileLoop.svelte'
import { initRequiredInputFilled } from '../utils'
import { initFlowStepWarnings } from '../utils'
export let flowModule: FlowModule
export let noEditor: boolean = false
export let enableAi = false
const { selectedId, schedule, flowStateStore, flowInputsStore } =
const { selectedId, schedule, flowStateStore, flowInputsStore, flowStore } =
getContext<FlowEditorContext>('FlowEditorContext')
let scriptKind: 'script' | 'trigger' | 'approval' = 'script'
@@ -65,9 +65,10 @@
if ($flowInputsStore) {
$flowInputsStore[module?.id] = {
requiredInputsFilled: initRequiredInputFilled(
flowStepWarnings: await initFlowStepWarnings(
module?.value,
$flowStateStore[module?.id]?.schema
$flowStateStore[module?.id]?.schema,
$flowStore.value.modules.map((m) => m.id)
)
}
}
@@ -147,9 +148,10 @@
if ($flowInputsStore) {
$flowInputsStore[module.id] = {
requiredInputsFilled: initRequiredInputFilled(
flowStepWarnings: await initFlowStepWarnings(
module.value,
$flowStateStore[module.id].schema
$flowStateStore[module.id].schema,
$flowStore.value.modules.map((m) => m.id)
)
}
}
@@ -19,6 +19,7 @@
import { fade } from 'svelte/transition'
import type { FlowInput } from '../types'
import type { Writable } from 'svelte/store'
import { twMerge } from 'tailwind-merge'
export let selected: boolean = false
export let deletable: boolean = false
@@ -175,14 +176,30 @@ hover:border-blue-700 {selected ? '' : '!hidden'}"
<Move class="mx-[3px]" size={14} strokeWidth={2} />
</button>
{#if (id && !Object.values($flowInputsStore?.[id]?.requiredInputsFilled || {}).every(Boolean)) || Boolean(warningMessage)}
{#if (id && Object.values($flowInputsStore?.[id]?.flowStepWarnings || {}).length > 0) || Boolean(warningMessage)}
<div class="absolute -top-[10px] -left-[10px]">
<Popover>
<svelte:fragment slot="text"
>{warningMessage ?? 'At least one required input is not set.'}
<svelte:fragment slot="text">
<ul class="list-disc px-2">
{#if id}
{#each Object.values($flowInputsStore?.[id]?.flowStepWarnings || {}) as m}
<li>
{m.message}
</li>
{/each}
{/if}
</ul>
</svelte:fragment>
<div
class="flex items-center justify-center h-full w-full rounded-md p-0.5 border border-yellow-600 text-yellow-600 bg-yellow-100 duration-150 hover:bg-yellow-300"
class={twMerge(
'flex items-center justify-center h-full w-full rounded-md p-0.5 border duration-150 ',
id &&
Object.values($flowInputsStore?.[id]?.flowStepWarnings || {})?.some(
(x) => x.type === 'error'
)
? 'border-red-600 text-red-600 bg-red-100 hover:bg-red-300'
: 'border-yellow-600 text-yellow-600 bg-yellow-100 hover:bg-yellow-300'
)}
>
<AlertTriangle size={14} strokeWidth={2} />
</div>
@@ -11,7 +11,7 @@
pickScript
} from '$lib/components/flows/flowStateUtils'
import type { FlowModule } from '$lib/gen'
import { emptyFlowModuleState } from '../utils'
import { emptyFlowModuleState, initFlowStepWarnings } from '../utils'
import FlowSettingsItem from './FlowSettingsItem.svelte'
import FlowConstantsItem from './FlowConstantsItem.svelte'
@@ -175,6 +175,30 @@
}
const dispatch = createEventDispatcher()
async function updateFlowInputsStore() {
const keys = Object.keys(dependents ?? {})
for (const key of keys) {
const module = $flowStore.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 ?? {},
$flowStore?.value?.modules?.map((m) => m.id) ?? []
)
}
}
}
</script>
<Portal>
@@ -246,6 +270,8 @@
delete $flowInputsStore[e.id]
}
$flowStore = $flowStore
updateFlowInputsStore()
}
if (Object.keys(dependents).length > 0) {
@@ -22,6 +22,14 @@ export function dfs(
id: string | undefined,
flow: OpenFlow,
getParents: boolean = true
): FlowModule[] {
return dfsByModule(id, flow.value.modules, getParents)
}
export function dfsByModule(
id: string | undefined,
modules: FlowModule[],
getParents: boolean = true
): FlowModule[] {
if (id === undefined) {
return []
@@ -48,7 +56,7 @@ export function dfs(
return undefined
}
return rec(id, [flow.value.modules]) ?? []
return rec(id, [modules]) ?? []
}
function getFlowInput(
+7 -1
View File
@@ -8,7 +8,13 @@ import type { Schedule } from './scheduleUtils'
export type FlowInput = Record<
string,
{
requiredInputsFilled?: Record<string, boolean>
flowStepWarnings?: Record<
string,
{
message: string
type: 'error' | 'warning'
}
>
}
>
+92 -23
View File
@@ -16,6 +16,7 @@ import type { PickableProperties } 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'
function create_context_function_template(eval_string: string, context: Record<string, any>) {
return `
@@ -189,34 +190,102 @@ export function isInputFilled(
return true
}
export function setRequiredInputFilled(
async function isConnectedToMissingModule(
argName: string,
flowModuleValue: FlowModuleValue,
requiredInputsFilled: Record<string, boolean>,
schema: Schema
) {
const type = flowModuleValue.type
if (type == 'rawscript' || type == 'script' || type == 'flow') {
requiredInputsFilled[argName] = isInputFilled(
flowModuleValue.input_transforms,
argName,
schema ?? {}
)
}
return requiredInputsFilled
}
export function initRequiredInputFilled(flowModuleValue: FlowModuleValue, schema: Schema) {
const requiredInputsFilled: Record<string, boolean> = {}
moduleIds: string[]
): Promise<string | undefined> {
const type = flowModuleValue.type
if (type == 'rawscript' || type == 'script' || type == 'flow') {
const keys = Object.keys(flowModuleValue.input_transforms)
for (const key of keys) {
requiredInputsFilled[key] = isInputFilled(flowModuleValue.input_transforms, key, schema ?? {})
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 requiredInputsFilled
return
}
export async function computeFlowStepWarning(
argName: string,
flowModuleValue: FlowModuleValue,
messages: Record<
string,
{
message: string
type: 'error' | 'warning'
}
>,
schema: Schema,
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,
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
}
@@ -26,6 +26,7 @@
import { deepEqual } from 'fast-equals'
import DarkModeObserver from '../DarkModeObserver.svelte'
import type { FlowInput } from '../flows/types'
import { dfsByModule } from '../flows/previousResults'
export let success: boolean | undefined = undefined
export let modules: FlowModule[] | undefined = []
@@ -184,21 +185,35 @@
if (useDataflow && $selectedId) {
let deps = getDependeeAndDependentComponents($selectedId, modules ?? [], failureModule)
if (deps) {
Object.entries(deps.dependees).forEach((x, i) => {
let pid = x[0]
edges.push({
id: `dep-${pid}-${$selectedId}`,
source: pid,
target: $selectedId!,
labelBgColor: darkMode ? '#999' : 'white',
edgeColor: darkMode ? 'white' : 'black',
arrow: false,
animate: true,
noHandle: true,
label: pid,
type: 'bezier',
offset: i * 20
const inputs = x[1]
inputs?.forEach((input, index) => {
let pid = x[0]
if (input?.startsWith('flow_input.iter')) {
const parent = dfsByModule($selectedId!, modules ?? [])?.pop()
if (parent?.id) {
pid = parent.id
}
}
edges.push({
id: `dep-${pid}-${$selectedId}`,
source: pid,
target: $selectedId!,
labelBgColor: darkMode ? '#999' : 'white',
edgeColor: darkMode ? 'white' : 'black',
arrow: false,
animate: true,
noHandle: true,
label: pid,
type: 'bezier',
offset: index * 20
})
})
})