Skip to content

Commit

Permalink
feat(ui/ux): adds a task edit dialog with create ticket action (#5260)
Browse files Browse the repository at this point in the history
  • Loading branch information
whitdog47 authored Oct 14, 2024
1 parent ddb3cd0 commit b2cd9b5
Show file tree
Hide file tree
Showing 18 changed files with 541 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -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 ###
4 changes: 4 additions & 0 deletions src/dispatch/incident/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
11 changes: 11 additions & 0 deletions src/dispatch/incident/type/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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):
Expand Down
16 changes: 16 additions & 0 deletions src/dispatch/plugins/dispatch_core/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
75 changes: 75 additions & 0 deletions src/dispatch/plugins/dispatch_jira/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down
127 changes: 127 additions & 0 deletions src/dispatch/static/dispatch/src/incident/TaskEditDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<template>
<v-dialog v-model="showEditTaskDialog" persistent max-width="750px">
<v-form @submit.prevent v-slot="{ isValid }">
<v-card>
<v-card-title>
<span class="text-h5">Edit Task</span>
</v-card-title>
<v-card-text>
<v-row>
<v-col cols="12">
<v-textarea
v-model="description"
auto-grow
:rows="1"
:max-rows="5"
class="mt-3"
label="Description"
hint="Description of the task."
clearable
required
/>
</v-col>
<v-col cols="12">
<v-select
v-model="status"
label="Status"
:items="statuses"
hint="The task's current status"
/>
</v-col>
<v-col cols="12">
<participant-select
v-model="owner"
label="Owner"
hint="The task's current owner"
clearable
required
name="owner"
:rules="[required_and_only_one]"
/>
</v-col>
<v-col cols="12">
<assignee-combobox
v-model="assignees"
label="Assignee"
hint="The task's current assignee"
clearable
required
name="assignees"
:rules="[required_and_only_one]"
/>
</v-col>
<v-col cols="12" v-if="status == 'Resolved'">
<date-time-picker-menu label="Resolved At" v-model="resolved_at" />
</v-col>
<v-col cols="12" v-else>
<date-time-picker-menu label="Resolve By" v-model="resolve_by" />
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="closeNewTaskDialog()"> Cancel </v-btn>
<v-btn
:disabled="!isValid.value"
color="green en-1"
variant="text"
@click="updateExistingTask()"
>
OK
</v-btn>
</v-card-actions>
</v-card>
</v-form>
</v-dialog>
</template>

<script>
import { mapFields } from "vuex-map-fields"
import { mapActions } from "vuex"
import ParticipantSelect from "@/components/ParticipantSelect.vue"
import AssigneeCombobox from "@/task/AssigneeCombobox.vue"
import DateTimePickerMenu from "@/components/DateTimePickerMenu.vue"
export default {
name: "EditTaskDialog",
components: {
AssigneeCombobox,
ParticipantSelect,
DateTimePickerMenu,
},
data() {
return {
statuses: ["Open", "Resolved"],
required_and_only_one: (value) => {
if (!value || value.length == 0) {
return "This field is required"
}
if (value && value.length > 1) {
return "Only one is allowed"
}
return true
},
}
},
computed: {
...mapFields("incident", [
"dialogs.showEditTaskDialog",
"selected.currentTask.description",
"selected.currentTask",
"selected.currentTask.owner",
"selected.currentTask.assignees",
"selected.currentTask.resolved_at",
"selected.currentTask.resolve_by",
"selected.currentTask.status",
]),
},
methods: {
...mapActions("incident", ["closeNewTaskDialog", "updateExistingTask"]),
},
}
</script>
Loading

0 comments on commit b2cd9b5

Please sign in to comment.