Skip to content

Commit 720bb11

Browse files
authored
feat(aci): Implement created_by search filter in Alerts (#112950)
A previous PR added support for this in the endpoint, this adds the frontend logic to display the list of users.
1 parent d0caf92 commit 720bb11

File tree

5 files changed

+147
-67
lines changed

5 files changed

+147
-67
lines changed

static/app/views/automations/components/automationListTable/search.tsx

Lines changed: 5 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,24 @@
11
import {SearchQueryBuilder} from 'sentry/components/searchQueryBuilder';
22
import {t} from 'sentry/locale';
3-
import type {TagCollection} from 'sentry/types/group';
4-
import type {FieldDefinition} from 'sentry/utils/fields';
5-
import {FieldKind} from 'sentry/utils/fields';
6-
import {AUTOMATION_FILTER_KEYS} from 'sentry/views/automations/constants';
3+
import {useAutomationFilterKeys} from 'sentry/views/automations/utils/useAutomationFilterKeys';
74

85
type AutomationSearchProps = {
96
initialQuery: string;
107
onSearch: (query: string) => void;
118
};
129

13-
function getAutomationFilterKeyDefinition(filterKey: string): FieldDefinition | null {
14-
if (
15-
AUTOMATION_FILTER_KEYS.hasOwnProperty(filterKey) &&
16-
AUTOMATION_FILTER_KEYS[filterKey]
17-
) {
18-
const {description, valueType, keywords, values} = AUTOMATION_FILTER_KEYS[filterKey];
19-
20-
return {
21-
kind: FieldKind.FIELD,
22-
desc: description,
23-
valueType,
24-
keywords,
25-
values,
26-
};
27-
}
28-
29-
return null;
30-
}
31-
32-
const FILTER_KEYS: TagCollection = Object.fromEntries(
33-
Object.keys(AUTOMATION_FILTER_KEYS).map(key => {
34-
const {values} = AUTOMATION_FILTER_KEYS[key] ?? {};
35-
36-
return [
37-
key,
38-
{
39-
key,
40-
name: key,
41-
predefined: values !== undefined,
42-
values,
43-
},
44-
];
45-
})
46-
);
47-
4810
export function AutomationSearch({initialQuery, onSearch}: AutomationSearchProps) {
11+
const {filterKeys, getFieldDefinition} = useAutomationFilterKeys();
12+
4913
return (
5014
<SearchQueryBuilder
5115
initialQuery={initialQuery}
5216
placeholder={t('Search for alerts')}
5317
onSearch={onSearch}
54-
filterKeys={FILTER_KEYS}
18+
filterKeys={filterKeys}
5519
getTagValues={() => Promise.resolve([])}
5620
searchSource="automations-list"
57-
fieldDefinitionGetter={getAutomationFilterKeyDefinition}
21+
fieldDefinitionGetter={getFieldDefinition}
5822
disallowUnsupportedFilters
5923
disallowWildcard
6024
disallowLogicalOperators
Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1 @@
1-
import {FieldValueType} from 'sentry/utils/fields';
2-
import {ActionType} from 'sentry/views/alerts/rules/metric/types';
3-
41
export const AUTOMATION_LIST_PAGE_LIMIT = 20;
5-
6-
const ACTION_TYPE_VALUES = Object.values(ActionType).sort();
7-
8-
export const AUTOMATION_FILTER_KEYS: Record<
9-
string,
10-
{
11-
description: string;
12-
valueType: FieldValueType;
13-
keywords?: string[];
14-
values?: string[];
15-
}
16-
> = {
17-
name: {
18-
description: 'Name of the automation (exact match).',
19-
valueType: FieldValueType.STRING,
20-
keywords: ['name'],
21-
},
22-
action: {
23-
description: 'Action triggered by the automation.',
24-
valueType: FieldValueType.STRING,
25-
values: ACTION_TYPE_VALUES,
26-
},
27-
};

static/app/views/automations/list.spec.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ describe('AutomationsList', () => {
4444
url: '/organizations/org-slug/prompts-activity/',
4545
body: {},
4646
});
47+
MockApiClient.addMockResponse({
48+
url: '/organizations/org-slug/members/',
49+
body: [],
50+
});
4751
MockApiClient.addMockResponse({
4852
url: '/organizations/org-slug/workflows/',
4953
body: [AutomationFixture({name: 'Automation 1'})],
@@ -189,6 +193,25 @@ describe('AutomationsList', () => {
189193
await screen.findByText('Slack Automation');
190194
expect(mockAutomationActionSlack).toHaveBeenCalled();
191195
});
196+
197+
it('can filter by created_by', async () => {
198+
const mockAutomationCreatedBy = MockApiClient.addMockResponse({
199+
url: '/organizations/org-slug/workflows/',
200+
body: [AutomationFixture({name: 'My Automation'})],
201+
match: [MockApiClient.matchQuery({query: 'created_by:me'})],
202+
});
203+
204+
render(<AutomationsList />, {organization});
205+
await screen.findByText('Automation 1');
206+
207+
// Click through menus to select created_by:me
208+
await userEvent.click(screen.getByRole('combobox', {name: 'Add a search term'}));
209+
await userEvent.click(await screen.findByRole('option', {name: 'created_by'}));
210+
await userEvent.click(await screen.findByRole('option', {name: 'me'}));
211+
212+
await screen.findByText('My Automation');
213+
expect(mockAutomationCreatedBy).toHaveBeenCalled();
214+
});
192215
});
193216

194217
describe('bulk actions', () => {
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import {useCallback, useMemo} from 'react';
2+
3+
import {ItemType, type SearchGroup} from 'sentry/components/searchBar/types';
4+
import {escapeTagValue} from 'sentry/components/searchBar/utils';
5+
import type {FieldDefinitionGetter} from 'sentry/components/searchQueryBuilder/types';
6+
import {IconStar, IconUser} from 'sentry/icons';
7+
import {t} from 'sentry/locale';
8+
import type {TagCollection} from 'sentry/types/group';
9+
import {FieldKind, FieldValueType, type FieldDefinition} from 'sentry/utils/fields';
10+
import {getUsername} from 'sentry/utils/membersAndTeams/userUtils';
11+
import {useMembers} from 'sentry/utils/useMembers';
12+
import {ActionType} from 'sentry/views/alerts/rules/metric/types';
13+
14+
const ACTION_TYPE_VALUES = Object.values(ActionType).sort();
15+
16+
const AUTOMATION_FILTER_KEYS: Record<
17+
string,
18+
{
19+
fieldDefinition: FieldDefinition;
20+
predefined?: boolean;
21+
}
22+
> = {
23+
name: {
24+
fieldDefinition: {
25+
desc: 'Name of the Alert.',
26+
kind: FieldKind.FIELD,
27+
valueType: FieldValueType.STRING,
28+
keywords: ['name'],
29+
},
30+
},
31+
action: {
32+
predefined: true,
33+
fieldDefinition: {
34+
desc: 'Action triggered by the Alert.',
35+
kind: FieldKind.FIELD,
36+
valueType: FieldValueType.STRING,
37+
values: ACTION_TYPE_VALUES,
38+
},
39+
},
40+
created_by: {
41+
predefined: true,
42+
fieldDefinition: {
43+
desc: 'User who created the Alert.',
44+
kind: FieldKind.FIELD,
45+
valueType: FieldValueType.STRING,
46+
allowWildcard: false,
47+
keywords: ['creator', 'author'],
48+
},
49+
},
50+
};
51+
52+
const convertToSearchItem = (value: string) => {
53+
const escapedValue = escapeTagValue(value);
54+
return {
55+
value: escapedValue,
56+
desc: value,
57+
type: ItemType.TAG_VALUE,
58+
};
59+
};
60+
61+
export function useAutomationFilterKeys(): {
62+
filterKeys: TagCollection;
63+
getFieldDefinition: FieldDefinitionGetter;
64+
} {
65+
const {members} = useMembers();
66+
67+
const createdByValues: SearchGroup[] = useMemo(() => {
68+
const usernames = members.map(getUsername);
69+
70+
return [
71+
{
72+
title: t('Suggested Values'),
73+
type: 'header',
74+
icon: <IconStar size="xs" />,
75+
children: [convertToSearchItem('me')],
76+
},
77+
{
78+
title: t('All Values'),
79+
type: 'header',
80+
icon: <IconUser size="xs" />,
81+
children: usernames.map(convertToSearchItem),
82+
},
83+
];
84+
}, [members]);
85+
86+
const filterKeys = useMemo(() => {
87+
const entries = Object.entries(AUTOMATION_FILTER_KEYS).map(([key, config]) => [
88+
key,
89+
{
90+
key,
91+
name: key,
92+
predefined: config.predefined,
93+
values: key === 'created_by' ? createdByValues : undefined,
94+
},
95+
]);
96+
97+
return Object.fromEntries(entries);
98+
}, [createdByValues]);
99+
100+
const getFieldDefinition = useCallback<FieldDefinitionGetter>((key: string) => {
101+
return AUTOMATION_FILTER_KEYS[key]?.fieldDefinition ?? null;
102+
}, []);
103+
104+
return {
105+
filterKeys,
106+
getFieldDefinition,
107+
};
108+
}

static/app/views/detectors/detectorViewContainer.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
1+
import {useEffect} from 'react';
12
import {Outlet} from 'react-router-dom';
23

4+
import {fetchOrgMembers} from 'sentry/actionCreators/members';
35
import {PageFiltersContainer} from 'sentry/components/pageFilters/container';
46
import {useWorkflowEngineFeatureGate} from 'sentry/components/workflowEngine/useWorkflowEngineFeatureGate';
7+
import {useApi} from 'sentry/utils/useApi';
8+
import {useOrganization} from 'sentry/utils/useOrganization';
59

610
export default function DetectorViewContainer() {
711
useWorkflowEngineFeatureGate({redirect: true});
812

13+
const api = useApi();
14+
const organization = useOrganization();
15+
16+
useEffect(() => {
17+
fetchOrgMembers(api, organization.slug);
18+
}, [api, organization.slug]);
19+
920
return (
1021
<PageFiltersContainer>
1122
<Outlet />

0 commit comments

Comments
 (0)