Skip to content

Commit 1a61afb

Browse files
merge
2 parents 9f099db + 5194d4e commit 1a61afb

28 files changed

Lines changed: 653 additions & 212 deletions

File tree

ai-prompts/chat-name.txt

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
You are an AI assistant on the OpenOps platform, where users interact about FinOps, cloud providers (AWS, Azure, GCP), OpenOps features, and workflow automation.
22

3-
Your task:
4-
Given the following conversation, suggest a short, descriptive name (max five words) that best summarizes the main topic, question, or action discussed in this chat.
3+
Task:
4+
Analyze the provided conversation and attempt to produce a concise chat name describing the main topic, question, or action.
55

6-
Guidelines:
7-
- The name should be specific (not generic like "Chat" or "Conversation"), and reflect the user's intent (e.g., "AWS Cost Optimization", "Create Budget Workflow", "OpenOps Integration Help").
8-
- Limit the name to five words or less.
9-
- Respond with only the chat name.
6+
Rules:
7+
- If you can confidently produce a specific, helpful name (not generic like "Chat" or "Conversation"), set `isGenerated` to true and provide `name`.
8+
- The `name` must be five words or fewer
9+
- If there is insufficient information, the content is unclear, or you cannot determine a good name, set `isGenerated` to false.
10+
11+
Notes:
12+
- Keep the name short and specific.
13+
- Avoid quotes, punctuation-heavy outputs, or trailing spaces in the name.

packages/react-ui/src/app/features/ai/lib/ai-assistant-chat-history-api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { api } from '@/app/lib/api';
2-
import { ListChatsResponse } from '@openops/shared';
2+
import { GeneratedChatName, ListChatsResponse } from '@openops/shared';
33

