Skip to content

Commit 6928633

Browse files
leedongweiandrewshie-sentry
authored andcommitted
feat(aci): UI to filter monitors by assignee (#95930)
### Step 1 <img width="1200" height="650" alt="Screenshot 2025-07-14 at 4 09 47 PM" src="https://github.com/user-attachments/assets/72a1f1d5-333d-4aeb-b1f7-978bcfb5a9f2" /> ### Step 2 <img width="1200" height="684" alt="Screenshot 2025-07-14 at 4 10 01 PM" src="https://github.com/user-attachments/assets/159aa917-ca20-40ce-9b5a-6beded742fdc" /> ### Step 3 <img width="1200" height="502" alt="Screenshot 2025-07-14 at 4 10 18 PM" src="https://github.com/user-attachments/assets/a959e87d-98d1-40de-a627-20cc7e701464" />
1 parent 8ca6361 commit 6928633

File tree

4 files changed

+64
-38
lines changed

4 files changed

+64
-38
lines changed

static/app/views/detectors/components/detectorSearch.tsx

Lines changed: 6 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,25 @@
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 {DETECTOR_FILTER_KEYS} from 'sentry/views/detectors/constants';
3+
import {getFieldDefinition} from 'sentry/utils/fields';
4+
import {useDetectorFilterKeys} from 'sentry/views/detectors/utils/useDetectorFilterKeys';
75

86
type DetectorSearchProps = {
97
initialQuery: string;
108
onSearch: (query: string) => void;
119
};
1210

13-
function getDetectorFilterKeyDefinition(filterKey: string): FieldDefinition | null {
14-
if (DETECTOR_FILTER_KEYS.hasOwnProperty(filterKey) && DETECTOR_FILTER_KEYS[filterKey]) {
15-
const {description, valueType, keywords, values} = DETECTOR_FILTER_KEYS[filterKey];
16-
17-
return {
18-
kind: FieldKind.FIELD,
19-
desc: description,
20-
valueType,
21-
keywords,
22-
values,
23-
};
24-
}
25-
26-
return null;
27-
}
28-
29-
const FILTER_KEYS: TagCollection = Object.fromEntries(
30-
Object.keys(DETECTOR_FILTER_KEYS).map(key => {
31-
const {values} = DETECTOR_FILTER_KEYS[key] ?? {};
32-
33-
return [
34-
key,
35-
{
36-
key,
37-
name: key,
38-
predefined: values !== undefined,
39-
values,
40-
},
41-
];
42-
})
43-
);
44-
4511
export function DetectorSearch({initialQuery, onSearch}: DetectorSearchProps) {
12+
const filterKeys = useDetectorFilterKeys();
13+
4614
return (
4715
<SearchQueryBuilder
4816
initialQuery={initialQuery}
4917
placeholder={t('Search for monitors')}
5018
onSearch={onSearch}
51-
filterKeys={FILTER_KEYS}
19+
filterKeys={filterKeys}
5220
getTagValues={() => Promise.resolve([])}
5321
searchSource="detectors-list"
54-
fieldDefinitionGetter={getDetectorFilterKeyDefinition}
22+
fieldDefinitionGetter={getFieldDefinition}
5523
disallowUnsupportedFilters
5624
disallowWildcard
5725
disallowLogicalOperators

static/app/views/detectors/constants.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,9 @@ export const DETECTOR_FILTER_KEYS: Record<
3636
] satisfies DetectorType[],
3737
keywords: ['type'],
3838
},
39+
assignee: {
40+
description: 'User or team assigned to the monitor',
41+
valueType: FieldValueType.STRING,
42+
keywords: ['assigned', 'owner'],
43+
},
3944
};

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,31 @@ describe('DetectorsList', function () {
171171
expect(mockDetectorsRequestErrorType).toHaveBeenCalled();
172172
});
173173

174+
it('can filter by assignee', async function () {
175+
const testUser = UserFixture({id: '2', email: '[email protected]'});
176+
const mockDetectorsRequestAssignee = MockApiClient.addMockResponse({
177+
url: '/organizations/org-slug/detectors/',
178+
body: [MetricDetectorFixture({name: 'Assigned Detector', owner: testUser.id})],
179+
match: [MockApiClient.matchQuery({query: 'assignee:[email protected]'})],
180+
});
181+
182+
render(<DetectorsList />, {organization});
183+
await screen.findByText('Detector 1');
184+
185+
// Click through menus to select assignee
186+
const searchInput = await screen.findByRole('combobox', {
187+
name: 'Add a search term',
188+
});
189+
await userEvent.type(searchInput, 'assignee:[email protected]');
190+
191+
// It takes two enters. One to enter the search term, and one to submit the search.
192+
await userEvent.keyboard('{enter}');
193+
await userEvent.keyboard('{enter}');
194+
195+
await screen.findByText('Assigned Detector');
196+
expect(mockDetectorsRequestAssignee).toHaveBeenCalled();
197+
});
198+
174199
it('can sort the table', async function () {
175200
const mockDetectorsRequest = MockApiClient.addMockResponse({
176201
url: '/organizations/org-slug/detectors/',
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {useMemo} from 'react';
2+
3+
import type {TagCollection} from 'sentry/types/group';
4+
import useAssignedSearchValues from 'sentry/utils/membersAndTeams/useAssignedSearchValues';
5+
import {DETECTOR_FILTER_KEYS} from 'sentry/views/detectors/constants';
6+
7+
export function useDetectorFilterKeys(): TagCollection {
8+
const assignedValues = useAssignedSearchValues();
9+
10+
return useMemo(() => {
11+
return Object.fromEntries(
12+
Object.entries(DETECTOR_FILTER_KEYS).map(([key, config]) => {
13+
const {values} = config ?? {};
14+
const isAssignee = key === 'assignee';
15+
16+
return [
17+
key,
18+
{
19+
key,
20+
name: key,
21+
predefined: isAssignee || values !== undefined,
22+
values: isAssignee ? assignedValues : values,
23+
},
24+
];
25+
})
26+
);
27+
}, [assignedValues]);
28+
}

0 commit comments

Comments
 (0)