From a7d94debdc00abb3795abcdc68bdedce6558d3df Mon Sep 17 00:00:00 2001 From: Roman Snapko Date: Fri, 5 Dec 2025 11:33:59 +0100 Subject: [PATCH 1/4] Add visual indicator for sensitive actions --- .../builder/flow-canvas/nodes/step-node.tsx | 97 ++++++++++++++----- .../execute-risky-flow-dialog/utils.ts | 2 +- 2 files changed, 73 insertions(+), 26 deletions(-) diff --git a/packages/react-ui/src/app/features/builder/flow-canvas/nodes/step-node.tsx b/packages/react-ui/src/app/features/builder/flow-canvas/nodes/step-node.tsx index a6abdf69e7..183fc41ef9 100644 --- a/packages/react-ui/src/app/features/builder/flow-canvas/nodes/step-node.tsx +++ b/packages/react-ui/src/app/features/builder/flow-canvas/nodes/step-node.tsx @@ -32,9 +32,12 @@ import { FlowRunStatus, FlowVersion, isNil, + RiskLevel, TriggerType, } from '@openops/shared'; +import { getActionMetadata } from '@/app/features/flows/components/execute-risky-flow-dialog/utils'; +import { ShieldHalf } from 'lucide-react'; import { CanvasContextMenu } from '../context-menu/canvas-context-menu'; import { CollapsibleButton } from './collapsible-button'; import { StackedNodeLayers } from './stacked-node-layer'; @@ -86,6 +89,11 @@ const WorkflowStepNode = React.memo( step: data.step!, }); + const { metadata: actionsMetadata } = blocksHooks.useAllStepsMetadata({ + searchQuery: '', + type: 'action', + }); + const stepIndex = useMemo(() => { const steps = flowHelper.getAllSteps(flowVersion.trigger); return steps.findIndex((step) => step.name === data.step!.name) + 1; @@ -122,6 +130,27 @@ const WorkflowStepNode = React.memo( return getStepStatus(data.step?.name, run, loopIndexes, flowVersion); }, [data.step?.name, run, loopIndexes, flowVersion]); + const isRiskyStep = useMemo(() => { + const actionName = data.step?.settings.actionName; + const blockName = data.step?.settings.blockName; + + if (!actionsMetadata || !actionName || !blockName) { + return false; + } + + const actionMetadata = getActionMetadata( + actionsMetadata, + data.step?.settings.blockName, + data.step?.settings.actionName, + ); + + return actionMetadata?.riskLevel === RiskLevel.HIGH; + }, [ + actionsMetadata, + data.step?.settings.actionName, + data.step?.settings.blockName, + ]); + const showRunningIcon = isNil(stepOutputStatus) && run?.status === FlowRunStatus.RUNNING; const statusInfo = isNil(stepOutputStatus) @@ -247,15 +276,49 @@ const WorkflowStepNode = React.memo( - {!readonly && ( - - )} +
+
+ {!data.step?.valid && ( + + + + + + {t('Incomplete settings')} + + + )} + + {isRiskyStep && ( + + +
+ +
+
+ + {t( + 'This step may make changes to your environment', + )} + +
+ )} +
+ + {!readonly && ( + + )} +
@@ -276,22 +339,6 @@ const WorkflowStepNode = React.memo( {showRunningIcon && ( )} - {!data.step?.valid && ( - - -
- -
-
- - {t('Incomplete settings')} - -
- )}
diff --git a/packages/react-ui/src/app/features/flows/components/execute-risky-flow-dialog/utils.ts b/packages/react-ui/src/app/features/flows/components/execute-risky-flow-dialog/utils.ts index 19fe322267..79a3e52b08 100644 --- a/packages/react-ui/src/app/features/flows/components/execute-risky-flow-dialog/utils.ts +++ b/packages/react-ui/src/app/features/flows/components/execute-risky-flow-dialog/utils.ts @@ -7,7 +7,7 @@ import { Action, ActionType, RiskLevel, Trigger } from '@openops/shared'; type ActionOrTriggerWithIndex = (Action | Trigger) & { index: number }; -const getActionMetadata = ( +export const getActionMetadata = ( metadata: StepMetadataWithSuggestions[] | undefined, blockName: string, actionName: string | undefined, From 84bdedd74e0c1abcaf0f67a1dadf42924822c70b Mon Sep 17 00:00:00 2001 From: Roman Snapko Date: Fri, 5 Dec 2025 16:58:17 +0100 Subject: [PATCH 2/4] chore --- .../src/app/features/builder/flow-canvas/nodes/step-node.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-ui/src/app/features/builder/flow-canvas/nodes/step-node.tsx b/packages/react-ui/src/app/features/builder/flow-canvas/nodes/step-node.tsx index 183fc41ef9..ce571aa091 100644 --- a/packages/react-ui/src/app/features/builder/flow-canvas/nodes/step-node.tsx +++ b/packages/react-ui/src/app/features/builder/flow-canvas/nodes/step-node.tsx @@ -140,8 +140,8 @@ const WorkflowStepNode = React.memo( const actionMetadata = getActionMetadata( actionsMetadata, - data.step?.settings.blockName, - data.step?.settings.actionName, + blockName, + actionName, ); return actionMetadata?.riskLevel === RiskLevel.HIGH; From 997d492adae5e9632abd287f0e79877981018de6 Mon Sep 17 00:00:00 2001 From: Roman Snapko Date: Mon, 8 Dec 2025 15:07:41 +0100 Subject: [PATCH 3/4] Refactor action metadata logic to centralize in flows-utils and replace redundant implementations --- .../builder/flow-canvas/nodes/step-node.tsx | 24 +++-------------- .../execute-risky-flow-dialog/utils.ts | 26 +++--------------- .../src/app/features/flows/lib/flows-hooks.ts | 27 ++++++++++++++++--- .../app/features/flows/lib/flows-utils.tsx | 25 ++++++++++++++++- 4 files changed, 55 insertions(+), 47 deletions(-) diff --git a/packages/react-ui/src/app/features/builder/flow-canvas/nodes/step-node.tsx b/packages/react-ui/src/app/features/builder/flow-canvas/nodes/step-node.tsx index ce571aa091..89d0f90579 100644 --- a/packages/react-ui/src/app/features/builder/flow-canvas/nodes/step-node.tsx +++ b/packages/react-ui/src/app/features/builder/flow-canvas/nodes/step-node.tsx @@ -32,11 +32,10 @@ import { FlowRunStatus, FlowVersion, isNil, - RiskLevel, TriggerType, } from '@openops/shared'; -import { getActionMetadata } from '@/app/features/flows/components/execute-risky-flow-dialog/utils'; +import { flowsHooks } from '@/app/features/flows/lib/flows-hooks'; import { ShieldHalf } from 'lucide-react'; import { CanvasContextMenu } from '../context-menu/canvas-context-menu'; import { CollapsibleButton } from './collapsible-button'; @@ -130,26 +129,11 @@ const WorkflowStepNode = React.memo( return getStepStatus(data.step?.name, run, loopIndexes, flowVersion); }, [data.step?.name, run, loopIndexes, flowVersion]); - const isRiskyStep = useMemo(() => { - const actionName = data.step?.settings.actionName; - const blockName = data.step?.settings.blockName; - - if (!actionsMetadata || !actionName || !blockName) { - return false; - } - - const actionMetadata = getActionMetadata( - actionsMetadata, - blockName, - actionName, - ); - - return actionMetadata?.riskLevel === RiskLevel.HIGH; - }, [ + const isRiskyStep = flowsHooks.useIsRiskyAction( actionsMetadata, - data.step?.settings.actionName, data.step?.settings.blockName, - ]); + data.step?.settings.actionName, + ); const showRunningIcon = isNil(stepOutputStatus) && run?.status === FlowRunStatus.RUNNING; diff --git a/packages/react-ui/src/app/features/flows/components/execute-risky-flow-dialog/utils.ts b/packages/react-ui/src/app/features/flows/components/execute-risky-flow-dialog/utils.ts index 79a3e52b08..99e621c0b7 100644 --- a/packages/react-ui/src/app/features/flows/components/execute-risky-flow-dialog/utils.ts +++ b/packages/react-ui/src/app/features/flows/components/execute-risky-flow-dialog/utils.ts @@ -1,29 +1,9 @@ -import { ActionBase } from '@openops/blocks-framework'; -import { - BlockStepMetadataWithSuggestions, - StepMetadataWithSuggestions, -} from '@openops/components/ui'; +import { StepMetadataWithSuggestions } from '@openops/components/ui'; import { Action, ActionType, RiskLevel, Trigger } from '@openops/shared'; +import { flowsUtils } from '@/app/features/flows/lib/flows-utils'; type ActionOrTriggerWithIndex = (Action | Trigger) & { index: number }; -export const getActionMetadata = ( - metadata: StepMetadataWithSuggestions[] | undefined, - blockName: string, - actionName: string | undefined, -): ActionBase | undefined => { - const blockStepMetadata = metadata?.find( - (stepMetadata: StepMetadataWithSuggestions) => - stepMetadata.type === ActionType.BLOCK && - (stepMetadata as BlockStepMetadataWithSuggestions).blockName === - blockName, - ) as BlockStepMetadataWithSuggestions | undefined; - - return blockStepMetadata?.suggestedActions?.find( - (suggestedAction) => suggestedAction.name === actionName, - ); -}; - export const getRiskyActionFormattedNames = ( allSteps: (Action | Trigger)[], metadata: StepMetadataWithSuggestions[] | undefined, @@ -35,7 +15,7 @@ export const getRiskyActionFormattedNames = ( .map((action) => { return { action, - metadata: getActionMetadata( + metadata: flowsUtils.getActionMetadata( metadata, action.settings.blockName, action.settings.actionName, diff --git a/packages/react-ui/src/app/features/flows/lib/flows-hooks.ts b/packages/react-ui/src/app/features/flows/lib/flows-hooks.ts index b9d18b3545..3301a81215 100644 --- a/packages/react-ui/src/app/features/flows/lib/flows-hooks.ts +++ b/packages/react-ui/src/app/features/flows/lib/flows-hooks.ts @@ -1,10 +1,15 @@ -import { INTERNAL_ERROR_TOAST, toast } from '@openops/components/ui'; -import { ListFlowsRequest, PopulatedFlow } from '@openops/shared'; +import { + INTERNAL_ERROR_TOAST, + StepMetadataWithSuggestions, + toast, +} from '@openops/components/ui'; +import { ListFlowsRequest, PopulatedFlow, RiskLevel } from '@openops/shared'; import { useMutation, useQuery } from '@tanstack/react-query'; import { t } from 'i18next'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { NavigateFunction } from 'react-router-dom'; +import { flowsUtils } from '@/app/features/flows/lib/flows-utils'; import { flowsApi } from './flows-api'; import { userSettingsHooks } from '@/app/common/hooks/user-settings-hooks'; @@ -65,6 +70,22 @@ export const flowsHooks = { setSearchTerm, }; }, + useIsRiskyAction: ( + metadata: StepMetadataWithSuggestions[] | undefined, + + blockName: string | undefined, + actionName: string | undefined, + ) => { + return useMemo(() => { + if (!metadata || !blockName || !actionName) return false; + const actionMetadata = flowsUtils.getActionMetadata( + metadata, + blockName, + actionName, + ); + return actionMetadata?.riskLevel === RiskLevel.HIGH; + }, [metadata, blockName, actionName]); + }, useCreateFlow: (navigate: NavigateFunction) => { const { updateHomePageOperationalViewFlag } = userSettingsHooks.useHomePageOperationalView(); diff --git a/packages/react-ui/src/app/features/flows/lib/flows-utils.tsx b/packages/react-ui/src/app/features/flows/lib/flows-utils.tsx index d0d694d18e..765b645457 100644 --- a/packages/react-ui/src/app/features/flows/lib/flows-utils.tsx +++ b/packages/react-ui/src/app/features/flows/lib/flows-utils.tsx @@ -1,8 +1,13 @@ +import { ActionBase } from '@openops/blocks-framework'; +import { + BlockStepMetadataWithSuggestions, + StepMetadataWithSuggestions, +} from '@openops/components/ui'; import cronstrue from 'cronstrue/i18n'; import { t } from 'i18next'; import { TimerReset, TriangleAlert, Zap } from 'lucide-react'; -import { Flow, FlowVersion, TriggerType } from '@openops/shared'; +import { ActionType, Flow, FlowVersion, TriggerType } from '@openops/shared'; import { flowsApi } from './flows-api'; @@ -29,8 +34,26 @@ const downloadFlow = async (flowId: string, versionId: string) => { downloadFile(JSON.stringify(template, null, 2), template.name, 'json'); }; +const getActionMetadata = ( + metadata: StepMetadataWithSuggestions[] | undefined, + blockName: string, + actionName: string | undefined, +): ActionBase | undefined => { + const blockStepMetadata = metadata?.find( + (stepMetadata: StepMetadataWithSuggestions) => + stepMetadata.type === ActionType.BLOCK && + (stepMetadata as BlockStepMetadataWithSuggestions).blockName === + blockName, + ) as BlockStepMetadataWithSuggestions | undefined; + + return blockStepMetadata?.suggestedActions?.find( + (suggestedAction) => suggestedAction.name === actionName, + ); +}; + export const flowsUtils = { downloadFlow, + getActionMetadata, flowStatusToolTipRenderer: (flow: Flow, version: FlowVersion) => { const trigger = version.trigger; switch (trigger.type) { From b2906bd3d9624bbe9fd3ee79b0837c1239c29343 Mon Sep 17 00:00:00 2001 From: Roman Snapko Date: Tue, 9 Dec 2025 08:23:28 +0100 Subject: [PATCH 4/4] fix: lint --- .../flows/components/execute-risky-flow-dialog/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-ui/src/app/features/flows/components/execute-risky-flow-dialog/utils.ts b/packages/react-ui/src/app/features/flows/components/execute-risky-flow-dialog/utils.ts index 99e621c0b7..38256e7b57 100644 --- a/packages/react-ui/src/app/features/flows/components/execute-risky-flow-dialog/utils.ts +++ b/packages/react-ui/src/app/features/flows/components/execute-risky-flow-dialog/utils.ts @@ -1,6 +1,6 @@ +import { flowsUtils } from '@/app/features/flows/lib/flows-utils'; import { StepMetadataWithSuggestions } from '@openops/components/ui'; import { Action, ActionType, RiskLevel, Trigger } from '@openops/shared'; -import { flowsUtils } from '@/app/features/flows/lib/flows-utils'; type ActionOrTriggerWithIndex = (Action | Trigger) & { index: number };