From c78d24f1bced9b7d9591cad222278862236e8509 Mon Sep 17 00:00:00 2001 From: Jenna Diop Date: Fri, 8 Nov 2024 11:08:27 +0100 Subject: [PATCH 1/3] Add /critical shortcut to create new incident more efficiently --- .../incidents/forms/create_incident.py | 8 +- .../management/commands/generate_manifest.py | 2 +- .../slack/views/events/commands.py | 3 + .../slack/views/modals/__init__.py | 2 + .../slack/views/modals/critical.py | 120 ++++++++++++++++++ 5 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 src/firefighter/slack/views/modals/critical.py diff --git a/src/firefighter/incidents/forms/create_incident.py b/src/firefighter/incidents/forms/create_incident.py index 53c0a73b..46ff166f 100644 --- a/src/firefighter/incidents/forms/create_incident.py +++ b/src/firefighter/incidents/forms/create_incident.py @@ -78,14 +78,16 @@ class CreateIncidentForm(CreateIncidentFormBase): def trigger_incident_workflow( self, creator: User, - impacts_data: dict[str, ImpactLevel], + impacts_data: dict[str, ImpactLevel] | None, *args: Any, **kwargs: Any, ) -> None: incident = Incident.objects.declare(created_by=creator, **self.cleaned_data) - impacts_form = SelectImpactForm(impacts_data) - impacts_form.save(incident=incident) + if impacts_data is not None: + impacts_form = SelectImpactForm(impacts_data) + impacts_form.save(incident=incident) + create_incident_conversation.send( "create_incident_form", incident=incident, diff --git a/src/firefighter/slack/management/commands/generate_manifest.py b/src/firefighter/slack/management/commands/generate_manifest.py index a3a66d3e..2bb2e226 100644 --- a/src/firefighter/slack/management/commands/generate_manifest.py +++ b/src/firefighter/slack/management/commands/generate_manifest.py @@ -118,7 +118,7 @@ def get_manifest( "command": command, "url": f"{public_base_url}/api/v2/firefighter/slack/incident/", "description": "Manage Incidents 🚨", - "usage_hint": "[open|update|close|status|help]", + "usage_hint": "[open|critical|update|close|status|help]", "should_escape": False, } for command in [main_command, *command_aliases] diff --git a/src/firefighter/slack/views/events/commands.py b/src/firefighter/slack/views/events/commands.py index ec98eb33..9d4e7590 100644 --- a/src/firefighter/slack/views/events/commands.py +++ b/src/firefighter/slack/views/events/commands.py @@ -15,6 +15,7 @@ ) from firefighter.slack.views.modals import ( modal_close, + modal_critical, modal_dowgrade_workflow, modal_edit, modal_open, @@ -111,6 +112,8 @@ def manage_incident(ack: Ack, respond: Respond, body: dict[str, Any]) -> None: modal_edit.open_modal_aio(ack, body) elif command == "close": modal_close.open_modal_aio(ack=ack, body=body) + elif command == "critical": + modal_critical.open_modal_aio(ack=ack, body=body) elif command == "status": modal_status.open_modal_aio(ack=ack, body=body) elif command in {"oncall", "on-call"}: diff --git a/src/firefighter/slack/views/modals/__init__.py b/src/firefighter/slack/views/modals/__init__.py index 8eb35b58..fa11b9ac 100644 --- a/src/firefighter/slack/views/modals/__init__.py +++ b/src/firefighter/slack/views/modals/__init__.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING from firefighter.slack.views.modals.close import CloseModal, modal_close +from firefighter.slack.views.modals.critical import CriticalModal, modal_critical from firefighter.slack.views.modals.downgrade_workflow import ( DowngradeWorkflowModal, modal_dowgrade_workflow, @@ -48,4 +49,5 @@ StatusModal, SendSosModal, DowngradeWorkflowModal, + CriticalModal ] diff --git a/src/firefighter/slack/views/modals/critical.py b/src/firefighter/slack/views/modals/critical.py new file mode 100644 index 00000000..7f5c4cd8 --- /dev/null +++ b/src/firefighter/slack/views/modals/critical.py @@ -0,0 +1,120 @@ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Any + +from django.conf import settings +from slack_sdk.models.blocks.blocks import SectionBlock +from slack_sdk.models.views import View + +from firefighter.incidents.forms.create_incident import CreateIncidentForm +from firefighter.slack.slack_app import SlackApp +from firefighter.slack.views.modals.base_modal.base import ModalForm + +if TYPE_CHECKING: + from slack_bolt.context.ack.ack import Ack + + from firefighter.incidents.models.incident import Priority, Severity + from firefighter.incidents.models.user import User + from firefighter.slack.views.modals.base_modal.form_utils import ( + SlackFormAttributesDict, + ) + +app = SlackApp() +logger = logging.getLogger(__name__) + + +def priority_label(obj: Severity | Priority) -> str: + return f"{obj.emoji} {obj.name} - {obj.description}" + + +class CriticalFormSlack(CreateIncidentForm): + slack_fields: SlackFormAttributesDict = { + "title": { + "input": { + "multiline": False, + "placeholder": "Short, punchy description of what's happening.", + }, + "block": {"hint": None}, + }, + "description": { + "input": { + "multiline": True, + "placeholder": "Help people responding to the incident. This will be posted to #tech-incidents and on our internal status page.\nThis description can be edited later.", + }, + "block": {"hint": None}, + }, + "component": { + "input": { + "placeholder": "Select affected component", + } + }, + "priority": { + "input": { + "placeholder": "Select a priority", + }, + "widget": { + "post_block": ( + SectionBlock( + text=f"_<{settings.SLACK_SEVERITY_HELP_GUIDE_URL}|How to choose the priority?>_" + ) + if settings.SLACK_SEVERITY_HELP_GUIDE_URL + else None + ), + "label_from_instance": priority_label, + }, + }, + } + + +class CriticalModal(ModalForm[CriticalFormSlack]): + open_action: str = "open_modal_incident_critical" + open_shortcut = "modal_critical" + callback_id: str = "incident_critical" + + form_class = CriticalFormSlack + + def build_modal_fn(self, **kwargs: Any) -> View: + form_instance = self.get_form_class()() + blocks = form_instance.slack_blocks() + + return View( + type="modal", + title="Open a critical incident"[:24], + submit="Create the incident"[:24], + callback_id=self.callback_id, + blocks=blocks, + clear_on_close=False, + close=None, + ) + + def handle_modal_fn( # type: ignore + self, ack: Ack, body: dict[str, Any], user: User + ) -> None : + slack_form = self.handle_form_errors( + ack, body, forms_kwargs={}, + ) + + if slack_form is None: + return + + form = slack_form.form + + try: + if hasattr(form, "trigger_incident_workflow") and callable( + form.trigger_incident_workflow + ): + form.trigger_incident_workflow( + creator=user, + impacts_data=None, + ) + except: # noqa: E722 + logger.exception("Error triggering incident workflow") + # XXX warn the user via DM! + + if len(form.cleaned_data) == 0: + logger.warning("Form is empty, no data captured.") + return + + +modal_critical = CriticalModal() From 2f7389ae658d54fd2b2fca9a9b2befa738436dec Mon Sep 17 00:00:00 2001 From: Jenna Diop Date: Fri, 8 Nov 2024 11:48:34 +0100 Subject: [PATCH 2/3] verification check on user is already in an incident --- src/firefighter/slack/views/modals/critical.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/firefighter/slack/views/modals/critical.py b/src/firefighter/slack/views/modals/critical.py index 7f5c4cd8..66bf62f8 100644 --- a/src/firefighter/slack/views/modals/critical.py +++ b/src/firefighter/slack/views/modals/critical.py @@ -8,7 +8,9 @@ from slack_sdk.models.views import View from firefighter.incidents.forms.create_incident import CreateIncidentForm +from firefighter.incidents.models.incident_membership import IncidentMembership from firefighter.slack.slack_app import SlackApp +from firefighter.slack.utils import respond from firefighter.slack.views.modals.base_modal.base import ModalForm if TYPE_CHECKING: @@ -91,6 +93,11 @@ def build_modal_fn(self, **kwargs: Any) -> View: def handle_modal_fn( # type: ignore self, ack: Ack, body: dict[str, Any], user: User ) -> None : + if not self.is_user_member_of_incident(user): + ack() + respond(body, text=":x: You must be linked to an incident to use this command.") + return + slack_form = self.handle_form_errors( ack, body, forms_kwargs={}, ) @@ -116,5 +123,8 @@ def handle_modal_fn( # type: ignore logger.warning("Form is empty, no data captured.") return + def is_user_member_of_incident(self, user: User) -> bool: + return IncidentMembership.objects.filter(user=user).exists() + modal_critical = CriticalModal() From 2cb44cc0eb97be940e5aef5100248c30638a64cb Mon Sep 17 00:00:00 2001 From: Jenna Diop Date: Thu, 28 Nov 2024 10:47:04 +0100 Subject: [PATCH 3/3] Remove unefficient check and add post block warning to use critical command --- src/firefighter/slack/views/modals/critical.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/firefighter/slack/views/modals/critical.py b/src/firefighter/slack/views/modals/critical.py index 66bf62f8..40a707da 100644 --- a/src/firefighter/slack/views/modals/critical.py +++ b/src/firefighter/slack/views/modals/critical.py @@ -8,9 +8,7 @@ from slack_sdk.models.views import View from firefighter.incidents.forms.create_incident import CreateIncidentForm -from firefighter.incidents.models.incident_membership import IncidentMembership from firefighter.slack.slack_app import SlackApp -from firefighter.slack.utils import respond from firefighter.slack.views.modals.base_modal.base import ModalForm if TYPE_CHECKING: @@ -78,7 +76,12 @@ class CriticalModal(ModalForm[CriticalFormSlack]): def build_modal_fn(self, **kwargs: Any) -> View: form_instance = self.get_form_class()() - blocks = form_instance.slack_blocks() + blocks = [ + SectionBlock( + text="*Warning:* The use of `/critical` is reserved for experienced users." + ), + *form_instance.slack_blocks(), + ] return View( type="modal", @@ -93,10 +96,6 @@ def build_modal_fn(self, **kwargs: Any) -> View: def handle_modal_fn( # type: ignore self, ack: Ack, body: dict[str, Any], user: User ) -> None : - if not self.is_user_member_of_incident(user): - ack() - respond(body, text=":x: You must be linked to an incident to use this command.") - return slack_form = self.handle_form_errors( ack, body, forms_kwargs={}, @@ -123,8 +122,5 @@ def handle_modal_fn( # type: ignore logger.warning("Form is empty, no data captured.") return - def is_user_member_of_incident(self, user: User) -> bool: - return IncidentMembership.objects.filter(user=user).exists() - modal_critical = CriticalModal()