Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Teams redesign #1528

Merged
merged 60 commits into from
Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
4f2a59b
Add alert group filtering based on owning team
iskhakov Mar 11, 2023
5aea1ee
part 2
iskhakov Mar 13, 2023
6f6548b
part 3
iskhakov Mar 13, 2023
b0201af
Rename owning team filter to team
iskhakov Mar 13, 2023
0230f0d
Add filters for schedule and escalation chains
iskhakov Mar 13, 2023
d98d675
Add integrations filter and maintenance filters endpoint
iskhakov Mar 13, 2023
9b3784e
Show team name next to the resource
iskhakov Mar 13, 2023
3535638
add RemoteFilters
Mar 14, 2023
8728e7d
move cards out of incidents filters
Mar 14, 2023
bd3defb
Refactor team filter
iskhakov Mar 15, 2023
6b40193
add RemoteFilters to all pages
Mar 15, 2023
4f1ce1d
Merge branch 'iskhakov/teams-refactoring' of github.com:grafana/oncal…
Mar 15, 2023
4a1fe3e
Display team
iskhakov Mar 15, 2023
78d4c60
add team selector to forms
Mar 15, 2023
a0f10eb
Merge branch 'iskhakov/teams-refactoring' of github.com:grafana/oncal…
Mar 15, 2023
4c3a600
Add teams settings page
iskhakov Mar 15, 2023
ef83c15
Fix bug
iskhakov Mar 15, 2023
3c6963a
Add schedule filters
iskhakov Mar 15, 2023
05f4fd4
Add schedule filters
iskhakov Mar 15, 2023
d7e4252
Add schedule filters
iskhakov Mar 15, 2023
b58a2cb
Add modal window for setting
iskhakov Mar 15, 2023
2df5c11
add TeamSettings modal, add Private esc chain render
Mar 15, 2023
7a9c39d
Clean up and add style name
iskhakov Mar 16, 2023
762279b
add private schedule, webhook display
Mar 16, 2023
6450113
Merge branch 'iskhakov/teams-refactoring' of github.com:grafana/oncal…
Mar 16, 2023
862bc71
Fix bug when unable to set default team
iskhakov Mar 16, 2023
78a544b
show team settings in main settings for old nav only
Mar 16, 2023
5d87a0b
Merge branch 'iskhakov/teams-refactoring' of github.com:grafana/oncal…
Mar 16, 2023
a0c7a27
Add shared teams support, fix team change for integration
iskhakov Mar 16, 2023
bf8b1c1
fix ts errors
Mar 16, 2023
c06fdc2
Merge branch 'iskhakov/teams-refactoring' of github.com:grafana/oncal…
Mar 16, 2023
3c9b32c
Clean up
iskhakov Mar 16, 2023
23e28f0
Add global flag to teams filter
iskhakov Mar 16, 2023
294b226
add support for global filters
Mar 16, 2023
7f8601b
fix integration team preselected, fix global filters
Mar 16, 2023
ae47988
Add team acess control to rest of the resources
iskhakov Mar 16, 2023
3d322dc
Cleanup
iskhakov Mar 16, 2023
1e424b1
Update grafana-plugin/src/components/PageErrorHandlingWrapper/PageErr…
iskhakov Mar 20, 2023
3d5aa3b
Apply suggestions from code review
iskhakov Mar 20, 2023
8da4b52
Fixes for code review
iskhakov Mar 20, 2023
dd364e5
Fix typos
iskhakov Mar 20, 2023
8108c10
Code optimisations after reviews
iskhakov Mar 20, 2023
c09edf1
Add tooltips and descriptions
iskhakov Mar 20, 2023
c12962a
Merge branch 'dev' into iskhakov/teams-refactoring
Mar 20, 2023
faaffcc
Deduplicate team ModelMultipleChoiceFilter
iskhakov Mar 20, 2023
68871ae
Add team field to direct paging
iskhakov Mar 20, 2023
d3dbabd
fix ts
Mar 20, 2023
e8438d9
fix escalation chains filtering
Mar 20, 2023
e365ed7
disable outgoing webhooks page test
Mar 20, 2023
6c8000b
fix remote filters parsers
Mar 20, 2023
9270721
Merge branch 'dev' into iskhakov/teams-refactoring
iskhakov Mar 21, 2023
cd54dfb
Change how team is displayed
iskhakov Mar 21, 2023
85d4468
Fix tests and minor bugs
iskhakov Mar 21, 2023
d7b849c
Merge branch 'dev' into iskhakov/teams-refactoring
iskhakov Mar 21, 2023
061899e
fix integrations and esc chains page autoselect
Mar 21, 2023
a245912
Add team selector to alert groups and add more descriptions
iskhakov Mar 21, 2023
b702d8c
Merge branch 'iskhakov/teams-refactoring' of github.com:grafana/oncal…
Mar 21, 2023
7665001
Fix integration tests and comment out integrations search test
iskhakov Mar 21, 2023
6d9217c
Merge branch 'dev' into iskhakov/teams-refactoring
iskhakov Mar 21, 2023
7733ab6
Add entry to changelog
iskhakov Mar 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 2 additions & 41 deletions engine/apps/alerts/models/alert_receive_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from django.conf import settings
from django.core.validators import MinLengthValidator
from django.db import models, transaction
from django.db.models import Count, Q
from django.db.models import Q
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils import timezone
Expand Down Expand Up @@ -236,55 +236,16 @@ def hard_delete(self):
super(AlertReceiveChannel, self).delete()