44
export const aiAssistantChatHistoryApi = {
55
list() {
@@ -9,7 +9,7 @@ export const aiAssistantChatHistoryApi = {
99
return api.delete<void>(`/v1/ai/conversation/${chatId}`);
1010
},
1111
generateName(chatId: string) {
12-
return api.post<{ chatName: string }>('/v1/ai/conversation/chat-name', {
12+
return api.post<GeneratedChatName>('/v1/ai/conversation/chat-name', {
1313
chatId,
1414
});
1515
},

packages/react-ui/src/app/features/ai/lib/assistant-ui-chat-hook.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,10 @@ export const useAssistantChat = ({
243243
if (messagesRef.current.length >= MIN_MESSAGES_BEFORE_NAME_GENERATION) {
244244
try {
245245
hasAttemptedNameGenerationRef.current[chatId] = true;
246-
await aiAssistantChatHistoryApi.generateName(chatId);
246+
const result = await aiAssistantChatHistoryApi.generateName(chatId);
247+
if (!result.isGenerated) {
248+
hasAttemptedNameGenerationRef.current[chatId] = false;
249+
}
247250
qc.invalidateQueries({ queryKey: [QueryKeys.assistantHistory] });
248251
} catch (error) {
249252
console.error('Failed to generate chat name', error);

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/builder/step-settings/block-settings/connection-select.tsx

Lines changed: 82 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import {
1818
removeConnectionBrackets,
1919
} from '@openops/shared';
2020
import { t } from 'i18next';
21-
import { Plus } from 'lucide-react';
21+
import { PencilLine, Plus, X } from 'lucide-react';
22+
import type React from 'react';
2223
import { memo, useCallback, useState } from 'react';
2324
import { ControllerRenderProps, useFormContext } from 'react-hook-form';
2425

@@ -38,6 +39,7 @@ type ConnectionSelectProps = {
3839
allowDynamicValues: boolean;
3940
providerKey: string;
4041
name: string;
42+
displayName?: string;
4143
};
4244

4345
const ConnectionSelect = memo((params: ConnectionSelectProps) => {
@@ -68,9 +70,9 @@ const ConnectionSelect = memo((params: ConnectionSelectProps) => {
6870
);
6971

7072
const handleReconnectClick = useCallback(
71-
(e: React.MouseEvent<HTMLButtonElement>) => {
72-
e.preventDefault();
73-
e.stopPropagation();
73+
(e?: React.SyntheticEvent<HTMLElement>) => {
74+
e?.preventDefault?.();
75+
e?.stopPropagation?.();
7476
const currentValue = form.getValues(params.name);
7577
const connectionName = removeConnectionBrackets(currentValue ?? '');
7678

@@ -85,6 +87,26 @@ const ConnectionSelect = memo((params: ConnectionSelectProps) => {
8587
[connectionsPage?.data, form, params.name],
8688
);
8789

90+
const suppressPointerOrMouseDown = useCallback(
91+
(e: React.PointerEvent<HTMLElement> | React.MouseEvent<HTMLElement>) => {
92+
e.stopPropagation();
93+
e.preventDefault();
94+
},
95+
[],
96+
);
97+
98+
const makeActivationKeysHandler = (
99+
action: (e: React.KeyboardEvent<HTMLElement>) => void,
100+
) => {
101+
return (e: React.KeyboardEvent<HTMLElement>) => {
102+
if (e.key === ' ' || e.key === 'Enter') {
103+
e.stopPropagation();
104+
e.preventDefault();
105+
action(e);
106+
}
107+
};
108+
};
109+
88110
return (
89111
<FormField
90112
control={form.control}
@@ -100,7 +122,11 @@ const ConnectionSelect = memo((params: ConnectionSelectProps) => {
100122
)}
101123
{!isLoading && (
102124
<AutoFormFieldWrapper
103-
property={params.block.auth!}
125+
property={{
126+
...params.block.auth!,
127+
displayName:
128+
params.displayName ?? params.block.auth?.displayName ?? '',
129+
}}
104130
propertyName="auth"
105131
field={field as unknown as ControllerRenderProps}
106132
disabled={params.disabled}
@@ -138,45 +164,68 @@ const ConnectionSelect = memo((params: ConnectionSelectProps) => {
138164
}}
139165
disabled={params.disabled}
140166
>
141-
<div className="relative">
142-
{field.value && !field.disabled && (
143-
<Button
144-
type="button"
145-
variant="ghost"
146-
size="xs"
147-
className="z-50 absolute right-8 top-2 "
148-
onClick={handleReconnectClick}
149-
>
150-
{t('Reconnect')}
151-
</Button>
152-
)}
153-
154-
<SelectTrigger className="flex gap-2 items-center">
167+
<SelectTrigger
168+
className="h-14 flex items-center gap-2"
169+
iconClassName="size-5"
170+
>
171+
<div className="flex-1 min-w-0 overflow-hidden">
155172
<SelectValue
156-
className="truncate flex-grow flex-shrink"
157-
placeholder={t('Select a connection')}
173+
placeholder={
174+
<span className="text-start block">
175+
{t('Select a connection')}
176+
</span>
177+
}
158178
>
159179
{!!field.value && (
160-
<div className="truncate flex-grow flex-shrink">
180+
<span className="text-start block truncate text-primary-700 text-base font-medium">
161181
{removeConnectionBrackets(field.value)}
162-
</div>
182+
</span>
163183
)}
164184
</SelectValue>
165-
<div className="grow"></div>
166-
{/* Hidden Button to take same space as shown button */}
167-
{field.value && (
185+
</div>
186+
187+
{field.value && !field.disabled && !params.disabled && (
188+
<div className="shrink-0 flex items-center gap-1">
189+
{selectConnectionOpen && (
190+
<Button
191+
type="button"
192+
variant="ghost"
193+
size="xs"
194+
className="text-primary-700 text-base font-medium"
195+
onPointerDown={suppressPointerOrMouseDown}
196+
onMouseDown={suppressPointerOrMouseDown}
197+
onKeyDown={makeActivationKeysHandler((e) =>
198+
handleReconnectClick(e),
199+
)}
200+
onClick={(e) => {
201+
handleReconnectClick(e);
202+
}}
203+
>
204+
<PencilLine size={16} />
205+
{t('Edit')}
206+
</Button>
207+
)}
208+
168209
<Button
169210
type="button"
170211
variant="ghost"
171212
size="xs"
172-
className="z-50 opacity-0 pointer-events-none"
213+
className="text-primary-700 text-base font-medium"
214+
onPointerDown={suppressPointerOrMouseDown}
215+
onMouseDown={suppressPointerOrMouseDown}
216+
onKeyDown={makeActivationKeysHandler(() => {
217+
field.onChange('');
218+
})}
219+
onClick={() => {
220+
field.onChange('');
221+
}}
173222
>
174-
{t('Reconnect')}
223+
<X size={16} />
224+
{t('Clear')}
175225
</Button>
176-
)}
177-
</SelectTrigger>
178-
</div>
179-
226+
</div>
227+
)}
228+
</SelectTrigger>
180229
<SelectContent>
181230
<SelectAction
182231
onClick={() => {
@@ -187,7 +236,7 @@ const ConnectionSelect = memo((params: ConnectionSelectProps) => {
187236
>
188237
<span className="flex items-center gap-1 text-primary w-full">
189238
<Plus size={16} />
190-
{t('Create Connection')}
239+
{t('Create new connection')}
191240
</span>
192241
</SelectAction>
193242

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,

0 commit comments

Comments
 (0)