Skip to content

Commit ed00071

Browse files
authored
feat(aci): add data condition validation to automation builder (#95149)
follow up to #95008 (action validation) https://github.com/user-attachments/assets/74fed697-37fe-462c-b255-86bd453b6848
1 parent 9305b92 commit ed00071

18 files changed

+592
-134
lines changed

static/app/views/automations/components/actionFilters/ageComparison.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
AGE_COMPARISON_CHOICES,
99
TimeUnit,
1010
} from 'sentry/views/automations/components/actionFilters/constants';
11+
import {useAutomationBuilderErrorContext} from 'sentry/views/automations/components/automationBuilderErrorContext';
12+
import type {ValidateDataConditionProps} from 'sentry/views/automations/components/automationFormData';
1113
import {useDataConditionNodeContext} from 'sentry/views/automations/components/dataConditionNodes';
1214

1315
const TIME_CHOICES = [
@@ -44,6 +46,8 @@ export function AgeComparisonNode() {
4446

4547
function ComparisonField() {
4648
const {condition, condition_id, onUpdate} = useDataConditionNodeContext();
49+
const {removeError} = useAutomationBuilderErrorContext();
50+
4751
return (
4852
<AutomationBuilderSelect
4953
name={`${condition_id}.comparison.comparison_type`}
@@ -52,13 +56,16 @@ function ComparisonField() {
5256
options={AGE_COMPARISON_CHOICES}
5357
onChange={(option: SelectValue<AgeComparison>) => {
5458
onUpdate({comparison: {...condition.comparison, comparison_type: option.value}});
59+
removeError(condition.id);
5560
}}
5661
/>
5762
);
5863
}
5964

6065
function ValueField() {
6166
const {condition, condition_id, onUpdate} = useDataConditionNodeContext();
67+
const {removeError} = useAutomationBuilderErrorContext();
68+
6269
return (
6370
<AutomationBuilderNumberInput
6471
name={`${condition_id}.comparison.value`}
@@ -68,14 +75,16 @@ function ValueField() {
6875
step={1}
6976
onChange={(value: number) => {
7077
onUpdate({comparison: {...condition.comparison, value}});
78+
removeError(condition.id);
7179
}}
72-
placeholder={'10'}
7380
/>
7481
);
7582
}
7683

7784
function TimeField() {
7885
const {condition, condition_id, onUpdate} = useDataConditionNodeContext();
86+
const {removeError} = useAutomationBuilderErrorContext();
87+
7988
return (
8089
<AutomationBuilderSelect
8190
name={`${condition_id}.comparison.time`}
@@ -84,7 +93,21 @@ function TimeField() {
8493
options={TIME_CHOICES}
8594
onChange={(option: SelectValue<TimeUnit>) => {
8695
onUpdate({comparison: {...condition.comparison, time: option.value}});
96+
removeError(condition.id);
8797
}}
8898
/>
8999
);
90100
}
101+
102+
export function validateAgeComparisonCondition({
103+
condition,
104+
}: ValidateDataConditionProps): string | undefined {
105+
if (
106+
!condition.comparison.comparison_type ||
107+
condition.comparison.value === undefined ||
108+
!condition.comparison.time
109+
) {
110+
return t('Ensure all fields are filled in.');
111+
}
112+
return undefined;
113+
}

static/app/views/automations/components/actionFilters/assignedTo.tsx

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import useOrganization from 'sentry/utils/useOrganization';
1313
import {useTeamsById} from 'sentry/utils/useTeamsById';
1414
import useUserFromId from 'sentry/utils/useUserFromId';
1515
import {TargetType} from 'sentry/views/automations/components/actionFilters/constants';
16+
import {useAutomationBuilderErrorContext} from 'sentry/views/automations/components/automationBuilderErrorContext';
17+
import type {ValidateDataConditionProps} from 'sentry/views/automations/components/automationFormData';
1618
import {useDataConditionNodeContext} from 'sentry/views/automations/components/dataConditionNodes';
1719

1820
const TARGET_TYPE_CHOICES = [
@@ -74,6 +76,7 @@ function TargetTypeField() {
7476

7577
function IdentifierField() {
7678
const {condition, condition_id, onUpdate} = useDataConditionNodeContext();
79+
const {removeError} = useAutomationBuilderErrorContext();
7780
const organization = useOrganization();
7881

7982
if (condition.comparison.targetType === TargetType.TEAM) {
@@ -83,11 +86,12 @@ function IdentifierField() {
8386
name={`${condition_id}.data.targetIdentifier`}
8487
aria-label={t('Team')}
8588
value={condition.comparison.targetIdentifier}
86-
onChange={(option: SelectValue<string>) =>
89+
onChange={(option: SelectValue<string>) => {
8790
onUpdate({
8891
comparison: {...condition.comparison, targetIdentifier: option.value},
89-
})
90-
}
92+
});
93+
removeError(condition.id);
94+
}}
9195
useId
9296
styles={selectControlStyles}
9397
/>
@@ -103,11 +107,12 @@ function IdentifierField() {
103107
key={`${condition_id}.data.targetIdentifier`}
104108
aria-label={t('Member')}
105109
value={condition.comparison.targetIdentifier}
106-
onChange={(value: any) =>
110+
onChange={(value: any) => {
107111
onUpdate({
108112
comparison: {...condition.comparison, targetIdentifier: value.actor.id},
109-
})
110-
}
113+
});
114+
removeError(condition.id);
115+
}}
111116
styles={selectControlStyles}
112117
/>
113118
</SelectWrapper>
@@ -117,6 +122,27 @@ function IdentifierField() {
117122
return null;
118123
}
119124

125+
export function validateAssignedToCondition({
126+
condition,
127+
}: ValidateDataConditionProps): string | undefined {
128+
if (!condition.comparison.targetType) {
129+
return t('You must specify an assignee type.');
130+
}
131+
if (
132+
condition.comparison.targetType === TargetType.TEAM &&
133+
!condition.comparison.targetIdentifier
134+
) {
135+
return t('You must specify a team.');
136+
}
137+
if (
138+
condition.comparison.targetType === TargetType.MEMBER &&
139+
!condition.comparison.targetIdentifier
140+
) {
141+
return t('You must specify a member.');
142+
}
143+
return undefined;
144+
}
145+
120146
const SelectWrapper = styled('div')`
121147
width: 200px;
122148
`;

static/app/views/automations/components/actionFilters/comparisonBranches.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
COMPARISON_INTERVAL_CHOICES,
77
INTERVAL_CHOICES,
88
} from 'sentry/views/automations/components/actionFilters/constants';
9+
import {useAutomationBuilderErrorContext} from 'sentry/views/automations/components/automationBuilderErrorContext';
910
import {useDataConditionNodeContext} from 'sentry/views/automations/components/dataConditionNodes';
1011

1112
export function CountBranch() {
@@ -25,6 +26,8 @@ export function PercentBranch() {
2526

2627
function ValueField() {
2728
const {condition, condition_id, onUpdate} = useDataConditionNodeContext();
29+
const {removeError} = useAutomationBuilderErrorContext();
30+
2831
return (
2932
<AutomationBuilderNumberInput
3033
name={`${condition_id}.comparison.value`}
@@ -34,14 +37,16 @@ function ValueField() {
3437
step={1}
3538
onChange={(value: number) => {
3639
onUpdate({comparison: {...condition.comparison, value}});
40+
removeError(condition.id);
3741
}}
38-
placeholder="100"
3942
/>
4043
);
4144
}
4245

4346
function IntervalField() {
4447
const {condition, condition_id, onUpdate} = useDataConditionNodeContext();
48+
const {removeError} = useAutomationBuilderErrorContext();
49+
4550
return (
4651
<AutomationBuilderSelect
4752
name={`${condition_id}.comparison.interval`}
@@ -50,13 +55,16 @@ function IntervalField() {
5055
options={INTERVAL_CHOICES}
5156
onChange={(option: SelectValue<string>) => {
5257
onUpdate({comparison: {...condition.comparison, interval: option.value}});
58+
removeError(condition.id);
5359
}}
5460
/>
5561
);
5662
}
5763

5864
function ComparisonIntervalField() {
5965
const {condition, condition_id, onUpdate} = useDataConditionNodeContext();
66+
const {removeError} = useAutomationBuilderErrorContext();
67+
6068
return (
6169
<AutomationBuilderSelect
6270
name={`${condition_id}.comparison.comparison_interval`}
@@ -67,6 +75,7 @@ function ComparisonIntervalField() {
6775
onUpdate({
6876
comparison: {...condition.comparison, comparison_interval: option.value},
6977
});
78+
removeError(condition.id);
7079
}}
7180
/>
7281
);

static/app/views/automations/components/actionFilters/eventAttribute.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
MATCH_CHOICES,
99
type MatchType,
1010
} from 'sentry/views/automations/components/actionFilters/constants';
11+
import {useAutomationBuilderErrorContext} from 'sentry/views/automations/components/automationBuilderErrorContext';
12+
import type {ValidateDataConditionProps} from 'sentry/views/automations/components/automationFormData';
1113
import {useDataConditionNodeContext} from 'sentry/views/automations/components/dataConditionNodes';
1214

1315
export function EventAttributeDetails({condition}: {condition: DataCondition}) {
@@ -64,6 +66,8 @@ function MatchField() {
6466

6567
function ValueField() {
6668
const {condition, condition_id, onUpdate} = useDataConditionNodeContext();
69+
const {removeError} = useAutomationBuilderErrorContext();
70+
6771
return (
6872
<AutomationBuilderInput
6973
name={`${condition_id}.comparison.value`}
@@ -72,7 +76,21 @@ function ValueField() {
7276
value={condition.comparison.value}
7377
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
7478
onUpdate({comparison: {...condition.comparison, value: e.target.value}});
79+
removeError(condition.id);
7580
}}
7681
/>
7782
);
7883
}
84+
85+
export function validateEventAttributeCondition({
86+
condition,
87+
}: ValidateDataConditionProps): string | undefined {
88+
if (
89+
!condition.comparison.attribute ||
90+
!condition.comparison.match ||
91+
!condition.comparison.value
92+
) {
93+
return t('Ensure all fields are filled in.');
94+
}
95+
return undefined;
96+
}

static/app/views/automations/components/actionFilters/eventFrequency.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,14 @@ import {
1616
import {
1717
SubfilterDetailsList,
1818
SubfiltersList,
19+
validateSubfilters,
1920
} from 'sentry/views/automations/components/actionFilters/subfiltersList';
20-
import {useDataConditionNodeContext} from 'sentry/views/automations/components/dataConditionNodes';
21+
import {useAutomationBuilderErrorContext} from 'sentry/views/automations/components/automationBuilderErrorContext';
22+
import type {ValidateDataConditionProps} from 'sentry/views/automations/components/automationFormData';
23+
import {
24+
dataConditionNodesMap,
25+
useDataConditionNodeContext,
26+
} from 'sentry/views/automations/components/dataConditionNodes';
2127

2228
export function EventFrequencyCountDetails({condition}: {condition: DataCondition}) {
2329
const hasSubfilters = condition.comparison.filters?.length > 0;
@@ -82,6 +88,7 @@ export function EventFrequencyNode() {
8288

8389
function ComparisonTypeField() {
8490
const {condition, condition_id, onUpdate} = useDataConditionNodeContext();
91+
const {removeError} = useAutomationBuilderErrorContext();
8592

8693
if (condition.type === DataConditionType.EVENT_FREQUENCY_COUNT) {
8794
return <CountBranch />;
@@ -106,8 +113,35 @@ function ComparisonTypeField() {
106113
},
107114
]}
108115
onChange={(option: SelectValue<DataConditionType>) => {
109-
onUpdate({type: option.value});
116+
onUpdate({
117+
type: option.value,
118+
comparison: {
119+
...condition.comparison,
120+
...dataConditionNodesMap.get(option.value)?.defaultComparison,
121+
},
122+
});
123+
removeError(condition.id);
110124
}}
111125
/>
112126
);
113127
}
128+
129+
export function validateEventFrequencyCondition({
130+
condition,
131+
}: ValidateDataConditionProps): string | undefined {
132+
if (condition.type === DataConditionType.EVENT_FREQUENCY) {
133+
return t('You must select a comparison type.');
134+
}
135+
if (
136+
!condition.comparison.value ||
137+
!condition.comparison.interval ||
138+
(condition.type === DataConditionType.EVENT_FREQUENCY_PERCENT &&
139+
!condition.comparison.comparison_interval)
140+
) {
141+
return t('Ensure all fields are filled in.');
142+
}
143+
if (condition.comparison.filters) {
144+
return validateSubfilters(condition.comparison.filters);
145+
}
146+
return undefined;
147+
}

static/app/views/automations/components/actionFilters/eventUniqueUserFrequency.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,14 @@ import {
1616
import {
1717
SubfilterDetailsList,
1818
SubfiltersList,
19+
validateSubfilters,
1920
} from 'sentry/views/automations/components/actionFilters/subfiltersList';
20-
import {useDataConditionNodeContext} from 'sentry/views/automations/components/dataConditionNodes';
21+
import {useAutomationBuilderErrorContext} from 'sentry/views/automations/components/automationBuilderErrorContext';
22+
import type {ValidateDataConditionProps} from 'sentry/views/automations/components/automationFormData';
23+
import {
24+
dataConditionNodesMap,
25+
useDataConditionNodeContext,
26+
} from 'sentry/views/automations/components/dataConditionNodes';
2127

2228
export function EventUniqueUserFrequencyCountDetails({
2329
condition,
@@ -94,6 +100,7 @@ export function EventUniqueUserFrequencyNode() {
94100

95101
function ComparisonTypeField() {
96102
const {condition, condition_id, onUpdate} = useDataConditionNodeContext();
103+
const {removeError} = useAutomationBuilderErrorContext();
97104

98105
if (condition.type === DataConditionType.EVENT_UNIQUE_USER_FREQUENCY_COUNT) {
99106
return <CountBranch />;
@@ -118,8 +125,35 @@ function ComparisonTypeField() {
118125
},
119126
]}
120127
onChange={(option: SelectValue<DataConditionType>) => {
121-
onUpdate({type: option.value});
128+
onUpdate({
129+
type: option.value,
130+
comparison: {
131+
...condition.comparison,
132+
...dataConditionNodesMap.get(option.value)?.defaultComparison,
133+
},
134+
});
135+
removeError(condition.id);
122136
}}
123137
/>
124138
);
125139
}
140+
141+
export function validateEventUniqueUserFrequencyCondition({
142+
condition,
143+
}: ValidateDataConditionProps): string | undefined {
144+
if (condition.type === DataConditionType.EVENT_UNIQUE_USER_FREQUENCY) {
145+
return t('You must select a comparison type.');
146+
}
147+
if (
148+
!condition.comparison.value ||
149+
!condition.comparison.interval ||
150+
(condition.type === DataConditionType.EVENT_UNIQUE_USER_FREQUENCY_PERCENT &&
151+
!condition.comparison.comparison_interval)
152+
) {
153+
return t('Ensure all fields are filled in.');
154+
}
155+
if (condition.comparison.filters) {
156+
return validateSubfilters(condition.comparison.filters);
157+
}
158+
return undefined;
159+
}

0 commit comments

Comments
 (0)