Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions static/app/views/automations/components/actionNodeList.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,30 @@ describe('ActionNodeList', () => {
expect(mockOnDeleteRow).toHaveBeenCalledWith(slackAction.id);
});

it('shows an error for actions with unavailable handlers', async () => {
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/available-actions/`,
body: [], // No available actions
});

const slackAction = ActionFixture();
render(
<AutomationBuilderErrorContext.Provider value={defaultErrorContextProps}>
<ActionNodeList {...defaultProps} actions={[slackAction]} />
</AutomationBuilderErrorContext.Provider>,
{
organization,
}
);

expect(
await screen.findByText(
'The Slack action is no longer available. Please remove and reconfigure this action.'
)
).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Delete row'})).toBeInTheDocument();
});

it('shows a warning message for an incompatible action', async () => {
const model = new FormModel();
model.setInitialData({
Expand Down
29 changes: 27 additions & 2 deletions static/app/views/automations/components/actionNodeList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ export function ActionNodeList({
onDeleteRow,
updateAction,
}: ActionNodeListProps) {
const {data: availableActions = []} = useAvailableActionsQuery();
const {data: availableActions = [], isLoading: isLoadingActions} =
useAvailableActionsQuery();
const {errors, removeError} = useAutomationBuilderErrorContext();
const {connectedDetectors} = useConnectedDetectors();

Expand Down Expand Up @@ -110,9 +111,33 @@ export function ActionNodeList({
return (
<Fragment>
{actions.map(action => {
if (isLoadingActions) {
return null;
}
const handler = getActionHandler(action, availableActions);
if (!handler) {
return null;
const actionLabel = actionNodesMap.get(action.type)?.label;
return (
<AutomationBuilderRow
key={`actionFilters.${conditionGroupId}.action.${action.id}`}
onDelete={() => {
onDeleteRow(action.id);
}}
hasError
errorMessage={
actionLabel
? t(
'The %s action is no longer available. Please remove and reconfigure this action.',
actionLabel
)
: t(
'The integration is no longer available. Please remove and reconfigure this action.'
)
}
>
{actionLabel ?? t('Unknown integration')}
</AutomationBuilderRow>
);
Comment thread
malwilley marked this conversation as resolved.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API failure misreported as permanently unavailable actions

Medium Severity

When the useAvailableActionsQuery call fails (network error, 500, etc.), isLoadingActions becomes false and availableActions defaults to []. This causes getActionHandler to return undefined for every action, triggering the "no longer available" error message for all of them. Users could then be misled into deleting perfectly valid actions. The code checks isLoadingActions but not isError from the query, so a transient API failure is indistinguishable from a genuinely removed integration.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 81a7f88. Configure here.

}
const error = errors?.[action.id];
const warningMessage = getIncompatibleActionWarning(action, {
Expand Down
Loading