Skip to content

Commit e346986

Browse files
authored
feat(dashboards): Add GET endpoint for starred dashboards (#93812)
This endpoint will be where I query for the starred data. It should return dashboards in order of how they are positioned. In a follow up PR I will add code here such that if there are any `None` positions, I'll trigger a backfill and update them given the ordering of their name, which is the current ordering in the sidebar.
1 parent 672893f commit e346986

File tree

4 files changed

+122
-40
lines changed

4 files changed

+122
-40
lines changed

src/sentry/api/endpoints/organization_dashboards_starred.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,54 @@
99
from sentry.api.api_publish_status import ApiPublishStatus
1010
from sentry.api.base import region_silo_endpoint
1111
from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission
12+
from sentry.api.paginator import GenericOffsetPaginator
13+
from sentry.api.serializers.base import serialize
14+
from sentry.api.serializers.models.dashboard import DashboardListSerializer
1215
from sentry.api.serializers.rest_framework.dashboard import DashboardStarredOrderSerializer
1316
from sentry.models.dashboard import DashboardFavoriteUser
1417
from sentry.models.organization import Organization
1518

1619

1720
class MemberPermission(OrganizationPermission):
1821
scope_map = {
22+
"GET": ["member:read", "member:write"],
1923
"PUT": ["member:read", "member:write"],
2024
}
2125

2226

27+
@region_silo_endpoint
28+
class OrganizationDashboardsStarredEndpoint(OrganizationEndpoint):
29+
publish_status = {"GET": ApiPublishStatus.PRIVATE}
30+
owner = ApiOwner.PERFORMANCE
31+
permission_classes = (MemberPermission,)
32+
33+
def has_feature(self, organization, request):
34+
return features.has(
35+
"organizations:dashboards-starred-reordering", organization, actor=request.user
36+
)
37+
38+
def get(self, request: Request, organization: Organization) -> Response:
39+
if not request.user.is_authenticated:
40+
return Response(status=status.HTTP_400_BAD_REQUEST)
41+
42+
if not self.has_feature(organization, request):
43+
return self.respond(status=status.HTTP_404_NOT_FOUND)
44+
45+
favorites = DashboardFavoriteUser.objects.get_favorite_dashboards(
46+
organization=organization, user_id=request.user.id
47+
).select_related("dashboard")
48+
49+
def data_fn(offset, limit):
50+
return [favorite.dashboard for favorite in favorites[offset : offset + limit]]
51+
52+
return self.paginate(
53+
request=request,
54+
paginator=GenericOffsetPaginator(data_fn=data_fn),
55+
on_results=lambda x: serialize(x, request.user, serializer=DashboardListSerializer()),
56+
default_per_page=25,
57+
)
58+
59+
2360
@region_silo_endpoint
2461
class OrganizationDashboardsStarredOrderEndpoint(OrganizationEndpoint):
2562
publish_status = {"PUT": ApiPublishStatus.PRIVATE}

src/sentry/api/urls.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
)
1313
from sentry.api.endpoints.organization_auth_tokens import OrganizationAuthTokensEndpoint
1414
from sentry.api.endpoints.organization_dashboards_starred import (
15+
OrganizationDashboardsStarredEndpoint,
1516
OrganizationDashboardsStarredOrderEndpoint,
1617
)
1718
from sentry.api.endpoints.organization_events_anomalies import OrganizationEventsAnomaliesEndpoint
@@ -1387,6 +1388,16 @@ def create_group_urls(name_prefix: str) -> list[URLPattern | URLResolver]:
13871388
OrganizationDashboardWidgetDetailsEndpoint.as_view(),
13881389
name="sentry-api-0-organization-dashboard-widget-details",
13891390
),
1391+
re_path(
1392+
r"^(?P<organization_id_or_slug>[^/]+)/dashboards/starred/$",
1393+
OrganizationDashboardsStarredEndpoint.as_view(),
1394+
name="sentry-api-0-organization-dashboard-starred",
1395+
),
1396+
re_path(
1397+
r"^(?P<organization_id_or_slug>[^/]+)/dashboards/starred/order/$",
1398+
OrganizationDashboardsStarredOrderEndpoint.as_view(),
1399+
name="sentry-api-0-organization-dashboard-starred-order",
1400+
),
13901401
re_path(
13911402
r"^(?P<organization_id_or_slug>[^/]+)/dashboards/(?P<dashboard_id>[^/]+)/$",
13921403
OrganizationDashboardDetailsEndpoint.as_view(),
@@ -1397,11 +1408,6 @@ def create_group_urls(name_prefix: str) -> list[URLPattern | URLResolver]:
13971408
OrganizationDashboardVisitEndpoint.as_view(),
13981409
name="sentry-api-0-organization-dashboard-visit",
13991410
),
1400-
re_path(
1401-
r"^(?P<organization_id_or_slug>[^/]+)/dashboards/starred/order/$",
1402-
OrganizationDashboardsStarredOrderEndpoint.as_view(),
1403-
name="sentry-api-0-organization-dashboard-starred-order",
1404-
),
14051411
re_path(
14061412
r"^(?P<organization_id_or_slug>[^/]+)/dashboards/(?P<dashboard_id>[^/]+)/favorite/$",
14071413
OrganizationDashboardFavoriteEndpoint.as_view(),

src/sentry/models/dashboard.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from django.db import models, router, transaction
77
from django.db.models import UniqueConstraint
8+
from django.db.models.query import QuerySet
89
from django.utils import timezone
910

1011
from sentry.backup.scopes import RelocationScope
@@ -50,6 +51,16 @@ def get_last_position(self, organization: Organization, user_id: int) -> int:
5051
return last_favorite_dashboard.position
5152
return 0
5253

54+
def get_favorite_dashboards(
55+
self, organization: Organization, user_id: int
56+
) -> QuerySet[DashboardFavoriteUser]:
57+
"""
58+
Returns all favorited dashboards for a user in an organization.
59+
"""
60+
return self.filter(organization=organization, user_id=user_id).order_by(
61+
"position", "dashboard__title"
62+
)
63+
5364
def get_favorite_dashboard(
5465
self, organization: Organization, user_id: int, dashboard: Dashboard
5566
) -> DashboardFavoriteUser | None:

tests/sentry/api/endpoints/test_organization_dashboards_starred.py

Lines changed: 63 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,60 @@
55
from sentry.testutils.cases import OrganizationDashboardWidgetTestCase
66

77

8-
class OrganizationDashboardsStarredOrderTest(OrganizationDashboardWidgetTestCase):
8+
class StarredDashboardTestCase(OrganizationDashboardWidgetTestCase):
9+
def create_dashboard_favorite(self, dashboard, user, organization, position):
10+
DashboardFavoriteUser.objects.create(
11+
dashboard=dashboard, user_id=user.id, organization=organization, position=position
12+
)
13+
14+
def do_request(self, *args, **kwargs):
15+
with self.feature("organizations:dashboards-starred-reordering"):
16+
return super().do_request(*args, **kwargs)
17+
18+
19+
class OrganizationDashboardsStarredTest(StarredDashboardTestCase):
920
def setUp(self):
1021
super().setUp()
1122
self.login_as(self.user)
1223
self.url = reverse(
13-
"sentry-api-0-organization-dashboard-starred-order",
24+
"sentry-api-0-organization-dashboard-starred",
1425
kwargs={"organization_id_or_slug": self.organization.slug},
1526
)
1627
self.dashboard_1 = self.create_dashboard(title="Dashboard 1")
1728
self.dashboard_2 = self.create_dashboard(title="Dashboard 2")
1829
self.dashboard_3 = self.create_dashboard(title="Dashboard 3")
1930

20-
def create_dashboard_favorite(self, dashboard, user, organization, position):
21-
DashboardFavoriteUser.objects.create(
22-
dashboard=dashboard, user_id=user.id, organization=organization, position=position
31+
def test_get_favorite_dashboards(self):
32+
self.create_dashboard_favorite(self.dashboard_1, self.user, self.organization, 2)
33+
self.create_dashboard_favorite(self.dashboard_2, self.user, self.organization, 0)
34+
self.create_dashboard_favorite(self.dashboard_3, self.user, self.organization, 1)
35+
36+
# Add a dashboard starred by another user to verify that it is not returned
37+
other_user = self.create_user("[email protected]")
38+
other_dashboard = self.create_dashboard(title="Other Dashboard")
39+
self.create_dashboard_favorite(other_dashboard, other_user, self.organization, 0)
40+
41+
response = self.do_request("get", self.url)
42+
assert response.status_code == 200
43+
assert len(response.data) == 3
44+
assert [int(dashboard["id"]) for dashboard in response.data] == [
45+
self.dashboard_2.id,
46+
self.dashboard_3.id,
47+
self.dashboard_1.id,
48+
]
49+
50+
51+
class OrganizationDashboardsStarredOrderTest(StarredDashboardTestCase):
52+
def setUp(self):
53+
super().setUp()
54+
self.login_as(self.user)
55+
self.url = reverse(
56+
"sentry-api-0-organization-dashboard-starred-order",
57+
kwargs={"organization_id_or_slug": self.organization.slug},
2358
)
59+
self.dashboard_1 = self.create_dashboard(title="Dashboard 1")
60+
self.dashboard_2 = self.create_dashboard(title="Dashboard 2")
61+
self.dashboard_3 = self.create_dashboard(title="Dashboard 3")
2462

2563
def test_reorder_dashboards(self):
2664
self.create_dashboard_favorite(self.dashboard_1, self.user, self.organization, 0)
@@ -40,14 +78,11 @@ def test_reorder_dashboards(self):
4078
]
4179

4280
# Reorder the favorited dashboards
43-
with self.feature("organizations:dashboards-starred-reordering"):
44-
response = self.do_request(
45-
"put",
46-
self.url,
47-
data={
48-
"dashboard_ids": [self.dashboard_3.id, self.dashboard_1.id, self.dashboard_2.id]
49-
},
50-
)
81+
response = self.do_request(
82+
"put",
83+
self.url,
84+
data={"dashboard_ids": [self.dashboard_3.id, self.dashboard_1.id, self.dashboard_2.id]},
85+
)
5186
assert response.status_code == 204
5287

5388
assert list(
@@ -67,14 +102,11 @@ def test_throws_an_error_if_dashboard_ids_are_not_unique(self):
67102
self.create_dashboard_favorite(self.dashboard_2, self.user, self.organization, 1)
68103
self.create_dashboard_favorite(self.dashboard_3, self.user, self.organization, 2)
69104

70-
with self.feature("organizations:dashboards-starred-reordering"):
71-
response = self.do_request(
72-
"put",
73-
self.url,
74-
data={
75-
"dashboard_ids": [self.dashboard_1.id, self.dashboard_1.id, self.dashboard_2.id]
76-
},
77-
)
105+
response = self.do_request(
106+
"put",
107+
self.url,
108+
data={"dashboard_ids": [self.dashboard_1.id, self.dashboard_1.id, self.dashboard_2.id]},
109+
)
78110
assert response.status_code == 400
79111
assert response.data == {
80112
"dashboard_ids": ["Single dashboard cannot take up multiple positions"]
@@ -85,12 +117,11 @@ def test_throws_an_error_if_reordered_dashboard_ids_are_not_complete(self):
85117
self.create_dashboard_favorite(self.dashboard_2, self.user, self.organization, 1)
86118
self.create_dashboard_favorite(self.dashboard_3, self.user, self.organization, 2)
87119

88-
with self.feature("organizations:dashboards-starred-reordering"):
89-
response = self.do_request(
90-
"put",
91-
self.url,
92-
data={"dashboard_ids": [self.dashboard_1.id, self.dashboard_2.id]},
93-
)
120+
response = self.do_request(
121+
"put",
122+
self.url,
123+
data={"dashboard_ids": [self.dashboard_1.id, self.dashboard_2.id]},
124+
)
94125
assert response.status_code == 400
95126
assert response.data == {
96127
"detail": ErrorDetail(
@@ -104,14 +135,11 @@ def test_allows_reordering_even_if_no_initial_positions(self):
104135
self.create_dashboard_favorite(self.dashboard_2, self.user, self.organization, 1)
105136
self.create_dashboard_favorite(self.dashboard_3, self.user, self.organization, 2)
106137

107-
with self.feature("organizations:dashboards-starred-reordering"):
108-
response = self.do_request(
109-
"put",
110-
self.url,
111-
data={
112-
"dashboard_ids": [self.dashboard_3.id, self.dashboard_1.id, self.dashboard_2.id]
113-
},
114-
)
138+
response = self.do_request(
139+
"put",
140+
self.url,
141+
data={"dashboard_ids": [self.dashboard_3.id, self.dashboard_1.id, self.dashboard_2.id]},
142+
)
115143
assert response.status_code == 204
116144

117145
assert list(

0 commit comments

Comments
 (0)