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..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 @@ -35,6 +35,8 @@ import { TriggerType, } from '@openops/shared'; +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'; import { StackedNodeLayers } from './stacked-node-layer'; @@ -86,6 +88,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 +129,12 @@ const WorkflowStepNode = React.memo( return getStepStatus(data.step?.name, run, loopIndexes, flowVersion); }, [data.step?.name, run, loopIndexes, flowVersion]); + const isRiskyStep = flowsHooks.useIsRiskyAction( + actionsMetadata, + data.step?.settings.blockName, + data.step?.settings.actionName, + ); + const showRunningIcon = isNil(stepOutputStatus) && run?.status === FlowRunStatus.RUNNING; const statusInfo = isNil(stepOutputStatus) @@ -247,15 +260,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 +323,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..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,29 +1,9 @@ -import { ActionBase } from '@openops/blocks-framework'; -import { - BlockStepMetadataWithSuggestions, - StepMetadataWithSuggestions, -} from '@openops/components/ui'; +import { flowsUtils } from '@/app/features/flows/lib/flows-utils'; +import { StepMetadataWithSuggestions } from '@openops/components/ui'; import { Action, ActionType, RiskLevel, Trigger } from '@openops/shared'; type ActionOrTriggerWithIndex = (Action | Trigger) & { index: number }; -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) {