Skip to content

Commit bb9c99d

Browse files
authored
feat(snooze): request longer snooze (#5862)
1 parent 169baf4 commit bb9c99d

File tree

7 files changed

+568
-2
lines changed

7 files changed

+568
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""adds snooze extension oncall service to project
2+
3+
Revision ID: bccbf255d6d1
4+
Revises: 92a359040b8e
5+
Create Date: 2025-03-28 13:12:07.514337
6+
7+
"""
8+
9+
from alembic import op
10+
import sqlalchemy as sa
11+
12+
13+
# revision identifiers, used by Alembic.
14+
revision = "bccbf255d6d1"
15+
down_revision = "92a359040b8e"
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
# ### commands auto generated by Alembic - please adjust! ###
22+
op.add_column(
23+
"project", sa.Column("snooze_extension_oncall_service_id", sa.Integer(), nullable=True)
24+
)
25+
op.create_foreign_key(
26+
None, "project", "service", ["snooze_extension_oncall_service_id"], ["id"]
27+
)
28+
# ### end Alembic commands ###
29+
30+
31+
def downgrade():
32+
# ### commands auto generated by Alembic - please adjust! ###
33+
op.drop_constraint(None, "project", type_="foreignkey")
34+
op.drop_column("project", "snooze_extension_oncall_service_id")
35+
# ### end Alembic commands ###

src/dispatch/plugins/dispatch_slack/case/interactive.py

+41-2
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
case_type_select,
7575
description_input,
7676
entity_select,
77+
extension_request_checkbox,
7778
incident_priority_select,
7879
incident_type_select,
7980
project_select,
@@ -106,6 +107,7 @@
106107
from dispatch.project import service as project_service
107108
from dispatch.search.utils import create_filter_expression
108109
from dispatch.service import flows as service_flows
110+
from dispatch.service.models import Service
109111
from dispatch.signal import service as signal_service
110112
from dispatch.signal.enums import SignalEngagementStatus
111113
from dispatch.signal.models import (
@@ -733,6 +735,7 @@ def snooze_button_click(
733735
title_input(placeholder="A name for your snooze filter."),
734736
description_input(placeholder="Provide a description for your snooze filter."),
735737
relative_date_picker_input(label="Expiration"),
738+
extension_request_checkbox(),
736739
]
737740

738741
# not all signals will have entities and slack doesn't like empty selects
@@ -903,11 +906,16 @@ def handle_snooze_submission_event(
903906
)
904907
mfa_enabled = True if mfa_plugin else False
905908

909+
form_data: FormData = context["subject"].form_data
910+
extension_request_data = form_data.get(DefaultBlockIds.extension_request_checkbox)
911+
extension_request_value = extension_request_data[0].value if extension_request_data else None
912+
extension_requested = True if extension_request_value == "Yes" else False
913+
906914
def _create_snooze_filter(
907915
db_session: Session,
908916
subject: SubjectMetadata,
909917
user: DispatchUser,
910-
) -> None:
918+
) -> SignalFilter:
911919
form_data: FormData = subject.form_data
912920
# Get the existing filters for the signal
913921
signal = signal_service.get(db_session=db_session, signal_id=subject.id)
@@ -1005,6 +1013,8 @@ def _create_snooze_filter(
10051013
signal=signal,
10061014
new_filter=new_filter,
10071015
thread_ts=thread_id,
1016+
extension_requested=extension_requested,
1017+
oncall_service=new_filter.project.snooze_extension_oncall_service,
10081018
)
10091019
send_success_modal(
10101020
client=client,
@@ -1044,6 +1054,8 @@ def _create_snooze_filter(
10441054
signal=signal,
10451055
new_filter=new_filter,
10461056
thread_ts=thread_id,
1057+
extension_requested=extension_requested,
1058+
oncall_service=new_filter.project.snooze_extension_oncall_service,
10471059
)
10481060
send_success_modal(
10491061
client=client,
@@ -1073,6 +1085,24 @@ def _create_snooze_filter(
10731085
)
10741086

10751087

1088+
def get_user_id_from_oncall_service(
1089+
client: WebClient,
1090+
db_session: Session,
1091+
oncall_service: Service | None,
1092+
) -> str | None:
1093+
if not oncall_service:
1094+
return None
1095+
1096+
oncall_email = service_flows.resolve_oncall(service=oncall_service, db_session=db_session)
1097+
if oncall_email:
1098+
# Get the Slack user ID for the current oncall
1099+
try:
1100+
return client.users_lookupByEmail(email=oncall_email)["user"]["id"]
1101+
except SlackApiError:
1102+
log.error(f"Failed to find Slack user for email: {oncall_email}")
1103+
return None
1104+
1105+
10761106
def post_snooze_message(
10771107
client: WebClient,
10781108
channel: str,
@@ -1081,6 +1111,8 @@ def post_snooze_message(
10811111
db_session: Session,
10821112
new_filter: SignalFilter,
10831113
thread_ts: str | None = None,
1114+
extension_requested: bool = False,
1115+
oncall_service: Service | None = None,
10841116
):
10851117
def extract_entity_ids(expression: list[dict]) -> list[int]:
10861118
entity_ids = []
@@ -1105,13 +1137,20 @@ def extract_entity_ids(expression: list[dict]) -> list[int]:
11051137

11061138
message = (
11071139
f":zzz: *New Signal Snooze Added*\n"
1108-
f"• User: {user.email}\n"
1140+
f"• Created by: {user.email}\n"
11091141
f"• Signal: {signal.name}\n"
11101142
f"• Snooze Name: {new_filter.name}\n"
11111143
f"• Description: {new_filter.description}\n"
11121144
f"• Expiration: {new_filter.expiration}\n"
11131145
f"• Entities: {entities_text}"
11141146
)
1147+
if extension_requested:
1148+
message += "\n• *Extension Requested*"
1149+
if user_id := get_user_id_from_oncall_service(
1150+
client=client, db_session=db_session, oncall_service=oncall_service
1151+
):
1152+
message += f" - notifying oncall: <@{user_id}>"
1153+
11151154
client.chat_postMessage(channel=channel, text=message, thread_ts=thread_ts)
11161155

11171156

src/dispatch/plugins/dispatch_slack/fields.py

+24
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import List
55

66
from blockkit import (
7+
Checkboxes,
78
DatePicker,
89
Input,
910
MultiExternalSelect,
@@ -65,6 +66,7 @@ class DefaultBlockIds(DispatchEnum):
6566

6667
# signals
6768
signal_definition_select = "signal-definition-select"
69+
extension_request_checkbox = "extension_request_checkbox"
6870

6971
# tags
7072
tags_multi_select = "tag-multi-select"
@@ -102,6 +104,7 @@ class DefaultActionIds(DispatchEnum):
102104

103105
# signals
104106
signal_definition_select = "signal-definition-select"
107+
extension_request_checkbox = "extension_request_checkbox"
105108

106109
# tags
107110
tags_multi_select = "tag-multi-select"
@@ -740,3 +743,24 @@ def signal_definition_select(
740743
label=label,
741744
**kwargs,
742745
)
746+
747+
748+
def extension_request_checkbox(
749+
action_id: str = DefaultActionIds.extension_request_checkbox,
750+
block_id: str = DefaultBlockIds.extension_request_checkbox,
751+
label: str = "Request longer expiration",
752+
**kwargs,
753+
):
754+
options = [
755+
PlainOption(
756+
text=("Check this box to request an expiration longer than 2 weeks."),
757+
value="Yes",
758+
)
759+
]
760+
return Input(
761+
block_id=block_id,
762+
element=Checkboxes(options=options, action_id=action_id),
763+
label=label,
764+
optional=True,
765+
**kwargs,
766+
)

src/dispatch/project/models.py

+18
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ class Project(Base):
7272
report_incident_title_hint = Column(String, nullable=True)
7373
report_incident_description_hint = Column(String, nullable=True)
7474

75+
snooze_extension_oncall_service_id = Column(Integer, nullable=True)
76+
snooze_extension_oncall_service = relationship(
77+
"Service",
78+
foreign_keys=[snooze_extension_oncall_service_id],
79+
primaryjoin="Service.id == Project.snooze_extension_oncall_service_id",
80+
)
81+
7582
@hybrid_property
7683
def slug(self):
7784
return slugify(self.name)
@@ -81,6 +88,15 @@ def slug(self):
8188
)
8289

8390

91+
class Service(DispatchBase):
92+
id: PrimaryKey
93+
description: Optional[str] = Field(None, nullable=True)
94+
external_id: str
95+
is_active: Optional[bool] = None
96+
name: NameStr
97+
type: Optional[str] = Field(None, nullable=True)
98+
99+
84100
class ProjectBase(DispatchBase):
85101
id: Optional[PrimaryKey]
86102
name: NameStr
@@ -105,6 +121,7 @@ class ProjectBase(DispatchBase):
105121
report_incident_instructions: Optional[str] = Field(None, nullable=True)
106122
report_incident_title_hint: Optional[str] = Field(None, nullable=True)
107123
report_incident_description_hint: Optional[str] = Field(None, nullable=True)
124+
snooze_extension_oncall_service: Optional[Service]
108125

109126

110127
class ProjectCreate(ProjectBase):
@@ -116,6 +133,7 @@ class ProjectUpdate(ProjectBase):
116133
send_weekly_reports: Optional[bool] = Field(False, nullable=True)
117134
weekly_report_notification_id: Optional[int] = Field(None, nullable=True)
118135
stable_priority_id: Optional[int]
136+
snooze_extension_oncall_service_id: Optional[int]
119137

120138

121139
class ProjectRead(ProjectBase):

src/dispatch/static/dispatch/src/project/NewEditSheet.vue

+12
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,15 @@
130130
name="Owner Conversation"
131131
/>
132132
</v-col>
133+
<v-col cols="12">
134+
<v-form @submit.prevent>
135+
<service-select
136+
:project="project"
137+
label="Oncall Service For Signal Snooze Extensions"
138+
v-model="snooze_extension_oncall_service"
139+
/>
140+
</v-form>
141+
</v-col>
133142
<span class="text-body-1 text-medium-emphasis">Alternative folder structure</span>
134143
<v-col cols="12">
135144
<v-text-field
@@ -217,6 +226,7 @@ import { mapActions } from "vuex"
217226
import { mapFields } from "vuex-map-fields"
218227
219228
import ColorPickerInput from "@/components/ColorPickerInput.vue"
229+
import ServiceSelect from "@/service/ServiceSelect.vue"
220230
221231
export default {
222232
setup() {
@@ -228,6 +238,7 @@ export default {
228238
229239
components: {
230240
ColorPickerInput,
241+
ServiceSelect,
231242
},
232243
233244
computed: {
@@ -253,6 +264,7 @@ export default {
253264
"selected.report_incident_instructions",
254265
"selected.report_incident_title_hint",
255266
"selected.report_incident_description_hint",
267+
"selected.snooze_extension_oncall_service",
256268
"dialogs.showCreateEdit",
257269
]),
258270
},

src/dispatch/static/dispatch/src/project/store.js

+6
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ const actions = {
8585
},
8686
save({ commit, dispatch }) {
8787
commit("SET_SELECTED_LOADING", true)
88+
if (state.selected.snooze_extension_oncall_service) {
89+
state.selected.snooze_extension_oncall_service_id =
90+
state.selected.snooze_extension_oncall_service.id
91+
} else {
92+
state.selected.snooze_extension_oncall_service_id = null
93+
}
8894
if (!state.selected.id) {
8995
return ProjectApi.create(state.selected)
9096
.then(() => {

0 commit comments

Comments
 (0)