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

feat(snooze): request longer snooze #5862

Merged
merged 3 commits into from
Mar 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""adds snooze extension oncall service to project

Revision ID: bccbf255d6d1
Revises: 92a359040b8e
Create Date: 2025-03-28 13:12:07.514337

"""

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "bccbf255d6d1"
down_revision = "92a359040b8e"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"project", sa.Column("snooze_extension_oncall_service_id", sa.Integer(), nullable=True)
)
op.create_foreign_key(
None, "project", "service", ["snooze_extension_oncall_service_id"], ["id"]
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, "project", type_="foreignkey")
op.drop_column("project", "snooze_extension_oncall_service_id")
# ### end Alembic commands ###
43 changes: 41 additions & 2 deletions src/dispatch/plugins/dispatch_slack/case/interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
case_type_select,
description_input,
entity_select,
extension_request_checkbox,
incident_priority_select,
incident_type_select,
project_select,
Expand Down Expand Up @@ -106,6 +107,7 @@
from dispatch.project import service as project_service
from dispatch.search.utils import create_filter_expression
from dispatch.service import flows as service_flows
from dispatch.service.models import Service
from dispatch.signal import service as signal_service
from dispatch.signal.enums import SignalEngagementStatus
from dispatch.signal.models import (
Expand Down Expand Up @@ -733,6 +735,7 @@ def snooze_button_click(
title_input(placeholder="A name for your snooze filter."),
description_input(placeholder="Provide a description for your snooze filter."),
relative_date_picker_input(label="Expiration"),
extension_request_checkbox(),
]

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

form_data: FormData = context["subject"].form_data
extension_request_data = form_data.get(DefaultBlockIds.extension_request_checkbox)
extension_request_value = extension_request_data[0].value if extension_request_data else None
extension_requested = True if extension_request_value == "Yes" else False

def _create_snooze_filter(
db_session: Session,
subject: SubjectMetadata,
user: DispatchUser,
) -> None:
) -> SignalFilter:
form_data: FormData = subject.form_data
# Get the existing filters for the signal
signal = signal_service.get(db_session=db_session, signal_id=subject.id)
Expand Down Expand Up @@ -1005,6 +1013,8 @@ def _create_snooze_filter(
signal=signal,
new_filter=new_filter,
thread_ts=thread_id,
extension_requested=extension_requested,
oncall_service=new_filter.project.snooze_extension_oncall_service,
)
send_success_modal(
client=client,
Expand Down Expand Up @@ -1044,6 +1054,8 @@ def _create_snooze_filter(
signal=signal,
new_filter=new_filter,
thread_ts=thread_id,
extension_requested=extension_requested,
oncall_service=new_filter.project.snooze_extension_oncall_service,
)
send_success_modal(
client=client,
Expand Down Expand Up @@ -1073,6 +1085,24 @@ def _create_snooze_filter(
)


def get_user_id_from_oncall_service(
client: WebClient,
db_session: Session,
oncall_service: Service | None,
) -> str | None:
if not oncall_service:
return None

oncall_email = service_flows.resolve_oncall(service=oncall_service, db_session=db_session)
if oncall_email:
# Get the Slack user ID for the current oncall
try:
return client.users_lookupByEmail(email=oncall_email)["user"]["id"]
except SlackApiError:
log.error(f"Failed to find Slack user for email: {oncall_email}")
return None


def post_snooze_message(
client: WebClient,
channel: str,
Expand All @@ -1081,6 +1111,8 @@ def post_snooze_message(
db_session: Session,
new_filter: SignalFilter,
thread_ts: str | None = None,
extension_requested: bool = False,
oncall_service: Service | None = None,
):
def extract_entity_ids(expression: list[dict]) -> list[int]:
entity_ids = []
Expand All @@ -1105,13 +1137,20 @@ def extract_entity_ids(expression: list[dict]) -> list[int]:

message = (
f":zzz: *New Signal Snooze Added*\n"
f"• User: {user.email}\n"
f"• Created by: {user.email}\n"
f"• Signal: {signal.name}\n"
f"• Snooze Name: {new_filter.name}\n"
f"• Description: {new_filter.description}\n"
f"• Expiration: {new_filter.expiration}\n"
f"• Entities: {entities_text}"
)
if extension_requested:
message += "\n• *Extension Requested*"
if user_id := get_user_id_from_oncall_service(
client=client, db_session=db_session, oncall_service=oncall_service
):
message += f" - notifying oncall: <@{user_id}>"

client.chat_postMessage(channel=channel, text=message, thread_ts=thread_ts)


Expand Down
24 changes: 24 additions & 0 deletions src/dispatch/plugins/dispatch_slack/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import List

from blockkit import (
Checkboxes,
DatePicker,
Input,
MultiExternalSelect,
Expand Down Expand Up @@ -65,6 +66,7 @@ class DefaultBlockIds(DispatchEnum):

# signals
signal_definition_select = "signal-definition-select"
extension_request_checkbox = "extension_request_checkbox"

# tags
tags_multi_select = "tag-multi-select"
Expand Down Expand Up @@ -102,6 +104,7 @@ class DefaultActionIds(DispatchEnum):

# signals
signal_definition_select = "signal-definition-select"
extension_request_checkbox = "extension_request_checkbox"

# tags
tags_multi_select = "tag-multi-select"
Expand Down Expand Up @@ -740,3 +743,24 @@ def signal_definition_select(
label=label,
**kwargs,
)


def extension_request_checkbox(
action_id: str = DefaultActionIds.extension_request_checkbox,
block_id: str = DefaultBlockIds.extension_request_checkbox,
label: str = "Request longer expiration",
**kwargs,
):
options = [
PlainOption(
text=("Check this box to request an expiration longer than 2 weeks."),
value="Yes",
)
]
return Input(
block_id=block_id,
element=Checkboxes(options=options, action_id=action_id),
label=label,
optional=True,
**kwargs,
)
18 changes: 18 additions & 0 deletions src/dispatch/project/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ class Project(Base):
report_incident_title_hint = Column(String, nullable=True)
report_incident_description_hint = Column(String, nullable=True)

snooze_extension_oncall_service_id = Column(Integer, nullable=True)
snooze_extension_oncall_service = relationship(
"Service",
foreign_keys=[snooze_extension_oncall_service_id],
primaryjoin="Service.id == Project.snooze_extension_oncall_service_id",
)

@hybrid_property
def slug(self):
return slugify(self.name)
Expand All @@ -81,6 +88,15 @@ def slug(self):
)


class Service(DispatchBase):
id: PrimaryKey
description: Optional[str] = Field(None, nullable=True)
external_id: str
is_active: Optional[bool] = None
name: NameStr
type: Optional[str] = Field(None, nullable=True)


class ProjectBase(DispatchBase):
id: Optional[PrimaryKey]
name: NameStr
Expand All @@ -105,6 +121,7 @@ class ProjectBase(DispatchBase):
report_incident_instructions: Optional[str] = Field(None, nullable=True)
report_incident_title_hint: Optional[str] = Field(None, nullable=True)
report_incident_description_hint: Optional[str] = Field(None, nullable=True)
snooze_extension_oncall_service: Optional[Service]


class ProjectCreate(ProjectBase):
Expand All @@ -116,6 +133,7 @@ class ProjectUpdate(ProjectBase):
send_weekly_reports: Optional[bool] = Field(False, nullable=True)
weekly_report_notification_id: Optional[int] = Field(None, nullable=True)
stable_priority_id: Optional[int]
snooze_extension_oncall_service_id: Optional[int]


class ProjectRead(ProjectBase):
Expand Down
12 changes: 12 additions & 0 deletions src/dispatch/static/dispatch/src/project/NewEditSheet.vue
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,15 @@
name="Owner Conversation"
/>
</v-col>
<v-col cols="12">
<v-form @submit.prevent>
<service-select
:project="project"
label="Oncall Service For Signal Snooze Extensions"
v-model="snooze_extension_oncall_service"
/>
</v-form>
</v-col>
<span class="text-body-1 text-medium-emphasis">Alternative folder structure</span>
<v-col cols="12">
<v-text-field
Expand Down Expand Up @@ -217,6 +226,7 @@ import { mapActions } from "vuex"
import { mapFields } from "vuex-map-fields"

import ColorPickerInput from "@/components/ColorPickerInput.vue"
import ServiceSelect from "@/service/ServiceSelect.vue"

export default {
setup() {
Expand All @@ -228,6 +238,7 @@ export default {

components: {
ColorPickerInput,
ServiceSelect,
},

computed: {
Expand All @@ -253,6 +264,7 @@ export default {
"selected.report_incident_instructions",
"selected.report_incident_title_hint",
"selected.report_incident_description_hint",
"selected.snooze_extension_oncall_service",
"dialogs.showCreateEdit",
]),
},
Expand Down
6 changes: 6 additions & 0 deletions src/dispatch/static/dispatch/src/project/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ const actions = {
},
save({ commit, dispatch }) {
commit("SET_SELECTED_LOADING", true)
if (state.selected.snooze_extension_oncall_service) {
state.selected.snooze_extension_oncall_service_id =
state.selected.snooze_extension_oncall_service.id
} else {
state.selected.snooze_extension_oncall_service_id = null
}
if (!state.selected.id) {
return ProjectApi.create(state.selected)
.then(() => {
Expand Down
Loading
Loading