From b2cd9b5466be8826e19b0150b94f56998e37d43f Mon Sep 17 00:00:00 2001 From: David Whittaker <84562015+whitdog47@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:45:40 -0700 Subject: [PATCH] feat(ui/ux): adds a task edit dialog with create ticket action (#5260) --- .../versions/2024-10-09_b8c1a8a4d957.py | 35 +++++ src/dispatch/incident/models.py | 4 + src/dispatch/incident/type/models.py | 11 ++ src/dispatch/plugins/dispatch_core/plugin.py | 16 +++ src/dispatch/plugins/dispatch_jira/plugin.py | 75 +++++++++++ .../dispatch/src/incident/TaskEditDialog.vue | 127 ++++++++++++++++++ .../static/dispatch/src/incident/TasksTab.vue | 99 ++++++++++++-- .../static/dispatch/src/incident/store.js | 48 +++++++ .../src/incident/type/NewEditSheet.vue | 12 +- .../src/plugin/PluginInstanceCombobox.vue | 10 +- .../static/dispatch/src/task/NewEditSheet.vue | 10 +- .../static/dispatch/src/task/Table.vue | 2 +- src/dispatch/static/dispatch/src/task/api.js | 4 + src/dispatch/task/models.py | 3 + src/dispatch/task/service.py | 19 +++ src/dispatch/task/views.py | 17 +++ src/dispatch/ticket/flows.py | 79 ++++++++++- src/dispatch/ticket/models.py | 1 + 18 files changed, 541 insertions(+), 31 deletions(-) create mode 100644 src/dispatch/database/revisions/tenant/versions/2024-10-09_b8c1a8a4d957.py create mode 100644 src/dispatch/static/dispatch/src/incident/TaskEditDialog.vue diff --git a/src/dispatch/database/revisions/tenant/versions/2024-10-09_b8c1a8a4d957.py b/src/dispatch/database/revisions/tenant/versions/2024-10-09_b8c1a8a4d957.py new file mode 100644 index 000000000000..103bffd67bfd --- /dev/null +++ b/src/dispatch/database/revisions/tenant/versions/2024-10-09_b8c1a8a4d957.py @@ -0,0 +1,35 @@ +"""Adds tickets to tasks and ticket metadata to incident types + +Revision ID: b8c1a8a4d957 +Revises: b057c079c2d5 +Create Date: 2024-10-05 09:06:34.177407 + +""" + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "b8c1a8a4d957" +down_revision = "b057c079c2d5" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "incident_type", + sa.Column("task_plugin_metadata", sa.JSON(), nullable=True, server_default="[]"), + ) + op.add_column("ticket", sa.Column("task_id", sa.Integer(), nullable=True)) + op.create_foreign_key(None, "ticket", "task", ["task_id"], ["id"], ondelete="CASCADE") + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, "ticket", type_="foreignkey") + op.drop_column("ticket", "task_id") + op.drop_column("incident_type", "task_plugin_metadata") + # ### end Alembic commands ### diff --git a/src/dispatch/incident/models.py b/src/dispatch/incident/models.py index bfd0ed554f8b..5d98f383768e 100644 --- a/src/dispatch/incident/models.py +++ b/src/dispatch/incident/models.py @@ -253,7 +253,11 @@ class TaskRead(DispatchBase): created_at: Optional[datetime] description: Optional[str] = Field(None, nullable=True) status: TaskStatus = TaskStatus.open + owner: Optional[ParticipantRead] weblink: Optional[AnyHttpUrl] = Field(None, nullable=True) + resolve_by: Optional[datetime] + resolved_at: Optional[datetime] + ticket: Optional[TicketRead] = None class TaskReadMinimal(DispatchBase): diff --git a/src/dispatch/incident/type/models.py b/src/dispatch/incident/type/models.py index fb98ec79732b..6fde577647a7 100644 --- a/src/dispatch/incident/type/models.py +++ b/src/dispatch/incident/type/models.py @@ -31,6 +31,7 @@ class IncidentType(ProjectMixin, Base): default = Column(Boolean, default=False) visibility = Column(String, default=Visibility.open) plugin_metadata = Column(JSON, default=[]) + task_plugin_metadata = Column(JSON, default=[]) incident_template_document_id = Column(Integer, ForeignKey("document.id")) incident_template_document = relationship( @@ -80,6 +81,15 @@ def get_meta(self, slug): if m["slug"] == slug: return m + @hybrid_method + def get_task_meta(self, slug): + if not self.task_plugin_metadata: + return + + for m in self.task_plugin_metadata: + if m["slug"] == slug: + return m + listen(IncidentType.default, "set", ensure_unique_default_per_project) @@ -110,6 +120,7 @@ class IncidentTypeBase(DispatchBase): cost_model: Optional[CostModelRead] = None channel_description: Optional[str] = Field(None, nullable=True) description_service: Optional[ServiceRead] + task_plugin_metadata: List[PluginMetadata] = [] @validator("plugin_metadata", pre=True) def replace_none_with_empty_list(cls, value): diff --git a/src/dispatch/plugins/dispatch_core/plugin.py b/src/dispatch/plugins/dispatch_core/plugin.py index 073b65738a0f..84593ce87b65 100644 --- a/src/dispatch/plugins/dispatch_core/plugin.py +++ b/src/dispatch/plugins/dispatch_core/plugin.py @@ -263,6 +263,22 @@ def update_case_ticket( """Updates a Dispatch case ticket.""" return + def create_task_ticket( + self, + task_id: int, + title: str, + assignee_email: str, + reporter_email: str, + incident_ticket_key: str = None, + task_plugin_metadata: dict = None, + db_session=None, + ): + """Creates a Dispatch task ticket.""" + return { + "resource_id": "", + "weblink": "https://dispatch.example.com", + } + class DispatchDocumentResolverPlugin(DocumentResolverPlugin): title = "Dispatch Plugin - Document Resolver" diff --git a/src/dispatch/plugins/dispatch_jira/plugin.py b/src/dispatch/plugins/dispatch_jira/plugin.py index 4b6ed634a3c4..42cde3f13a0b 100644 --- a/src/dispatch/plugins/dispatch_jira/plugin.py +++ b/src/dispatch/plugins/dispatch_jira/plugin.py @@ -123,6 +123,17 @@ def process_plugin_metadata(plugin_metadata: dict): return project_id, issue_type_name +def create_dict_from_plugin_metadata(plugin_metadata: dict): + """Creates a dictionary from plugin metadata, excluding project_id and issue_type_name.""" + metadata_dict = {} + if plugin_metadata: + for key_value in plugin_metadata["metadata"]: + if key_value["key"] != "project_id" and key_value["key"] != "issue_type_name": + metadata_dict[key_value["key"]] = key_value["value"] + + return metadata_dict + + def create_client(configuration: JiraConfiguration) -> JIRA: """Creates a Jira client.""" return JIRA( @@ -392,6 +403,70 @@ def create_case_ticket( return create(self.configuration, client, issue_fields) + def create_task_ticket( + self, + task_id: int, + title: str, + assignee_email: str, + reporter_email: str, + incident_ticket_key: str = None, + task_plugin_metadata: dict = None, + db_session=None, + ): + """Creates a task Jira issue.""" + client = create_client(self.configuration) + + assignee = get_user_field(client, self.configuration, assignee_email) + reporter = get_user_field(client, self.configuration, reporter_email) + + project_id, issue_type_name = process_plugin_metadata(task_plugin_metadata) + other_fields = create_dict_from_plugin_metadata(task_plugin_metadata) + + if not project_id: + project_id = self.configuration.default_project_id + + project = {"id": project_id} + if not project_id.isdigit(): + project = {"key": project_id} + + if not issue_type_name: + issue_type_name = self.configuration.default_issue_type_name + + issuetype = {"name": issue_type_name} + + issue_fields = { + "project": project, + "issuetype": issuetype, + "assignee": assignee, + "reporter": reporter, + "summary": title, + **other_fields, + } + + issue = client.create_issue(fields=issue_fields) + + if incident_ticket_key: + update = { + "issuelinks": [ + { + "add": { + "type": { + "name": "Relates", + "inward": "is related to", + "outward": "relates to", + }, + "outwardIssue": {"key": incident_ticket_key}, + } + } + ] + } + issue.update(update=update) + + return { + "resource_id": issue.key, + "weblink": f"{self.configuration.browser_url}/browse/{issue.key}", + } + def update_case_ticket( self, ticket_id: str, diff --git a/src/dispatch/static/dispatch/src/incident/TaskEditDialog.vue b/src/dispatch/static/dispatch/src/incident/TaskEditDialog.vue new file mode 100644 index 000000000000..84513a9ae054 --- /dev/null +++ b/src/dispatch/static/dispatch/src/incident/TaskEditDialog.vue @@ -0,0 +1,127 @@ + + + diff --git a/src/dispatch/static/dispatch/src/incident/TasksTab.vue b/src/dispatch/static/dispatch/src/incident/TasksTab.vue index 2da5d784f257..44174f20790d 100644 --- a/src/dispatch/static/dispatch/src/incident/TasksTab.vue +++ b/src/dispatch/static/dispatch/src/incident/TasksTab.vue @@ -1,19 +1,90 @@