Skip to content

Commit 9c51892

Browse files
committed
feat(aci): Allow user to filter monitors by assignee
1 parent 2c8fdfc commit 9c51892

File tree

3 files changed

+114
-20
lines changed

3 files changed

+114
-20
lines changed

src/sentry/workflow_engine/endpoints/organization_detector_index.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from sentry.issues import grouptype
3030
from sentry.models.organization import Organization
3131
from sentry.models.project import Project
32+
from sentry.search.utils import parse_actor_or_none_value
3233
from sentry.workflow_engine.endpoints.serializers import DetectorSerializer
3334
from sentry.workflow_engine.endpoints.utils.filters import apply_filter
3435
from sentry.workflow_engine.endpoints.utils.sortby import SortByParam
@@ -39,12 +40,40 @@
3940
detector_search_config = SearchConfig.create_from(
4041
default_config,
4142
text_operator_keys={"name", "type"},
42-
allowed_keys={"name", "type"},
43+
allowed_keys={"name", "type", "assignee"},
4344
allow_boolean=False,
4445
free_text_key="query",
4546
)
4647
parse_detector_query = partial(base_parse_search_query, config=detector_search_config)
4748

49+
50+
def convert_assignee_value(value: str, projects: list[Project], user) -> Q:
51+
"""
52+
Convert an assignee search value to a Django Q object for filtering detectors.
53+
"""
54+
if value == "me":
55+
# Handle "me" keyword to search for current user
56+
return Q(owner_user_id=user.id)
57+
elif value == "unassigned":
58+
# Handle "unassigned" keyword
59+
return Q(owner_user_id__isnull=True, owner_team__isnull=True)
60+
else:
61+
# Parse actor (user or team) and create appropriate Q object
62+
actor = parse_actor_or_none_value(projects, value, user)
63+
if actor is None:
64+
# If we can't parse the actor, return a query that matches nothing
65+
return Q(pk__isnull=True)
66+
elif hasattr(actor, "id") and hasattr(actor, "_meta") and actor._meta.model_name == "user":
67+
# It's a user
68+
return Q(owner_user_id=actor.id)
69+
elif hasattr(actor, "id") and hasattr(actor, "_meta") and actor._meta.model_name == "team":
70+
# It's a team
71+
return Q(owner_team_id=actor.id)
72+
else:
73+
# Unknown actor type, return a query that matches nothing
74+
return Q(pk__isnull=True)
75+
76+
4877
# Maps API field name to database field name, with synthetic aggregate fields keeping
4978
# to our field naming scheme for consistency.
5079
SORT_ATTRS = {
@@ -134,6 +163,24 @@ def get(self, request: Request, organization: Organization) -> Response:
134163
queryset = apply_filter(queryset, filter, "name")
135164
case SearchFilter(key=SearchKey("type"), operator=("=" | "IN" | "!=")):
136165
queryset = apply_filter(queryset, filter, "type")
166+
case SearchFilter(key=SearchKey("assignee"), operator=("=" | "IN" | "!=")):
167+
breakpoint()
168+
# Handle assignee filtering with support for users, teams, "me", and "unassigned"
169+
if isinstance(filter.value.value, list):
170+
# Handle multiple values (IN operator)
171+
assignee_q = Q()
172+
for value in filter.value.value:
173+
assignee_q |= convert_assignee_value(value, projects, request.user)
174+
else:
175+
# Handle single value
176+
assignee_q = convert_assignee_value(
177+
filter.value.value, projects, request.user
178+
)
179+
180+
if filter.operator == "!=":
181+
queryset = queryset.exclude(assignee_q)
182+
else:
183+
queryset = queryset.filter(assignee_q)
137184
case SearchFilter(key=SearchKey("query"), operator="="):
138185
# 'query' is our free text key; all free text gets returned here
139186
# as '=', and we search any relevant fields for it.

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

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import {SearchQueryBuilder} from 'sentry/components/searchQueryBuilder';
22
import {t} from 'sentry/locale';
3-
import type {TagCollection} from 'sentry/types/group';
3+
import type {Tag, TagCollection} from 'sentry/types/group';
44
import type {FieldDefinition} from 'sentry/utils/fields';
55
import {FieldKind} from 'sentry/utils/fields';
6+
import useAssignedSearchValues from 'sentry/utils/membersAndTeams/useAssignedSearchValues';
67
import {useLocation} from 'sentry/utils/useLocation';
78
import {useNavigate} from 'sentry/utils/useNavigate';
89
import {DETECTOR_FILTER_KEYS} from 'sentry/views/detectors/constants';
@@ -23,27 +24,68 @@ function getDetectorFilterKeyDefinition(filterKey: string): FieldDefinition | nu
2324
return null;
2425
}
2526

26-
const FILTER_KEYS: TagCollection = Object.fromEntries(
27-
Object.keys(DETECTOR_FILTER_KEYS).map(key => {
28-
const {values} = DETECTOR_FILTER_KEYS[key] ?? {};
29-
30-
return [
31-
key,
32-
{
33-
key,
34-
name: key,
35-
predefined: values !== undefined,
36-
values,
37-
},
38-
];
39-
})
40-
);
41-
4227
export function DetectorSearch() {
4328
const location = useLocation();
4429
const navigate = useNavigate();
30+
const assignedValues = useAssignedSearchValues();
4531
const query = typeof location.query.query === 'string' ? location.query.query : '';
4632

33+
const filterKeys: TagCollection = Object.fromEntries(
34+
Object.keys(DETECTOR_FILTER_KEYS).map(key => {
35+
const {values} = DETECTOR_FILTER_KEYS[key] ?? {};
36+
37+
// Special handling for assignee field to provide user/team values
38+
if (key === 'assignee') {
39+
return [
40+
key,
41+
{
42+
key,
43+
name: key,
44+
predefined: true,
45+
values: assignedValues,
46+
},
47+
];
48+
}
49+
50+
return [
51+
key,
52+
{
53+
key,
54+
name: key,
55+
predefined: values !== undefined,
56+
values,
57+
},
58+
];
59+
})
60+
);
61+
62+
const getTagValues = (tag: Tag, _query: string): string[] => {
63+
// For assignee field, return the assigned values filtered by query
64+
if (tag.key === 'assignee') {
65+
const allAssigneeValues: string[] = [];
66+
assignedValues.forEach(group => {
67+
if (Array.isArray(group.children)) {
68+
group.children.forEach(child => {
69+
if (typeof child.value === 'string') {
70+
allAssigneeValues.push(child.value);
71+
}
72+
});
73+
}
74+
});
75+
76+
// Filter by query if provided
77+
if (_query) {
78+
return allAssigneeValues.filter(value =>
79+
value.toLowerCase().includes(query.toLowerCase())
80+
);
81+
}
82+
return allAssigneeValues;
83+
}
84+
85+
// For other fields, return empty array (no dynamic values)
86+
return [];
87+
};
88+
4789
return (
4890
<SearchQueryBuilder
4991
initialQuery={query}
@@ -57,8 +99,8 @@ export function DetectorSearch() {
5799
},
58100
});
59101
}}
60-
filterKeys={FILTER_KEYS}
61-
getTagValues={() => Promise.resolve([])}
102+
filterKeys={filterKeys}
103+
getTagValues={getTagValues}
62104
searchSource="detectors-list"
63105
fieldDefinitionGetter={getDetectorFilterKeyDefinition}
64106
disallowUnsupportedFilters

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 detector',
41+
valueType: FieldValueType.STRING,
42+
keywords: ['assigned', 'owner'],
43+
},
3944
};

0 commit comments

Comments
 (0)