def change_team(self, team_id, user):
EscalationPolicy = apps.get_model("alerts", "EscalationPolicy")

if team_id == self.team_id:
raise TeamCanNotBeChangedError("Integration is already in this team")

if team_id is not None:
new_team = user.teams.filter(public_primary_key=team_id).first()
new_team = user.available_teams.filter(public_primary_key=team_id).first()
if not new_team:
raise TeamCanNotBeChangedError("User is not a member of the selected team")
else:
new_team = None # means General team

iskhakov marked this conversation as resolved.
Show resolved Hide resolved
escalation_chains_pks = self.channel_filters.all().values_list("escalation_chain", flat=True)
escalation_chains = self.organization.escalation_chains.filter(pk__in=escalation_chains_pks).annotate(
num_integrations=Count(
"channel_filters__alert_receive_channel",
distinct=True,
filter=Q(channel_filters__alert_receive_channel__deleted_at__isnull=True),
),
)
if escalation_chains:
# check if escalation chains are connected to routes of other integrations
shared_escalation_chains = []
for escalation_chain in escalation_chains:
if escalation_chain.num_integrations > 1:
shared_escalation_chains.append(escalation_chain)
if shared_escalation_chains:
shared_escalation_chains_verbal = ", ".join([ec.name for ec in shared_escalation_chains])
raise TeamCanNotBeChangedError(
f"Team cannot be changed because one or more escalation chain of integration routes "
f"is connected to other integration: {shared_escalation_chains_verbal}"
)

escalation_policies = EscalationPolicy.objects.filter(escalation_chain__in=escalation_chains)

users_in_escalation = self.organization.users.filter(escalationpolicy__in=escalation_policies)
if new_team:
team_members = new_team.users.filter(pk__in=[user.pk for user in users_in_escalation])
else:
team_members = self.organization.users.filter(pk__in=[user.pk for user in users_in_escalation])
not_team_members = set(users_in_escalation) - set(team_members)
if not_team_members:
not_team_members_verbal = ", ".join([user.username for user in not_team_members])
raise TeamCanNotBeChangedError(
f"Team cannot be changed because one or more user from escalation chain(s) is not a member "
f"of the selected team: {not_team_members_verbal}"
)

