diff --git a/static/app/types/workflowEngine/dataConditions.tsx b/static/app/types/workflowEngine/dataConditions.tsx index 6bfa2cf9422cb5..6f0fb1b4930b03 100644 --- a/static/app/types/workflowEngine/dataConditions.tsx +++ b/static/app/types/workflowEngine/dataConditions.tsx @@ -24,6 +24,7 @@ export enum DataConditionType { EXISTING_HIGH_PRIORITY_ISSUE = 'existing_high_priority_issue', FIRST_SEEN_EVENT = 'first_seen_event', ISSUE_CATEGORY = 'issue_category', + ISSUE_TYPE = 'issue_type', ISSUE_OCCURRENCES = 'issue_occurrences', LATEST_ADOPTED_RELEASE = 'latest_adopted_release', LATEST_RELEASE = 'latest_release', diff --git a/static/app/views/automations/components/actionFilters/issueType.tsx b/static/app/views/automations/components/actionFilters/issueType.tsx new file mode 100644 index 00000000000000..f85fa0fbb41452 --- /dev/null +++ b/static/app/views/automations/components/actionFilters/issueType.tsx @@ -0,0 +1,88 @@ +import {AutomationBuilderSelect} from 'sentry/components/workflowEngine/form/automationBuilderSelect'; +import {t, tct} from 'sentry/locale'; +import type {SelectValue} from 'sentry/types/core'; +import {getIssueTitleFromType, VISIBLE_ISSUE_TYPES} from 'sentry/types/group'; +import type {DataCondition} from 'sentry/types/workflowEngine/dataConditions'; +import {useAutomationBuilderErrorContext} from 'sentry/views/automations/components/automationBuilderErrorContext'; +import type {ValidateDataConditionProps} from 'sentry/views/automations/components/automationFormData'; +import {useDataConditionNodeContext} from 'sentry/views/automations/components/dataConditionNodes'; + +const INCLUDE_CHOICES = [ + {value: true, label: t('equal to')}, + {value: false, label: t('not equal to')}, +]; + +type IssueTypeChoice = { + label: string; + value: string; +}; + +const ISSUE_TYPE_CHOICES: IssueTypeChoice[] = VISIBLE_ISSUE_TYPES.map(value => ({ + value, + label: getIssueTitleFromType(value) ?? value, +})); + +export function IssueTypeDetails({condition}: {condition: DataCondition}) { + const include = condition.comparison.include ?? true; + const includeLabel = + INCLUDE_CHOICES.find(choice => choice.value === include)?.label ?? ''; + + return tct('Issue type is [include] [type]', { + include: includeLabel, + type: + ISSUE_TYPE_CHOICES.find(choice => choice.value === condition.comparison.value) + ?.label || condition.comparison.value, + }); +} + +export function IssueTypeNode() { + return tct('Issue type is [include] [type]', { + include: , + type: , + }); +} + +function IncludeField() { + const {condition, condition_id, onUpdate} = useDataConditionNodeContext(); + const {removeError} = useAutomationBuilderErrorContext(); + + return ( + ) => { + onUpdate({comparison: {...condition.comparison, include: option.value}}); + removeError(condition.id); + }} + /> + ); +} + +function TypeField() { + const {condition, condition_id, onUpdate} = useDataConditionNodeContext(); + const {removeError} = useAutomationBuilderErrorContext(); + + return ( + ) => { + onUpdate({comparison: {...condition.comparison, value: option.value}}); + removeError(condition.id); + }} + /> + ); +} + +export function validateIssueTypeCondition({ + condition, +}: ValidateDataConditionProps): string | undefined { + if (condition.comparison.value === undefined || condition.comparison.value === null) { + return t('You must select an issue type.'); + } + return undefined; +} diff --git a/static/app/views/automations/components/dataConditionNodeList.spec.tsx b/static/app/views/automations/components/dataConditionNodeList.spec.tsx index e4e43a6998b4ab..ff690bacdf1585 100644 --- a/static/app/views/automations/components/dataConditionNodeList.spec.tsx +++ b/static/app/views/automations/components/dataConditionNodeList.spec.tsx @@ -4,6 +4,7 @@ import {DataConditionHandlerFixture} from 'sentry-fixture/workflowEngine'; import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary'; +import {IssueType} from 'sentry/types/group'; import type {DataConditionHandler} from 'sentry/types/workflowEngine/dataConditions'; import { DataConditionGroupLogicType, @@ -25,6 +26,9 @@ const dataConditionHandlers: DataConditionHandler[] = [ DataConditionHandlerFixture({ type: DataConditionType.ISSUE_PRIORITY_DEESCALATING, }), + DataConditionHandlerFixture({ + type: DataConditionType.ISSUE_TYPE, + }), DataConditionHandlerFixture({ type: DataConditionType.EVENT_FREQUENCY, handlerSubgroup: DataConditionHandlerSubgroupType.EVENT_ATTRIBUTES, @@ -115,11 +119,12 @@ describe('DataConditionNodeList', () => { ); await userEvent.click(screen.getByRole('textbox', {name: 'Add condition'})); - expect(screen.getAllByRole('menuitemradio')).toHaveLength(4); + expect(screen.getAllByRole('menuitemradio')).toHaveLength(5); expect(screen.getByRole('menuitemradio', {name: 'Issue age'})).toBeInTheDocument(); expect( screen.getByRole('menuitemradio', {name: 'Issue priority'}) ).toBeInTheDocument(); + expect(screen.getByRole('menuitemradio', {name: 'Issue type'})).toBeInTheDocument(); expect( screen.getByRole('menuitemradio', {name: 'Number of events'}) ).toBeInTheDocument(); @@ -349,5 +354,38 @@ describe('DataConditionNodeList', () => { comparison: {value: 11, include: false}, }); }); + + it('issue type node', async () => { + const condition = DataConditionFixture({ + id: 'issue-type', + type: DataConditionType.ISSUE_TYPE, + comparison: { + value: IssueType.ERROR, + include: true, + }, + }); + + render( + + + + + + + , + {organization} + ); + + await userEvent.click(await screen.findByLabelText('Issue type')); + await userEvent.click( + screen.getByRole('menuitemradio', {name: 'Issue Detected by Metric Monitor'}) + ); + + expect(mockUpdateCondition).toHaveBeenLastCalledWith('issue-type', { + comparison: {value: IssueType.METRIC_ISSUE, include: true}, + }); + }); }); }); diff --git a/static/app/views/automations/components/dataConditionNodes.tsx b/static/app/views/automations/components/dataConditionNodes.tsx index d59278a5467e4f..2d31cbeb31a317 100644 --- a/static/app/views/automations/components/dataConditionNodes.tsx +++ b/static/app/views/automations/components/dataConditionNodes.tsx @@ -63,6 +63,11 @@ import { validateIssuePriorityCondition, } from 'sentry/views/automations/components/actionFilters/issuePriority'; import {IssuePriorityDeescalating} from 'sentry/views/automations/components/actionFilters/issuePriorityDeescalating'; +import { + IssueTypeDetails, + IssueTypeNode, + validateIssueTypeCondition, +} from 'sentry/views/automations/components/actionFilters/issueType'; import { LatestAdoptedReleaseDetails, LatestAdoptedReleaseNode, @@ -202,6 +207,15 @@ export const dataConditionNodesMap = new Map