Skip to content

Commit 542d3d6

Browse files
authored
feat(dashboards): Add environment and release filters to endpoint (#95271)
Since these fields need to be displayed on the list view now, we are exposing `environment` and `releases` in the same way as the details endpoint. The details endpoint needs other data like start, end, period, etc but I've made a mixin that can be shared across the two serializer classes to fetch filter information that we can expose as we need in the response. The keys are split into page filters and tag filters because we hope to extend this list in the future. Requires #95246 Closes DAIN-712
1 parent ffcf084 commit 542d3d6

File tree

4 files changed

+87
-31
lines changed

4 files changed

+87
-31
lines changed

src/sentry/api/serializers/models/dashboard.py

Lines changed: 67 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,8 @@ class DashboardListResponse(TypedDict):
187187
title: str
188188
dateCreated: str
189189
createdBy: UserSerializerResponse
190+
environment: list[str]
191+
filters: DashboardFilters
190192
widgetDisplay: list[str]
191193
widgetPreview: list[dict[str, str]]
192194
permissions: DashboardPermissionsResponse | None
@@ -206,9 +208,59 @@ class _Widget(TypedDict):
206208
permissions: NotRequired[dict[str, Any]]
207209
is_favorited: NotRequired[bool]
208210
projects: list[int]
211+
environment: list[str]
212+
filters: DashboardFilters
213+
209214

215+
class PageFiltersOptional(TypedDict, total=False):
216+
period: str
217+
utc: str
218+
expired: bool
219+
start: datetime
220+
end: str
221+
222+
223+
class PageFilters(PageFiltersOptional):
224+
projects: list[int]
225+
environment: list[str]
226+
227+
228+
class DashboardFiltersMixin:
229+
def get_filters(self, obj: Dashboard) -> tuple[PageFilters, DashboardFilters]:
230+
from sentry.api.serializers.rest_framework.base import camel_to_snake_case
210231

211-
class DashboardListSerializer(Serializer):
232+
dashboard_filters = obj.get_filters()
233+
page_filters: PageFilters = {
234+
"projects": dashboard_filters.get("projects", []),
235+
"environment": dashboard_filters.get("environment", []),
236+
"expired": dashboard_filters.get("expired", False),
237+
}
238+
start, end, period = (
239+
dashboard_filters.get("start"),
240+
dashboard_filters.get("end"),
241+
dashboard_filters.get("period"),
242+
)
243+
if start and end:
244+
start, end = parse_timestamp(start), parse_timestamp(end)
245+
page_filters["expired"], page_filters["start"] = outside_retention_with_modified_start(
246+
start, end, obj.organization
247+
)
248+
page_filters["end"] = end
249+
elif period:
250+
page_filters["period"] = period
251+
252+
if dashboard_filters.get("utc") is not None:
253+
page_filters["utc"] = dashboard_filters["utc"]
254+
255+
tag_filters: DashboardFilters = {}
256+
for filter_key in ("release", "releaseId"):
257+
if dashboard_filters.get(camel_to_snake_case(filter_key)):
258+
tag_filters[filter_key] = dashboard_filters[camel_to_snake_case(filter_key)]
259+
260+
return page_filters, tag_filters
261+
262+
263+
class DashboardListSerializer(Serializer, DashboardFiltersMixin):
212264
def get_attrs(self, item_list, user, **kwargs):
213265
item_dict = {i.id: i for i in item_list}
214266
prefetch_related_objects(item_list, "projects")
@@ -232,6 +284,8 @@ def get_attrs(self, item_list, user, **kwargs):
232284
"widget_preview": [],
233285
"created_by": {},
234286
"projects": [],
287+
"environment": [],
288+
"filters": {},
235289
}
236290
)
237291
for widget in widgets:
@@ -272,11 +326,11 @@ def get_attrs(self, item_list, user, **kwargs):
272326
for dashboard in item_dict.values():
273327
result[dashboard]["created_by"] = serialized_users.get(str(dashboard.created_by_id))
274328
result[dashboard]["is_favorited"] = dashboard.id in favorited_dashboard_ids
275-
result[dashboard]["projects"] = list(dashboard.projects.values_list("id", flat=True))
276329

277-
filters = dashboard.get_filters()
278-
if filters and filters.get("projects"):
279-
result[dashboard]["projects"] = filters["projects"]
330+
page_filters, tag_filters = self.get_filters(dashboard)
331+
result[dashboard]["projects"] = page_filters.get("projects", [])
332+
result[dashboard]["environment"] = page_filters.get("environment", [])
333+
result[dashboard]["filters"] = tag_filters
280334

281335
return result
282336

@@ -291,6 +345,8 @@ def serialize(self, obj, attrs, user, **kwargs) -> DashboardListResponse:
291345
"permissions": attrs.get("permissions", None),
292346
"isFavorited": attrs.get("is_favorited", False),
293347
"projects": attrs.get("projects", []),
348+
"environment": attrs.get("environment", []),
349+
"filters": attrs.get("filters", {}),
294350
}
295351

296352

@@ -321,7 +377,7 @@ class DashboardDetailsResponse(DashboardDetailsResponseOptional):
321377