escalation_chains.update(team=new_team)
self.team = new_team
self.save(update_fields=["team"])

Expand Down
3 changes: 3 additions & 0 deletions engine/apps/api/serializers/alert_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from apps.alerts.incident_appearance.renderers.web_renderer import AlertGroupWebRenderer
from apps.alerts.models import AlertGroup, AlertGroupLogRecord
from apps.user_management.models import User
from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField
from common.api_helpers.mixins import EagerLoadingMixin

from .alert import AlertSerializer
Expand Down Expand Up @@ -82,6 +83,7 @@ class AlertGroupListSerializer(EagerLoadingMixin, AlertGroupFieldsCacheSerialize
related_users = serializers.SerializerMethodField()
dependent_alert_groups = ShortAlertGroupSerializer(many=True)
root_alert_group = ShortAlertGroupSerializer()
team = TeamPrimaryKeyRelatedField(source="channel.team", allow_null=True)

alerts_count = serializers.IntegerField(read_only=True)
render_for_web = serializers.SerializerMethodField()
Expand Down Expand Up @@ -129,6 +131,7 @@ class Meta:
"root_alert_group",
"status",
"declare_incident_link",
"team",
]

def get_render_for_web(self, obj):
Expand Down
4 changes: 4 additions & 0 deletions engine/apps/api/serializers/paging.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from rest_framework import serializers

from apps.alerts.models import AlertGroup
from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField
from common.api_helpers.utils import CurrentTeamDefault


class UserReferenceSerializer(serializers.Serializer):
Expand Down Expand Up @@ -49,6 +51,8 @@ class DirectPagingSerializer(serializers.Serializer):
title = serializers.CharField(required=False, default=None)
message = serializers.CharField(required=False, default=None)

team = TeamPrimaryKeyRelatedField(allow_null=True, default=CurrentTeamDefault())

def validate(self, attrs):
organization = self.context["organization"]

Expand Down
8 changes: 8 additions & 0 deletions engine/apps/api/serializers/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,12 @@ class Meta:
"name",
"email",
"avatar_url",
"is_sharing_resources_to_all",
iskhakov marked this conversation as resolved.
Show resolved Hide resolved
)

read_only_fields = (
"id",
"name",
"email",
"avatar_url",
)
21 changes: 17 additions & 4 deletions engine/apps/api/views/alert_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@
from apps.mobile_app.auth import MobileAppAuthTokenAuthentication
from apps.user_management.models import Team, User
from common.api_helpers.exceptions import BadRequest
from common.api_helpers.filters import DateRangeFilterMixin, ModelFieldFilterMixin
from common.api_helpers.filters import (
ByTeamModelFieldFilterMixin,
DateRangeFilterMixin,
ModelFieldFilterMixin,
TeamModelMultipleChoiceFilter,
)
from common.api_helpers.mixins import PreviewTemplateMixin, PublicPrimaryKeyMixin, TeamFilteringMixin
from common.api_helpers.paginators import TwentyFiveCursorPaginator

Expand All @@ -48,7 +53,7 @@ def get_user_queryset(request):
return User.objects.filter(organization=request.user.organization).distinct()


class AlertGroupFilter(DateRangeFilterMixin, ModelFieldFilterMixin, filters.FilterSet):
class AlertGroupFilter(DateRangeFilterMixin, ByTeamModelFieldFilterMixin, ModelFieldFilterMixin, filters.FilterSet):
"""
Examples of possible date formats here https://docs.djangoproject.com/en/1.9/ref/settings/#datetime-input-formats
"""
Expand Down Expand Up @@ -103,6 +108,7 @@ class AlertGroupFilter(DateRangeFilterMixin, ModelFieldFilterMixin, filters.Filt
)
with_resolution_note = filters.BooleanFilter(method="filter_with_resolution_note")
mine = filters.BooleanFilter(method="filter_mine")
team = TeamModelMultipleChoiceFilter(field_name="channel__team")

