Skip to content

Commit 3d591b0

Browse files
Add visual indicator for sensitive actions (#1736)
<!-- Ensure the title clearly reflects what was changed. Provide a clear and concise description of the changes made. The PR should only contain the changes related to the issue, and no other unrelated changes. --> Fixes OPS-2810 <img width="488" height="370" alt="image" src="https://github.com/user-attachments/assets/2ece287a-c309-4a35-81af-e9929b7858b9" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added visual risk indicators to highlight risky steps in the flow canvas, displaying a shield badge with tooltips explaining potential environmental impacts. * Enhanced validation indicators for incomplete steps, now consolidated with risk warnings for improved visibility and clarity. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 8e230cd commit 3d591b0

File tree

4 files changed

+107
-52
lines changed

4 files changed

+107
-52
lines changed

packages/react-ui/src/app/features/builder/flow-canvas/nodes/step-node.tsx

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ import {
3535
TriggerType,
3636
} from '@openops/shared';
3737

38+
import { flowsHooks } from '@/app/features/flows/lib/flows-hooks';
39+
import { ShieldHalf } from 'lucide-react';
3840
import { CanvasContextMenu } from '../context-menu/canvas-context-menu';
3941
import { CollapsibleButton } from './collapsible-button';
4042
import { StackedNodeLayers } from './stacked-node-layer';
@@ -86,6 +88,11 @@ const WorkflowStepNode = React.memo(
8688
step: data.step!,
8789
});
8890