322378

323379
@register(Dashboard)
324-
class DashboardDetailsModelSerializer(Serializer):
380+
class DashboardDetailsModelSerializer(Serializer, DashboardFiltersMixin):
325381
def get_attrs(self, item_list, user, **kwargs):
326382
result = {}
327383

@@ -341,37 +397,19 @@ def get_attrs(self, item_list, user, **kwargs):
341397
return result
342398

343399
def serialize(self, obj, attrs, user, **kwargs) -> DashboardDetailsResponse:
344-
from sentry.api.serializers.rest_framework.base import camel_to_snake_case
345-
346-
dashboard_filters = obj.get_filters()
400+
page_filters, tag_filters = self.get_filters(obj)
347401
data: DashboardDetailsResponse = {
348402
"id": str(obj.id),
349403
"title": obj.title,
350404
"dateCreated": obj.date_added,
351405
"createdBy": user_service.serialize_many(filter={"user_ids": [obj.created_by_id]})[0],
352406
"widgets": attrs["widgets"],
353-
"projects": dashboard_filters.get("projects", []),
354-
"filters": {},
407+
"filters": tag_filters,
355408
"permissions": serialize(obj.permissions) if hasattr(obj, "permissions") else None,
356409
"isFavorited": user.id in obj.favorited_by,
410+
"projects": page_filters.get("projects", []),
411+
"environment": page_filters.get("environment", []),
412+
**page_filters,
357413
}
358414

359-
# TODO: The logic for obtaining these filters will be moved to the Dashboard model.
360-
if obj.filters is not None:
361-
for tl_key in ("environment", "period", "utc"):
362-
if obj.filters.get(tl_key) is not None:
363-
data[tl_key] = obj.filters[tl_key]
364-
365-
for filter_key in ("release", "releaseId"):
366-
if obj.filters.get(camel_to_snake_case(filter_key)):
367-
data["filters"][filter_key] = obj.filters[camel_to_snake_case(filter_key)]
368-
369-
start, end = obj.filters.get("start"), obj.filters.get("end")
370-
if start and end:
371-
start, end = parse_timestamp(start), parse_timestamp(end)
372-
data["expired"], data["start"] = outside_retention_with_modified_start(
373-
start, end, obj.organization
374-
)
375-
data["end"] = end
376-
377415
return data

src/sentry/apidocs/examples/dashboard_examples.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
],
6868
"projects": [1],
6969
"filters": {},
70+
"environment": ["alpha"],
7071
"period": "7d",
7172
"permissions": {
7273
"isEditableByEveryone": True,
@@ -81,6 +82,13 @@
8182
"title": "Dashboard",
8283
"dateCreated": "2024-06-20T14:38:03.498574Z",
8384
"projects": [1],
85+
"environment": ["alpha"],
86+
"filters": {
87+
"release": [
88+
"frontend@a02311a400636ff9640b3e4ca2991ee153dbbdcc",
89+
"frontend@36934c05140c16df93aa8ebf671f9386e916b501",
90+
]
91+
},
8492
"createdBy": {
8593
"id": "1",
8694
"name": "Admin",
@@ -114,6 +122,13 @@
114122
"title": "Dashboard",
115123
"dateCreated": "2024-06-20T14:38:03.498574Z",
116124
"projects": [],
125+
"environment": ["alpha"],
126+
"filters": {
127+
"release": [
128+
"frontend@a02311a400636ff9640b3e4ca2991ee153dbbdcc",
129+
"frontend@36934c05140c16df93aa8ebf671f9386e916b501",
130+
]
131+
},
117132
"createdBy": {
118133
"id": "1",
119134
"name": "Admin",

src/sentry/models/dashboard.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ def get_filters(self) -> dict[str, Any]:
335335
)
336336

337337
return {
338+
**(self.filters or {}),
338339
"projects": projects,
339340
}
340341

tests/sentry/api/endpoints/test_organization_dashboards.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -723,18 +723,20 @@ def test_get_shared_dashboards_across_organizations(self):
723723
values = [row["title"] for row in response.data]
724724
assert values == ["General", "Initial dashboard"]
725725

726-
def test_get_with_all_projects_filter(self):
726+
def test_get_with_filters(self):
727727
Dashboard.objects.create(
728728
title="Dashboard with all projects filter",
729729
organization=self.organization,
730730
created_by_id=self.user.id,
731-
filters={"all_projects": True},
731+
filters={"all_projects": True, "environment": ["alpha"], "release": ["v1"]},
732732
)
733733
response = self.client.get(self.url, data={"query": "Dashboard with all projects filter"})
734734
assert response.status_code == 200, response.content
735735
assert len(response.data) == 1
736736
assert response.data[0]["title"] == "Dashboard with all projects filter"
737737
assert response.data[0].get("projects") == [-1]
738+
assert response.data[0].get("environment") == ["alpha"]
739+
assert response.data[0].get("filters") == {"release": ["v1"]}
738740

739741
def test_post(self):
740742
response = self.do_request("post", self.url, data={"title": "Dashboard from Post"})

0 commit comments

Comments
 (0)