diff --git a/src/ui/src/builder/settings/useBuilderSettingsActions.spec.ts b/src/ui/src/builder/settings/useBuilderSettingsActions.spec.ts index 0142f6c40..ec952bcf2 100644 --- a/src/ui/src/builder/settings/useBuilderSettingsActions.spec.ts +++ b/src/ui/src/builder/settings/useBuilderSettingsActions.spec.ts @@ -30,7 +30,7 @@ describe("useBuilderSettingsActions", () => { wfbm, ); - expect(dropdownOptions.value).toHaveLength(8); + expect(dropdownOptions.value).toHaveLength(9); for (const option of dropdownOptions.value) { expect(option.disabled).toBe(true); @@ -52,7 +52,7 @@ describe("useBuilderSettingsActions", () => { wfbm, ); - expect(dropdownOptions.value).toHaveLength(8); + expect(dropdownOptions.value).toHaveLength(9); expect( dropdownOptions.value.find( @@ -89,7 +89,7 @@ describe("useBuilderSettingsActions", () => { wfbm, ); - expect(dropdownOptions.value).toHaveLength(8); + expect(dropdownOptions.value).toHaveLength(9); expect( dropdownOptions.value.find( diff --git a/src/ui/src/builder/settings/useBuilderSettingsActions.ts b/src/ui/src/builder/settings/useBuilderSettingsActions.ts index 2431c0bf6..c1d566997 100644 --- a/src/ui/src/builder/settings/useBuilderSettingsActions.ts +++ b/src/ui/src/builder/settings/useBuilderSettingsActions.ts @@ -8,6 +8,7 @@ import { Option } from "@/components/shared/SharedMoreDropdown.vue"; export enum BuilderSettingsDropdownActions { Add = "add", + Run = "run", MoveUp = "moveUp", MoveDown = "moveDown", Cut = "cut", @@ -42,9 +43,13 @@ export function useBuilderSettingsActions( moveComponentUp, moveComponentDown, cutComponent, + runBlueprintFromComponent, + stopBlueprintFromComponent, + isBlueprintRunning, pasteComponent, copyComponent, isAddAllowed, + isRunAllowed, isCopyAllowed, isCutAllowed, isGoToParentAllowed, @@ -77,6 +82,7 @@ export function useBuilderSettingsActions( isAddEnabled: isAddAllowed(componentId.value), componentTypeName: wf.getComponentDefinition(component.type)?.name, toolkit: wf.getComponentDefinition(component.type)?.toolkit, + isRunEnabled: isRunAllowed(componentId.value), isMoveUpEnabled, isMoveDownEnabled, isCopyEnabled: isCopyAllowed(componentId.value), @@ -99,6 +105,22 @@ export function useBuilderSettingsActions( } } + async function handleRunBlueprint() { + try { + await runBlueprintFromComponent(componentId.value); + } catch (error) { + toasts.pushToast({ type: "error", message: String(error) }); + } + } + + async function handleStopBlueprint() { + try { + await stopBlueprintFromComponent(componentId.value); + } catch (error) { + toasts.pushToast({ type: "error", message: String(error) }); + } + } + function deleteComponent() { if (!shortcutsInfo.value.isDeleteEnabled) return; if (targetComponent) { @@ -118,6 +140,16 @@ export function useBuilderSettingsActions( icon: "plus", disabled: !shortcutsInfo.value.isAddEnabled, }, + { + value: BuilderSettingsDropdownActions.Run, + label: isBlueprintRunning(componentId.value) + ? "Stop run" + : "Run from here", + icon: isBlueprintRunning(componentId.value) ? "square" : "play", + disabled: isBlueprintRunning(componentId.value) + ? false + : !shortcutsInfo.value.isRunEnabled, + }, { value: BuilderSettingsDropdownActions.MoveUp, label: `Move up`, @@ -178,6 +210,13 @@ export function useBuilderSettingsActions( case BuilderSettingsDropdownActions.Add: // Handled by callback break; + case BuilderSettingsDropdownActions.Run: + if (isBlueprintRunning(componentId.value)) { + handleStopBlueprint(); + } else { + handleRunBlueprint(); + } + break; case BuilderSettingsDropdownActions.MoveUp: moveComponentUp(componentId.value); break; diff --git a/src/ui/src/builder/useComponentActions.ts b/src/ui/src/builder/useComponentActions.ts index 8945af5dc..e7e843d23 100644 --- a/src/ui/src/builder/useComponentActions.ts +++ b/src/ui/src/builder/useComponentActions.ts @@ -5,6 +5,7 @@ import { useComponentClipboard } from "./useComponentClipboard"; import { COMPONENT_TYPES_ROOT } from "@/constants/component"; import { getComponentPage } from "@/composables/useComponentPage"; import { SHARED_BLUEPRINT_FLAG_VALUE } from "@/utils/sharedBlueprint"; +import { useBlueprintRun } from "@/composables/useBlueprintRun"; export function useComponentActions( wf: Core, @@ -420,6 +421,35 @@ export function useComponentActions( return removeComponentsSubtree(componentId); } + /** + * Runs a blueprint from a target component. + */ + async function runBlueprintFromComponent( + componentId: Component["id"], + ): Promise { + const { run } = useBlueprintRun(wf, ssbm, componentId); + await run(componentId); + } + + /** + * Stops a running blueprint. + */ + async function stopBlueprintFromComponent( + componentId: Component["id"], + ): Promise { + const { stop } = useBlueprintRun(wf, ssbm, componentId); + await stop(); + } + + /** + * Checks if a blueprint is currently running. + * Note: This checks if any blueprint is running, not specifically for the given component. + */ + function isBlueprintRunning(_componentId: Component["id"]): boolean { + const { isRunning } = useBlueprintRun(wf, ssbm, _componentId); + return isRunning.value !== null; + } + /** * Whether a target component is the root */ @@ -446,6 +476,12 @@ export function useComponentActions( ); } + /** + * Whether the blueprint can be run from the target component. + */ + function isRunAllowed(targetId: Component["id"]): boolean { + return !isRoot(targetId); + } /** * Whether a component can be copied into the clipboard. */ @@ -1227,6 +1263,9 @@ export function useComponentActions( copyComponent, pasteComponent, createAndInsertComponent, + runBlueprintFromComponent, + stopBlueprintFromComponent, + isBlueprintRunning, createAndInsertComponentsTree, removeComponentSubtree, removeComponentsSubtree, @@ -1243,6 +1282,7 @@ export function useComponentActions( getUndoRedoSnapshot, setHandlerValue, isAddAllowed, + isRunAllowed, isCopyAllowed, isCutAllowed, isDeleteAllowed, diff --git a/src/ui/src/components/blueprints/base/BlueprintToolbar.vue b/src/ui/src/components/blueprints/base/BlueprintToolbar.vue index 510012996..a588aecae 100644 --- a/src/ui/src/components/blueprints/base/BlueprintToolbar.vue +++ b/src/ui/src/components/blueprints/base/BlueprintToolbar.vue @@ -95,6 +95,7 @@ async function runBlueprint(componentId?: string) { Publish blueprint + , ) { - const isRunning = ref(false); - async function run(branchId?: string) { - if (isRunning.value) return; - isRunning.value = true; - try { - await runBlueprint(wf, unref(blueprintComponentId), branchId); - } finally { - isRunning.value = false; - } + if (wfbm.activeBlueprintRunId.value) return; + await runBlueprint(wf, unref(blueprintComponentId), branchId); } - async function stop() { const activeRunId = wfbm.activeBlueprintRunId.value; if (!activeRunId) return; await stopBlueprintRun(wf, activeRunId); } - return { isRunning: readonly(isRunning), run, stop }; + return { isRunning: readonly(wfbm.activeBlueprintRunId), run, stop }; } export type BlueprintsRunListItem = { blueprintId: string; branchId: string }; diff --git a/src/writer/blueprints.py b/src/writer/blueprints.py index 0089e5bb3..fde9ba049 100644 --- a/src/writer/blueprints.py +++ b/src/writer/blueprints.py @@ -300,6 +300,7 @@ def _get_blueprint_nodes(self, component_id): current_node_id = node.parentId return [] + def run_branch( self, start_node_id: str, @@ -321,6 +322,7 @@ def run_branch( execution_environment, self, title=title ).run() + def run_branch_batch( self, base_component_id: str, base_outcome: str, execution_environments: List[Dict] ): @@ -333,6 +335,9 @@ def run_branch_batch( results.append(result) return results + + + def run_blueprint( self, component_id: str, execution_environment: Dict, title="Blueprint execution"