Skip to content

Commit 0532e5d

Browse files
committed
ref(aci): ref actionNodeContext to include action handler
1 parent d858161 commit 0532e5d

File tree

10 files changed

+195
-31
lines changed

10 files changed

+195
-31
lines changed

static/app/types/workflowEngine/actions.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,28 @@ export enum ActionType {
1515
GITHUB_ENTERPRISE = 'github_enterprise',
1616
JIRA = 'jira',
1717
JIRA_SERVER = 'jira_server',
18-
AZURE_DEVOPS = 'azure_devops',
18+
AZURE_DEVOPS = 'vsts',
1919
EMAIL = 'email',
2020
SENTRY_APP = 'sentry_app',
2121
PLUGIN = 'plugin',
2222
WEBHOOK = 'webhook',
2323
}
2424

25+
export enum ActionGroup {
26+
NOTIFICATION = 'notification',
27+
TICKET_CREATION = 'ticket_creation',
28+
OTHER = 'other',
29+
}
30+
31+
export interface ActionHandler {
32+
configSchema: Record<string, any>;
33+
dataSchema: Record<string, any>;
34+
handlerGroup: ActionGroup;
35+
type: ActionType;
36+
integrations?: Integration[];
37+
sentryApp?: SentryAppContext;
38+
services?: PluginService[];
39+
}
2540
export interface Integration {
2641
id: string;
2742
name: string;
@@ -30,3 +45,17 @@ export interface Integration {
3045
name: string;
3146
}>;
3247
}
48+
49+
export interface SentryAppContext {
50+
id: string;
51+
installationId: string;
52+
name: string;
53+
status: number;
54+
settings?: Record<string, any>;
55+
title?: string;
56+
}
57+
58+
export interface PluginService {
59+
name: string;
60+
slug: string;
61+
}

static/app/views/automations/components/actionNodeList.tsx

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
1-
import {Fragment} from 'react';
1+
import {Fragment, useMemo, useRef} from 'react';
22
import styled from '@emotion/styled';
3+
import {uuid4} from '@sentry/core';
34

45
import {Select} from 'sentry/components/core/select';
5-
import type {Action, ActionType, Integration} from 'sentry/types/workflowEngine/actions';
6+
import {t} from 'sentry/locale';
7+
import {
8+
type Action,
9+
ActionGroup,
10+
type ActionHandler,
11+
} from 'sentry/types/workflowEngine/actions';
612
import {
713
ActionNodeContext,
814
actionNodesMap,
915
useActionNodeContext,
1016
} from 'sentry/views/automations/components/actionNodes';
1117
import AutomationBuilderRow from 'sentry/views/automations/components/automationBuilderRow';
18+
import {useAvailableActionsQuery} from 'sentry/views/automations/hooks';
1219