class Meta:
model = AlertGroup
Expand Down Expand Up @@ -280,11 +286,12 @@ def get_queryset(self):
alert_receive_channels_ids = list(
AlertReceiveChannel.objects.filter(
organization_id=self.request.auth.organization.id,
team_id=self.request.user.current_team,
).values_list("id", flat=True)
)

queryset = AlertGroup.unarchived_objects.filter(
channel__in=alert_receive_channels_ids,
*self.available_teams_lookup_args,
).only("id")

return queryset
Expand Down Expand Up @@ -538,6 +545,12 @@ def filters(self, request):
)

filter_options = [
{
iskhakov marked this conversation as resolved.
Show resolved Hide resolved
"name": "team",
"type": "team_select",
"href": api_root + "teams/",
"global": True,
},
{"name": "search", "type": "search"},
{"name": "integration", "type": "options", "href": api_root + "alert_receive_channels/?filters=true"},
{"name": "escalation_chain", "type": "options", "href": api_root + "escalation_chains/?filters=true"},
Expand Down Expand Up @@ -572,7 +585,7 @@ def filters(self, request):
"name": "status",
"type": "options",
"options": [
{"display_name": "new", "value": AlertGroup.NEW},
{"display_name": "firing", "value": AlertGroup.NEW},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could this be done in a separate PR? Are you sure to have updated all other references to "new" to "firing"? If we make this change I think it'd also make sense to refactor AlertGroup.NEW -> AlertGroup.FIRING

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made alert groups page look consistent, also it is called "firing" everywhere in the docs, we can finish that renaming in the separate PR

Screenshot 2023-03-20 at 12 40 46

{"display_name": "acknowledged", "value": AlertGroup.ACKNOWLEDGED},
{"display_name": "resolved", "value": AlertGroup.RESOLVED},
{"display_name": "silenced", "value": AlertGroup.SILENCED},
Expand Down
28 changes: 25 additions & 3 deletions engine/apps/api/views/alert_receive_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from apps.api.throttlers import DemoAlertThrottler
from apps.auth_token.auth import PluginAuthentication
from common.api_helpers.exceptions import BadRequest
from common.api_helpers.filters import ByTeamModelFieldFilterMixin, TeamModelMultipleChoiceFilter
from common.api_helpers.mixins import (
FilterSerializerMixin,
PreviewTemplateMixin,
Expand All @@ -29,11 +30,12 @@
from common.insight_log import EntityEvent, write_resource_insight_log


class AlertReceiveChannelFilter(filters.FilterSet):
class AlertReceiveChannelFilter(ByTeamModelFieldFilterMixin, filters.FilterSet):
maintenance_mode = filters.MultipleChoiceFilter(
choices=AlertReceiveChannel.MAINTENANCE_MODE_CHOICES, method="filter_maintenance_mode"
)
integration = filters.ChoiceFilter(choices=AlertReceiveChannel.INTEGRATION_CHOICES)
team = TeamModelMultipleChoiceFilter()

class Meta:
model = AlertReceiveChannel
Expand Down Expand Up @@ -92,6 +94,7 @@ class AlertReceiveChannelView(
"partial_update": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
"destroy": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
"change_team": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
"filters": [RBACPermission.Permissions.INTEGRATIONS_READ],
}

def create(self, request, *args, **kwargs):
Expand Down Expand Up @@ -127,12 +130,12 @@ def get_queryset(self, eager=True):
if is_filters_request:
queryset = AlertReceiveChannel.objects_with_maintenance.filter(
organization=organization,
team=self.request.user.current_team,
*self.available_teams_lookup_args,
)
else:
queryset = AlertReceiveChannel.objects.filter(
organization=organization,
team=self.request.user.current_team,
*self.available_teams_lookup_args,
)
if eager:
queryset = self.serializer_class.setup_eager_loading(queryset)
Expand Down Expand Up @@ -216,3 +219,22 @@ def get_alert_to_template(self):
return self.get_object().alert_groups.last().alerts.first()
except AttributeError:
return None

@action(methods=["get"], detail=False)
def filters(self, request):
filter_name = request.query_params.get("search", None)
api_root = "/api/internal/v1/"

filter_options = [
{
"name": "team",
"type": "team_select",
"href": api_root + "teams/",
"global": True,
},
]

if filter_name is not None:
filter_options = list(filter(lambda f: filter_name in f["name"], filter_options))

return Response(filter_options)
5 changes: 3 additions & 2 deletions engine/apps/api/views/alert_receive_channel_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
from apps.api.permissions import RBACPermission
from apps.api.serializers.alert_receive_channel import AlertReceiveChannelTemplatesSerializer
from apps.auth_token.auth import PluginAuthentication
from common.api_helpers.mixins import PublicPrimaryKeyMixin
from common.api_helpers.mixins import PublicPrimaryKeyMixin, TeamFilteringMixin
from common.insight_log import EntityEvent, write_resource_insight_log
from common.jinja_templater.apply_jinja_template import JinjaTemplateError


class AlertReceiveChannelTemplateView(
TeamFilteringMixin,
PublicPrimaryKeyMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
Expand All @@ -34,7 +35,7 @@ class AlertReceiveChannelTemplateView(
def get_queryset(self):
queryset = AlertReceiveChannel.objects.filter(
organization=self.request.auth.organization,
team=self.request.user.current_team,
*self.available_teams_lookup_args,
)
return queryset

Expand Down
17 changes: 13 additions & 4 deletions engine/apps/api/views/channel_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,19 @@
from apps.auth_token.auth import PluginAuthentication
from apps.slack.models import SlackChannel
from common.api_helpers.exceptions import BadRequest
from common.api_helpers.mixins import CreateSerializerMixin, PublicPrimaryKeyMixin, UpdateSerializerMixin
from common.api_helpers.mixins import (
CreateSerializerMixin,
PublicPrimaryKeyMixin,
TeamFilteringMixin,
UpdateSerializerMixin,
)
from common.exceptions import UnableToSendDemoAlert
from common.insight_log import EntityEvent, write_resource_insight_log


class ChannelFilterView(PublicPrimaryKeyMixin, CreateSerializerMixin, UpdateSerializerMixin, ModelViewSet):
class ChannelFilterView(
TeamFilteringMixin, PublicPrimaryKeyMixin, CreateSerializerMixin, UpdateSerializerMixin, ModelViewSet
):
authentication_classes = (PluginAuthentication,)
permission_classes = (IsAuthenticated, RBACPermission)
rbac_permissions = {
Expand All @@ -41,6 +48,8 @@ class ChannelFilterView(PublicPrimaryKeyMixin, CreateSerializerMixin, UpdateSeri
update_serializer_class = ChannelFilterUpdateSerializer
create_serializer_class = ChannelFilterCreateSerializer

TEAM_LOOKUP = "alert_receive_channel__team"

def get_queryset(self):
alert_receive_channel_id = self.request.query_params.get("alert_receive_channel", None)
lookup_kwargs = {}
Expand All @@ -53,10 +62,10 @@ def get_queryset(self):
).order_by("pk")

queryset = ChannelFilter.objects.filter(
**lookup_kwargs,
alert_receive_channel__organization=self.request.auth.organization,
alert_receive_channel__team=self.request.user.current_team,
alert_receive_channel__deleted_at=None,
*self.available_teams_lookup_args, # TODO: double check this
Copy link
Member

@vstpme vstpme Mar 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: There are multiple similar TODOs in the PR, is this intended?

**lookup_kwargs,
).annotate(
slack_channel_name=Subquery(slack_channels_subq.values("name")[:1]),
slack_channel_pk=Subquery(slack_channels_subq.values("public_primary_key")[:1]),
Expand Down
Loading