91+
const { metadata: actionsMetadata } = blocksHooks.useAllStepsMetadata({
92+
searchQuery: '',
93+
type: 'action',
94+
});
95+
8996
const stepIndex = useMemo(() => {
9097
const steps = flowHelper.getAllSteps(flowVersion.trigger);
9198
return steps.findIndex((step) => step.name === data.step!.name) + 1;
@@ -122,6 +129,12 @@ const WorkflowStepNode = React.memo(
122129
return getStepStatus(data.step?.name, run, loopIndexes, flowVersion);
123130
}, [data.step?.name, run, loopIndexes, flowVersion]);
124131

132+
const isRiskyStep = flowsHooks.useIsRiskyAction(
133+
actionsMetadata,
134+
data.step?.settings.blockName,
135+
data.step?.settings.actionName,
136+
);
137+
125138
const showRunningIcon =
126139
isNil(stepOutputStatus) && run?.status === FlowRunStatus.RUNNING;
127140
const statusInfo = isNil(stepOutputStatus)
@@ -247,15 +260,49 @@ const WorkflowStepNode = React.memo(
247260
</div>
248261
</div>
249262

250-
{!readonly && (
251-
<CanvasContextMenu
252-
data={data}
253-
isAction={isAction}
254-
openStepActionsMenu={openStepActionsMenu}
255-
setOpenStepActionsMenu={setOpenStepActionsMenu}
256-
setOpenBlockSelector={setOpenBlockSelector}
257-
/>
258-
)}
263+
<div className="flex items-center gap-0.5">
264+
<div className="flex items-center gap-[7px]">
265+
{!data.step?.valid && (
266+
<Tooltip>
267+
<TooltipTrigger asChild>
268+
<InvalidStepIcon
269+
size={16}
270+
viewBox="0 0 16 16"
271+
className="stroke-0 animate-fade"
272+
></InvalidStepIcon>
273+
</TooltipTrigger>
274+
<TooltipContent side="bottom">
275+
{t('Incomplete settings')}
276+
</TooltipContent>
277+
</Tooltip>
278+
)}
279+
280+
{isRiskyStep && (
281+
<Tooltip>
282+
<TooltipTrigger asChild>
283+
<div className="size-4 flex items-center justify-center rounded-full bg-destructive-100">
284+
<ShieldHalf className="size-[10px] text-destructive-300"></ShieldHalf>
285+
</div>
286+
</TooltipTrigger>
287+
<TooltipContent side="bottom">
288+
{t(
289+
'This step may make changes to your environment',
290+
)}
291+
</TooltipContent>
292+
</Tooltip>
293+
)}
294+
</div>
295+
296+
{!readonly && (
297+
<CanvasContextMenu
298+
data={data}
299+
isAction={isAction}
300+
openStepActionsMenu={openStepActionsMenu}
301+
setOpenStepActionsMenu={setOpenStepActionsMenu}
302+
setOpenBlockSelector={setOpenBlockSelector}
303+
/>
304+
)}
305+
</div>
259306
</div>
260307

261308
<div className="flex justify-between gap-[6px] w-full items-center">
@@ -276,22 +323,6 @@ const WorkflowStepNode = React.memo(
276323
{showRunningIcon && (
277324
<LoadingSpinner className="w-4 h-4 text-primary"></LoadingSpinner>
278325
)}
279-
{!data.step?.valid && (
280-
<Tooltip>
281-
<TooltipTrigger asChild>
282-
<div className="mr-2">
283-
<InvalidStepIcon
284-
size={16}
285-
viewBox="0 0 16 16"
286-
className="stroke-0 animate-fade"
287-
></InvalidStepIcon>
288-
</div>
289-
</TooltipTrigger>
290-
<TooltipContent side="bottom">
291-
{t('Incomplete settings')}
292-
</TooltipContent>
293-
</Tooltip>
294-
)}
295326
</div>
296327
</div>
297328
</div>

packages/react-ui/src/app/features/flows/components/execute-risky-flow-dialog/utils.ts

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,9 @@
1-
import { ActionBase } from '@openops/blocks-framework';
2-
import {
3-
BlockStepMetadataWithSuggestions,
4-
StepMetadataWithSuggestions,
5-
} from '@openops/components/ui';
1+
import { flowsUtils } from '@/app/features/flows/lib/flows-utils';
2+
import { StepMetadataWithSuggestions } from '@openops/components/ui';
63
import { Action, ActionType, RiskLevel, Trigger } from '@openops/shared';
74

85
type ActionOrTriggerWithIndex = (Action | Trigger) & { index: number };
96

10-
const getActionMetadata = (
11-
metadata: StepMetadataWithSuggestions[] | undefined,
12-
blockName: string,
13-
actionName: string | undefined,
14-
): ActionBase | undefined => {
15-
const blockStepMetadata = metadata?.find(
16-
(stepMetadata: StepMetadataWithSuggestions) =>
17-
stepMetadata.type === ActionType.BLOCK &&
18-
(stepMetadata as BlockStepMetadataWithSuggestions).blockName ===
19-
blockName,
20-
) as BlockStepMetadataWithSuggestions | undefined;
21-
22-
return blockStepMetadata?.suggestedActions?.find(
23-
(suggestedAction) => suggestedAction.name === actionName,
24-
);
25-
};
26-
277
export const getRiskyActionFormattedNames = (
288
allSteps: (Action | Trigger)[],
299
metadata: StepMetadataWithSuggestions[] | undefined,
@@ -35,7 +15,7 @@ export const getRiskyActionFormattedNames = (
3515
.map((action) => {
3616
return {
3717
action,
38-
metadata: getActionMetadata(
18+
metadata: flowsUtils.getActionMetadata(
3919
metadata,
4020
action.settings.blockName,
4121
action.settings.actionName,

packages/react-ui/src/app/features/flows/lib/flows-hooks.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
import { INTERNAL_ERROR_TOAST, toast } from '@openops/components/ui';
2-
import { ListFlowsRequest, PopulatedFlow } from '@openops/shared';
1+
import {
2+
INTERNAL_ERROR_TOAST,
3+
StepMetadataWithSuggestions,
4+
toast,
5+
} from '@openops/components/ui';
6+
import { ListFlowsRequest, PopulatedFlow, RiskLevel } from '@openops/shared';
37
import { useMutation, useQuery } from '@tanstack/react-query';
48
import { t } from 'i18next';
5-
import { useState } from 'react';
9+
import { useMemo, useState } from 'react';
610
import { NavigateFunction } from 'react-router-dom';
711

12+
import { flowsUtils } from '@/app/features/flows/lib/flows-utils';
813
import { flowsApi } from './flows-api';
914

1015
import { userSettingsHooks } from '@/app/common/hooks/user-settings-hooks';
@@ -65,6 +70,22 @@ export const flowsHooks = {
6570
setSearchTerm,
6671
};
6772
},
73+
useIsRiskyAction: (
74+
metadata: StepMetadataWithSuggestions[] | undefined,
75+
76+
blockName: string | undefined,
77+
actionName: string | undefined,
78+
) => {
79+
return useMemo(() => {
80+
if (!metadata || !blockName || !actionName) return false;
81+
const actionMetadata = flowsUtils.getActionMetadata(
82+
metadata,
83+
blockName,
84+
actionName,
85+
);
86+
return actionMetadata?.riskLevel === RiskLevel.HIGH;
87+
}, [metadata, blockName, actionName]);
88+
},
6889
useCreateFlow: (navigate: NavigateFunction) => {
6990
const { updateHomePageOperationalViewFlag } =
7091
userSettingsHooks.useHomePageOperationalView();

packages/react-ui/src/app/features/flows/lib/flows-utils.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
import { ActionBase } from '@openops/blocks-framework';
2+
import {
3+
BlockStepMetadataWithSuggestions,
4+
StepMetadataWithSuggestions,
5+
} from '@openops/components/ui';
16
import cronstrue from 'cronstrue/i18n';
27
import { t } from 'i18next';
38
import { TimerReset, TriangleAlert, Zap } from 'lucide-react';
49

5-
import { Flow, FlowVersion, TriggerType } from '@openops/shared';
10+
import { ActionType, Flow, FlowVersion, TriggerType } from '@openops/shared';
611

712
import { flowsApi } from './flows-api';
813

@@ -29,8 +34,26 @@ const downloadFlow = async (flowId: string, versionId: string) => {
2934
downloadFile(JSON.stringify(template, null, 2), template.name, 'json');
3035
};
3136

37+
const getActionMetadata = (
38+
metadata: StepMetadataWithSuggestions[] | undefined,
39+
blockName: string,
40+
actionName: string | undefined,
41+
): ActionBase | undefined => {
42+
const blockStepMetadata = metadata?.find(
43+
(stepMetadata: StepMetadataWithSuggestions) =>
44+
stepMetadata.type === ActionType.BLOCK &&
45+
(stepMetadata as BlockStepMetadataWithSuggestions).blockName ===
46+
blockName,
47+
) as BlockStepMetadataWithSuggestions | undefined;
48+
49+
return blockStepMetadata?.suggestedActions?.find(
50+
(suggestedAction) => suggestedAction.name === actionName,
51+
);
52+
};
53+
3254
export const flowsUtils = {
3355
downloadFlow,
56+
getActionMetadata,
3457
flowStatusToolTipRenderer: (flow: Flow, version: FlowVersion) => {
3558
const trigger = version.trigger;
3659
switch (trigger.type) {

0 commit comments

Comments
 (0)