1320
interface ActionNodeListProps {
1421
actions: Action[];
15-
availableActions: Array<{type: ActionType; integrations?: Integration[]}>;
1622
group: string;
17-
onAddRow: (type: ActionType) => void;
23+
onAddRow: (actionId: string, actionHandler: ActionHandler) => void;
1824
onDeleteRow: (id: string) => void;
1925
placeholder: string;
2026
updateAction: (id: string, data: Record<string, any>) => void;
@@ -24,14 +30,51 @@ export default function ActionNodeList({
2430
group,
2531
placeholder,
2632
actions,
27-
availableActions,
2833
onAddRow,
2934
onDeleteRow,
3035
updateAction,
3136
}: ActionNodeListProps) {
32-
const options = Array.from(actionNodesMap)
33-
.filter(([value]) => availableActions.some(action => action.type === value))
34-
.map(([value, {label}]) => ({value, label}));
37+
const {data: availableActions = []} = useAvailableActionsQuery();
38+
const actionHandlerMapRef = useRef(new Map());
39+
40+
const options = useMemo(() => {
41+
const typeOptionsMap = new Map<
42+
ActionGroup,
43+
Array<{label: string; value: ActionHandler}>
44+
>();
45+
46+
availableActions.forEach(action => {
47+
const existingOptions = typeOptionsMap.get(action.handlerGroup) || [];
48+
const label =
49+
actionNodesMap.get(action.type)?.label || action.sentryApp?.name || action.type;
50+
51+
typeOptionsMap.set(action.handlerGroup, [
52+
...existingOptions,
53+
{
54+
value: action,
55+
label,
56+
},
57+
]);
58+
});
59+
60+
return [
61+
{
62+
key: ActionGroup.NOTIFICATION,
63+
label: t('Notifications'),
64+
options: typeOptionsMap.get(ActionGroup.NOTIFICATION) || [],
65+
},
66+
{
67+
key: ActionGroup.TICKET_CREATION,
68+
label: t('Ticket Creation'),
69+
options: typeOptionsMap.get(ActionGroup.TICKET_CREATION) || [],
70+
},
71+
{
72+
key: ActionGroup.OTHER,
73+
label: t('Other Integrations'),
74+
options: typeOptionsMap.get(ActionGroup.OTHER) || [],
75+
},
76+
];
77+
}, [availableActions]);
3578

3679
return (
3780
<Fragment>
@@ -47,8 +90,7 @@ export default function ActionNodeList({
4790
action,
4891
actionId: `${group}.action.${action.id}`,
4992
onUpdate: newAction => updateAction(action.id, newAction),
50-
integrations: availableActions.find(a => a.type === action.type)
51-
?.integrations,
93+
handler: actionHandlerMapRef.current.get(action.id),
5294
}}
5395
>
5496
<Node />
@@ -58,7 +100,9 @@ export default function ActionNodeList({
58100
<StyledSelectControl
59101
options={options}
60102
onChange={(obj: any) => {
61-
onAddRow(obj.value);
103+
const actionId = uuid4();
104+
onAddRow(actionId, obj.value);
105+
actionHandlerMapRef.current.set(actionId, obj.value);
62106
}}
63107
placeholder={placeholder}
64108
value={null}
@@ -70,7 +114,9 @@ export default function ActionNodeList({
70114
function Node() {
71115
const {action} = useActionNodeContext();
72116
const node = actionNodesMap.get(action.type);
73-
return node?.action;
117+
118+
const component = node?.action;
119+
return component ? component : node?.label;
74120
}
75121

76122
const StyledSelectControl = styled(Select)`

static/app/views/automations/components/actionNodes.tsx

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {createContext, useContext} from 'react';
22

33
import {t} from 'sentry/locale';
4-
import type {Action, Integration} from 'sentry/types/workflowEngine/actions';
4+
import type {Action, ActionHandler} from 'sentry/types/workflowEngine/actions';
55
import {ActionType} from 'sentry/types/workflowEngine/actions';
66
import {AzureDevOpsNode} from 'sentry/views/automations/components/actions/azureDevOps';
77
import {DiscordNode} from 'sentry/views/automations/components/actions/discord';
@@ -13,13 +13,15 @@ import {JiraServerNode} from 'sentry/views/automations/components/actions/jiraSe
1313
import {MSTeamsNode} from 'sentry/views/automations/components/actions/msTeams';
1414
import {OpsgenieNode} from 'sentry/views/automations/components/actions/opsgenie';
1515
import {PagerdutyNode} from 'sentry/views/automations/components/actions/pagerduty';
16+
import {SentryAppNode} from 'sentry/views/automations/components/actions/sentryApp';
1617
import {SlackNode} from 'sentry/views/automations/components/actions/slack';
18+
import {WebhookNode} from 'sentry/views/automations/components/actions/webhook';
1719

1820
interface ActionNodeProps {
1921
action: Action;
2022
actionId: string;
23+
handler: ActionHandler;
2124
onUpdate: (condition: Record<string, any>) => void;
22-
integrations?: Integration[];
2325
}
2426

2527
export const ActionNodeContext = createContext<ActionNodeProps | null>(null);
@@ -33,13 +35,13 @@ export function useActionNodeContext(): ActionNodeProps {
3335
}
3436

3537
type ActionNode = {
36-
action: React.ReactNode;
37-
label: string;
38+
action?: React.ReactNode;
39+
label?: string;
3840
};
3941

4042
export const actionNodesMap = new Map<ActionType, ActionNode>([
4143
[ActionType.AZURE_DEVOPS, {label: t('Azure DevOps'), action: <AzureDevOpsNode />}],
42-
[ActionType.EMAIL, {label: t('Email'), action: <EmailNode />}],
44+
[ActionType.EMAIL, {label: t('Notify on preferred channel'), action: <EmailNode />}],
4345
[
4446
ActionType.DISCORD,
4547
{
@@ -69,11 +71,28 @@ export const actionNodesMap = new Map<ActionType, ActionNode>([
6971
action: <PagerdutyNode />,
7072
},
7173
],
74+
[
75+
ActionType.PLUGIN,
76+
{
77+
label: t('Legacy integrations'),
78+
action: t('Send a notification (for all legacy integrations)'),
79+
},
80+
],
81+
[
82+
ActionType.SENTRY_APP,
83+
{
84+
action: <SentryAppNode />,
85+
},
86+
],
7287
[
7388
ActionType.SLACK,
7489
{
7590
label: t('Slack'),
7691
action: <SlackNode />,
7792
},
7893
],
94+
[
95+
ActionType.WEBHOOK,
96+
{label: t('Send a notification via an integration'), action: <WebhookNode />},
97+
],
7998
]);

static/app/views/automations/components/actions/integrationField.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import AutomationBuilderSelectField from 'sentry/components/workflowEngine/form/
22
import {useActionNodeContext} from 'sentry/views/automations/components/actionNodes';
33

44
export function IntegrationField() {
5-
const {action, actionId, onUpdate, integrations} = useActionNodeContext();
5+
const {action, actionId, onUpdate, handler} = useActionNodeContext();
6+
const integrations = handler?.integrations;
7+
68
return (
79
<AutomationBuilderSelectField
810
name={`${actionId}.integrationId`}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {tct} from 'sentry/locale';
2+
import {useActionNodeContext} from 'sentry/views/automations/components/actionNodes';
3+
import {SentryAppActionSettingsButton} from 'sentry/views/automations/components/actions/sentryAppSettingsButton';
4+
5+
export function SentryAppNode() {
6+
const {handler} = useActionNodeContext();
7+
const name = handler?.sentryApp?.name;
8+
const title = handler?.sentryApp?.title;
9+
return tct('[label] with these [settings]', {
10+
label: title || name,
11+
settings: <SentryAppActionSettingsButton />,
12+
});
13+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {Button} from 'sentry/components/core/button';
2+
import {IconSettings} from 'sentry/icons';
3+
import {t} from 'sentry/locale';
4+
5+
// TODO(miahsu): Implement the action settings button/modal
6+
export function SentryAppActionSettingsButton() {
7+
return (
8+
<Button size="sm" icon={<IconSettings />}>
9+
{t('Action Settings')}
10+
</Button>
11+
);
12+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import AutomationBuilderSelectField from 'sentry/components/workflowEngine/form/automationBuilderSelectField';
2+
import {tct} from 'sentry/locale';
3+
import {useActionNodeContext} from 'sentry/views/automations/components/actionNodes';
4+
5+
export function WebhookNode() {
6+
return tct('Send a notification via [services]', {
7+
services: <ServicesField />,
8+
});
9+
}
10+
11+
export function ServicesField() {
12+
const {action, actionId, onUpdate, handler} = useActionNodeContext();
13+
const services = handler?.services;
14+
15+
return (
16+
<AutomationBuilderSelectField
17+
name={`${actionId}.data.targetIdentifier`}
18+
value={action.data.targetIdentifier}
19+
options={services?.map(service => ({
20+
label: service.name,
21+
value: service.slug,
22+
}))}
23+
onChange={(value: string) => {
24+
onUpdate({
25+
targetIdentifier: value,
26+
});
27+
}}
28+
/>
29+
);
30+
}

static/app/views/automations/components/automationBuilder.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,10 @@ function ActionFilterBlock({actionFilter}: ActionFilterBlockProps) {
178178
</StepLead>
179179
{/* TODO: add actions dropdown here */}
180180
<ActionNodeList
181-
// TODO: replace constant availableActions with API response
182-
availableActions={[]}
183181
placeholder={t('Select an action')}
184182
group={`actionFilters.${actionFilter.id}`}
185183
actions={actionFilter?.actions || []}
186-
onAddRow={type => actions.addIfAction(actionFilter.id, type)}
184+
onAddRow={(id, type) => actions.addIfAction(actionFilter.id, id, type)}
187185
onDeleteRow={id => actions.removeIfAction(actionFilter.id, id)}
188186
updateAction={(id, data) => actions.updateIfAction(actionFilter.id, id, data)}
189187
/>

static/app/views/automations/components/automationBuilderContext.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {createContext, type Reducer, useCallback, useContext, useReducer} from 'react';
22
import {uuid4} from '@sentry/core';
33

4-
import type {ActionType} from 'sentry/types/workflowEngine/actions';
4+
import type {ActionHandler} from 'sentry/types/workflowEngine/actions';
55
import {
66
type DataConditionGroup,
77
DataConditionGroupLogicType,
@@ -100,8 +100,8 @@ export function useAutomationBuilderReducer() {
100100
[dispatch]
101101
),
102102
addIfAction: useCallback(
103-
(groupId: string, actionType: ActionType) =>
104-
dispatch({type: 'ADD_IF_ACTION', groupId, actionType}),
103+
(groupId: string, actionId: string, actionHandler: ActionHandler) =>
104+
dispatch({type: 'ADD_IF_ACTION', groupId, actionId, actionHandler}),
105105
[dispatch]
106106
),
107107
removeIfAction: useCallback(
@@ -135,7 +135,7 @@ interface AutomationBuilderState {
135135
// 2. The AutomationActions interface
136136
interface AutomationActions {
137137
addIf: () => void;
138-
addIfAction: (groupId: string, actionType: ActionType) => void;
138+
addIfAction: (groupId: string, actionId: string, actionHandler: ActionHandler) => void;
139139
addIfCondition: (groupId: string, conditionType: DataConditionType) => void;
140140
addWhenCondition: (conditionType: DataConditionType) => void;
141141
removeIf: (groupId: string) => void;
@@ -245,7 +245,8 @@ type UpdateIfConditionAction = {
245245
};
246246

247247
type AddIfActionAction = {
248-
actionType: ActionType;
248+
actionHandler: ActionHandler;
249+
actionId: string;
249250
groupId: string;
250251
type: 'ADD_IF_ACTION';
251252
};
@@ -468,7 +469,7 @@ function addIfAction(
468469
state: AutomationBuilderState,
469470
action: AddIfActionAction
470471
): AutomationBuilderState {
471-
const {groupId, actionType} = action;
472+
const {groupId, actionId, actionHandler} = action;
472473
return {
473474
...state,
474475
actionFilters: state.actionFilters.map(group => {
@@ -480,9 +481,13 @@ function addIfAction(
480481
actions: [
481482
...(group.actions ?? []),
482483
{
483-
id: uuid4(),
484-
type: actionType,
485-
data: {},
484+
id: actionId,
485+
type: actionHandler.type,
486+
data: {
487+
...(actionHandler.sentryApp
488+
? {targetIdentifier: actionHandler.sentryApp.id}
489+
: {}),
490+
},
486491
},
487492
],
488493
};

0 commit comments

Comments
 (0)