Skip to content

Commit

Permalink
feat(slack): edit command to update the metadata of an incident (#96)
Browse files Browse the repository at this point in the history
* Add edit command to update an incident

* fix(incidents): save environment in IncidentUpdate

* fix(ui): display edits in the web UI timeline

* fix(slack): display edits in Slack update

---------

Co-authored-by: Gabriel Dugny <[email protected]>
  • Loading branch information
jennafauconnier and GabDug authored Sep 27, 2024
1 parent 7fbaea4 commit 2f3bfc6
Show file tree
Hide file tree
Showing 10 changed files with 301 additions and 47 deletions.
33 changes: 33 additions & 0 deletions src/firefighter/incidents/forms/edit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from __future__ import annotations

from django import forms

from firefighter.incidents.models import Environment


def initial_environments() -> Environment:
return Environment.objects.get(default=True)


class EditMetaForm(forms.Form):
title = forms.CharField(
label="Title",
max_length=128,
min_length=10,
widget=forms.TextInput(attrs={"placeholder": "What's going on?"}),
)
description = forms.CharField(
label="Summary",
widget=forms.Textarea(
attrs={
"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."
}
),
min_length=10,
max_length=1200,
)
environment = forms.ModelChoiceField(
label="Environment",
queryset=Environment.objects.all(),
initial=initial_environments,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 4.2.14 on 2024-09-27 10:06

from django.db import migrations, models

import firefighter.incidents.models.environment


class Migration(migrations.Migration):

dependencies = [
("incidents", "0003_delete_featureteam"),
]

operations = [
migrations.AddField(
model_name="incidentupdate",
name="environment",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=models.SET(
firefighter.incidents.models.environment.Environment.get_default
),
to="incidents.environment",
),
),
]
35 changes: 22 additions & 13 deletions src/firefighter/incidents/models/incident.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,21 +335,27 @@ def can_be_closed(self) -> tuple[bool, list[tuple[str, str]]]:
return True, []
if self.needs_postmortem:
if self.status.value != IncidentStatus.POST_MORTEM:
cant_closed_reasons.append((
"STATUS_NOT_POST_MORTEM",
f"Incident is not in PostMortem status, and needs one because of its priority and environment ({self.priority.name}/{self.environment.value}).",
))
cant_closed_reasons.append(
(
"STATUS_NOT_POST_MORTEM",
f"Incident is not in PostMortem status, and needs one because of its priority and environment ({self.priority.name}/{self.environment.value}).",
)
)
elif self.status.value < IncidentStatus.FIXED:
cant_closed_reasons.append((
"STATUS_NOT_MITIGATED",
f"Incident is not in {IncidentStatus.FIXED.label} status (currently {self.status.label}).",
))
cant_closed_reasons.append(
(
"STATUS_NOT_MITIGATED",
f"Incident is not in {IncidentStatus.FIXED.label} status (currently {self.status.label}).",
)
)
missing_milestones = self.missing_milestones()
if len(missing_milestones) > 0:
cant_closed_reasons.append((
"MISSING_REQUIRED_KEY_EVENTS",
f"Missing key events: {', '.join(missing_milestones)}",
))
cant_closed_reasons.append(
(
"MISSING_REQUIRED_KEY_EVENTS",
f"Missing key events: {', '.join(missing_milestones)}",
)
)

if len(cant_closed_reasons) > 0:
return False, cant_closed_reasons
Expand Down Expand Up @@ -534,7 +540,7 @@ def update_roles(

return incident_update

def create_incident_update(
def create_incident_update( # noqa: PLR0913
self: Incident,
message: str | None = None,
status: int | None = None,
Expand All @@ -544,6 +550,7 @@ def create_incident_update(
event_type: str | None = None,
title: str | None = None,
description: str | None = None,
environment_id: str | None = None,
event_ts: datetime | None = None,
) -> IncidentUpdate:
updated_fields: list[str] = []
Expand All @@ -560,6 +567,7 @@ def _update_incident_field(
_update_incident_field(self, "component_id", component_id, updated_fields)
_update_incident_field(self, "title", title, updated_fields)
_update_incident_field(self, "description", description, updated_fields)
_update_incident_field(self, "environment_id", environment_id, updated_fields)

old_priority = self.priority if priority_id is not None else None

Expand All @@ -573,6 +581,7 @@ def _update_incident_field(
incident=self,
status=status, # type: ignore
priority_id=priority_id,
environment_id=environment_id,
component_id=component_id,
message=message,
created_by=created_by,
Expand Down
7 changes: 7 additions & 0 deletions src/firefighter/incidents/models/incident_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from firefighter.incidents.enums import IncidentStatus
from firefighter.incidents.models.component import Component
from firefighter.incidents.models.environment import Environment
from firefighter.incidents.models.priority import Priority
from firefighter.incidents.models.severity import Severity
from firefighter.incidents.models.user import User
Expand Down Expand Up @@ -65,6 +66,12 @@ class IncidentUpdate(models.Model):
priority = models.ForeignKey[Priority | None, Priority | None](
Priority, null=True, blank=True, on_delete=models.SET(Priority.get_default)
)
environment = models.ForeignKey[Environment | None, Environment | None](
Environment,
null=True,
blank=True,
on_delete=models.SET(Environment.get_default),
)
incident = models.ForeignKey["Incident", "Incident"](
"Incident", on_delete=models.CASCADE
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,26 @@
</div>
</div>
<div class="text-neutral-600 dark:text-neutral-100">
{% if incident_update.title %}
<p class="mt-0.5">
Title update
</p>
<p class="pl-2 mt-0.5 text-sm text-neutral-500 dark:text-neutral-300">
Title changed to: {{ incident_update.title }}
</p>
{% endif %}
{% if incident_update.description or incident_update.description|length > 0 %}
<p class="mt-0.5">
Description update
</p>
<div
class="pb-2"
>
<div class="text-sm flex flex-col border-b border-neutral-200 py-1 p-4 text-left sm:border-0 sm:border-l-4 break-words">
{{ incident_update.description | urlize | linebreaksbr}}
</div>
</div>
{% endif %}
{% if incident_update.event_type or incident_update.event_type|length > 0 %}
<p class="mt-0.5">
Key event: {{ incident_update.event_type|title }}
Expand All @@ -65,6 +85,17 @@
{% include "./status_pill.html" with status=incident_update.status IncidentStatus=IncidentStatus only %}
</p>
{% endif %}
{% if incident_update.environment %}
<p class="mt-0.5">
Environment update
</p>
{% endif %}
{% if incident_update.environment or incident_update.environment|length > 0 %}
<p class="pl-2 mt-0.5 text-sm text-neutral-500 dark:text-neutral-300">
Environment changed to:
{% include "./environment_pill.html" with environment=incident_update.environment only %}
</p>
{% endif %}
{% if incident_update.severity %}
<p class="mt-0.5">
Severity update
Expand All @@ -84,8 +115,6 @@
<p class="mt-0.5">
Component update
</p>
{% endif %}
{% if incident_update.component %}
<p class="pl-2 mt-0.5 text-sm text-neutral-500 dark:text-neutral-300">
Component impacted changed to:
{{ incident_update.component}}
Expand Down
86 changes: 54 additions & 32 deletions src/firefighter/slack/messages/slack_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,18 +359,20 @@ def get_blocks(self) -> list[Block]:
if len(fields) == 0:
fields.append("_No changes detected._")

blocks.extend([
DividerBlock(),
SectionBlock(
block_id="message_role_update",
fields=fields,
accessory=ButtonElement(
text="Update",
value=str(self.incident.id),
action_id=UpdateRolesModal.open_action,
blocks.extend(
[
DividerBlock(),
SectionBlock(
block_id="message_role_update",
fields=fields,
accessory=ButtonElement(
text="Update",
value=str(self.incident.id),
action_id=UpdateRolesModal.open_action,
),
),
),
])
]
)
if not self.first_update:
blocks.append(
ContextBlock(
Expand Down Expand Up @@ -457,6 +459,16 @@ def get_blocks(self) -> list[Block]:
blocks.append(
slack_block_quote(self.incident_update.message),
)
if self.incident_update.title and self.incident_update.title != "":
blocks.append(
SectionBlock(
text=f"New title: *{shorten(self.incident_update.title, 2985)}*"
),
)
if self.incident_update.description and self.incident_update.description != "":
blocks.append(
slack_block_quote(self.incident_update.description),
)
fields = []
if self.in_channel:
if self.incident_update.status:
Expand All @@ -477,24 +489,32 @@ def get_blocks(self) -> list[Block]:
text=f":package: *Component:* {self.incident.component.group.name} - {self.incident.component.name}"
)
)
if self.incident_update.environment:
fields.append(
MarkdownTextObject(
text=f":round_pushpin: *Environment:* {self.incident_update.environment.value}"
)
)

if len(fields) > 0:
blocks.extend([
DividerBlock(),
SectionBlock(
block_id="message_status_update",
fields=fields,
accessory=(
ButtonElement(
text="Update",
value=str(self.incident.id),
action_id=UpdateStatusModal.open_action,
)
if self.in_channel
else None
blocks.extend(
[
DividerBlock(),
SectionBlock(
block_id="message_status_update",
fields=fields,
accessory=(
ButtonElement(
text="Update",
value=str(self.incident.id),
action_id=UpdateStatusModal.open_action,
)
if self.in_channel
else None
),
),
),
])
]
)

if self.incident_update.created_by:
blocks.append(
Expand Down Expand Up @@ -683,13 +703,15 @@ def get_blocks(self) -> list[Block]:
]

if self.incident.status >= IncidentStatus.FIXED:
blocks.extend([
SectionBlock(
text=MarkdownTextObject(
text=f":white_check_mark: *UPDATE*: Incident #{self.incident.conversation.name} has been mitigated, you can resume your deployments."
blocks.extend(
[
SectionBlock(
text=MarkdownTextObject(
text=f":white_check_mark: *UPDATE*: Incident #{self.incident.conversation.name} has been mitigated, you can resume your deployments."
)
)
)
])
]
)
return blocks

def get_text(self) -> str:
Expand Down
3 changes: 3 additions & 0 deletions src/firefighter/slack/views/events/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from firefighter.slack.views.modals import (
modal_close,
modal_dowgrade_workflow,
modal_edit,
modal_open,
modal_postmortem,
modal_send_sos,
Expand Down Expand Up @@ -106,6 +107,8 @@ def manage_incident(ack: Ack, respond: Respond, body: dict[str, Any]) -> None:
modal_open.open_modal_aio(ack, body)
elif command == "update":
modal_update.open_modal_aio(ack, body)
elif command == "edit":
modal_edit.open_modal_aio(ack, body)
elif command == "close":
modal_close.open_modal_aio(ack=ack, body=body)
elif command == "status":
Expand Down
2 changes: 2 additions & 0 deletions src/firefighter/slack/views/modals/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
DowngradeWorkflowModal,
modal_dowgrade_workflow,
)
from firefighter.slack.views.modals.edit import EditMetaModal, modal_edit
from firefighter.slack.views.modals.key_event_message import ( # XXX(dugab) move and rename (not a modal but a surface...)
KeyEvents,
)
Expand Down Expand Up @@ -39,6 +40,7 @@

selectable_modals: list[type[SlackModal]] = [
UpdateModal,
EditMetaModal,
UpdateRolesModal,
OnCallModal,
CloseModal,
Expand Down
Loading

0 comments on commit 2f3bfc6

Please sign in to comment.