Skip to content

Commit 321e50d

Browse files
DolevNedolevneshahargl
authored
feat: Adding ticket creation an linking to incident overview (#5136)
Signed-off-by: DolevNe <130115929+DolevNe@users.noreply.github.com> Signed-off-by: Shahar Glazner <shaharglazner@gmail.com> Co-authored-by: dolevne <ydolev.nezer@gmail.com> Co-authored-by: Shahar Glazner <shaharglazner@gmail.com>
1 parent e8f6c2a commit 321e50d

24 files changed

+1089
-16
lines changed

docs/snippets/providers/jira-snippet-autogenerated.mdx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ This provider requires authentication.
66
- **email**: Atlassian Jira Email (required: True, sensitive: False)
77
- **api_token**: Atlassian Jira API Token (required: True, sensitive: True)
88
- **host**: Atlassian Jira Host (required: True, sensitive: False)
9+
- **ticket_creation_url**: URL for creating new tickets (optional, will use default if not provided) (required: False, sensitive: False)
910

1011
Certain scopes may be required to perform specific actions or queries via the provider. Below is a summary of relevant scopes and their use cases:
11-
- **BROWSE_PROJECTS**: Browse Jira Projects (mandatory)
12-
- **CREATE_ISSUES**: Create Jira Issues (mandatory)
13-
- **CLOSE_ISSUES**: Close Jira Issues
14-
- **EDIT_ISSUES**: Edit Jira Issues
15-
- **DELETE_ISSUES**: Delete Jira Issues
16-
- **MODIFY_REPORTER**: Modify Jira Issue Reporter
12+
- **BROWSE_PROJECTS**: Browse Jira Projects (mandatory)
13+
- **CREATE_ISSUES**: Create Jira Issues (mandatory)
14+
- **CLOSE_ISSUES**: Close Jira Issues
15+
- **EDIT_ISSUES**: Edit Jira Issues
16+
- **DELETE_ISSUES**: Delete Jira Issues
17+
- **MODIFY_REPORTER**: Modify Jira Issue Reporter
1718

1819

1920

docs/snippets/providers/jiraonprem-snippet-autogenerated.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py
1+
{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py
22
Do not edit it manually, as it will be overwritten */}
33

44
## Authentication
55
This provider requires authentication.
66
- **host**: Jira Host (required: True, sensitive: False)
77
- **personal_access_token**: Jira PAT (required: True, sensitive: True)
8+
- **ticket_creation_url**: URL for creating new tickets (required: False, sensitive: False)
89

910
Certain scopes may be required to perform specific actions or queries via the provider. Below is a summary of relevant scopes and their use cases:
1011
- **BROWSE_PROJECTS**: Browse Jira Projects (mandatory)

docs/snippets/providers/linear-snippet-autogenerated.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py
1+
{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py
22
Do not edit it manually, as it will be overwritten */}
33

44
## Authentication
55
This provider requires authentication.
66
- **api_token**: Linear API Token (required: True, sensitive: True)
7+
- **ticket_creation_url**: URL for creating new tickets (required: False, sensitive: False)
78

89

910
## In workflows

docs/snippets/providers/redmine-snippet-autogenerated.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py
1+
{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py
22
Do not edit it manually, as it will be overwritten */}
33

44
## Authentication
55
This provider requires authentication.
66
- **host**: Redmine Host (required: True, sensitive: False)
77
- **api_access_key**: Redmine API Access key (required: True, sensitive: True)
8+
- **ticket_creation_url**: URL for creating new tickets (required: False, sensitive: False)
89

910
Certain scopes may be required to perform specific actions or queries via the provider. Below is a summary of relevant scopes and their use cases:
1011
- **authenticated**: Authenticated with Redmine API (mandatory)

docs/snippets/providers/servicenow-snippet-autogenerated.mdx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py
1+
{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py
22
Do not edit it manually, as it will be overwritten */}
33

44
## Authentication
@@ -8,6 +8,7 @@ This provider requires authentication.
88
- **password**: The password of the ServiceNow user (required: True, sensitive: True)
99
- **client_id**: The client ID to use OAuth 2.0 based authentication (required: False, sensitive: False)
1010
- **client_secret**: The client secret to use OAuth 2.0 based authentication (required: False, sensitive: True)
11+
- **ticket_creation_url**: URL for creating new tickets (required: False, sensitive: False)
1112

1213
Certain scopes may be required to perform specific actions or queries via the provider. Below is a summary of relevant scopes and their use cases:
1314
- **itil**: The user can read/write tickets from the table (mandatory) ([Documentation](https://docs.servicenow.com/bundle/sandiego-platform-administration/page/administer/roles/reference/r_BaseSystemRoles.html))
@@ -57,6 +58,6 @@ Check the following workflow examples:
5758
5859
5960
## Topology
60-
This provider pulls [topology](/overview/servicetopology) to Keep. It could be used in [correlations](/overview/correlation-topology)
61-
and [mapping](/overview/enrichment/mapping#mapping-with-topology-data), and as a context
61+
This provider pulls [topology](/overview/servicetopology) to Keep. It could be used in [correlations](/overview/correlation-topology)
62+
and [mapping](/overview/enrichment/mapping#mapping-with-topology-data), and as a context
6263
for [alerts](/alerts/sidebar#7-alert-topology-view) and [incidents](/overview#17-incident-topology).

docs/snippets/providers/youtrack-snippet-autogenerated.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py
1+
{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py
22
Do not edit it manually, as it will be overwritten */}
33

44
## Authentication
55
This provider requires authentication.
66
- **host_url**: YouTrack Host URL (required: True, sensitive: False)
77
- **project_id**: YouTrack Project ID (required: True, sensitive: False)
88
- **permanent_token**: YouTrack Permanent Token (required: True, sensitive: True)
9+
- **ticket_creation_url**: URL for creating new tickets (required: False, sensitive: False)
910

1011
Certain scopes may be required to perform specific actions or queries via the provider. Below is a summary of relevant scopes and their use cases:
1112
- **create_issue**: (mandatory)

docs/snippets/providers/zendesk-snippet-autogenerated.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py
1+
{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py
22
Do not edit it manually, as it will be overwritten */}
33

44
## Authentication
55
This provider requires authentication.
66
- **api_key**: Zendesk API key (required: True, sensitive: True)
7+
- **zendesk_domain**: Zendesk domain (required: True, sensitive: False)
8+
- **ticket_creation_url**: URL for creating new tickets (required: False, sensitive: False)
79

810

911
## In workflows
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
"use client";
2+
3+
import { useState, useMemo, useEffect } from "react";
4+
import { Button, Text, Select, SelectItem, TextInput, Textarea } from "@tremor/react";
5+
import Modal from "@/components/ui/Modal";
6+
import { DynamicImageProviderIcon } from "@/components/ui";
7+
import { useFetchProviders } from "@/app/(keep)/providers/page.client";
8+
import { type IncidentDto } from "@/entities/incidents/model";
9+
import { type Provider } from "@/shared/api/providers";
10+
import { getProviderBaseUrl, getTicketCreateUrl, canCreateTickets } from "@/entities/incidents/lib/ticketing-utils";
11+
12+
interface CreateTicketModalProps {
13+
incident: IncidentDto;
14+
isOpen: boolean;
15+
onClose: () => void;
16+
}
17+
18+
export function CreateTicketModal({
19+
incident,
20+
isOpen,
21+
onClose,
22+
}: CreateTicketModalProps) {
23+
const [selectedProviderId, setSelectedProviderId] = useState<string>("");
24+
const [ticketTitle, setTicketTitle] = useState<string>("");
25+
const [ticketDescription, setTicketDescription] = useState<string>("");
26+
const { installedProviders, isLoading: isLoadingProviders } = useFetchProviders();
27+
28+
// Initialize title and description when modal opens or incident changes
29+
useEffect(() => {
30+
setTicketTitle(incident.user_generated_name || "");
31+
setTicketDescription((incident.user_summary || "").replace(/<[^>]*>/g, ''));
32+
}, [incident, isOpen]);
33+
34+
const ticketingProviders = useMemo(() => {
35+
return installedProviders.filter(canCreateTickets);
36+
}, [installedProviders]);
37+
38+
// Auto-select the provider if there's only one
39+
useEffect(() => {
40+
if (ticketingProviders.length === 1) {
41+
setSelectedProviderId(ticketingProviders[0].id);
42+
}
43+
}, [ticketingProviders]);
44+
45+
// Get the selected provider
46+
const selectedProvider = useMemo(() => {
47+
return ticketingProviders.find(provider => provider.id === selectedProviderId);
48+
}, [ticketingProviders, selectedProviderId]);
49+
50+
const handleCreateTicket = () => {
51+
if (!selectedProvider) return;
52+
53+
const createUrl = getTicketCreateUrl(selectedProvider, ticketDescription, ticketTitle);
54+
55+
if (createUrl) {
56+
window.open(createUrl);
57+
onClose();
58+
}
59+
};
60+
61+
const handleCancel = () => {
62+
setSelectedProviderId("");
63+
setTicketTitle("");
64+
setTicketDescription("");
65+
onClose();
66+
};
67+
68+
// Show loading state while providers are being fetched
69+
if (isLoadingProviders) {
70+
return (
71+
<Modal
72+
isOpen={isOpen}
73+
onClose={handleCancel}
74+
title="Create New Ticket"
75+
className="w-[450px]"
76+
>
77+
<div className="flex flex-col gap-4">
78+
<Text className="text-gray-500">
79+
Loading ticketing providers...
80+
</Text>
81+
<div className="flex justify-end">
82+
<Button variant="secondary" onClick={handleCancel}>
83+
Close
84+
</Button>
85+
</div>
86+
</div>
87+
</Modal>
88+
);
89+
}
90+
91+
// If no ticketing providers are available after loading
92+
if (ticketingProviders.length === 0) {
93+
return (
94+
<Modal
95+
isOpen={isOpen}
96+
onClose={handleCancel}
97+
title="Create New Ticket"
98+
className="w-[450px]"
99+
>
100+
<div className="flex flex-col gap-4">
101+
<Text className="text-red-500">
102+
No providers with ticket creation URL are configured. Please configure a ticketing creation URL first.
103+
</Text>
104+
<div className="flex justify-end">
105+
<Button variant="secondary" onClick={handleCancel}>
106+
Close
107+
</Button>
108+
</div>
109+
</div>
110+
</Modal>
111+
);
112+
}
113+
114+
return (
115+
<Modal
116+
isOpen={isOpen}
117+
onClose={handleCancel}
118+
title="Create New Ticket"
119+
className="w-[450px]"
120+
>
121+
<div className="flex flex-col gap-2">
122+
{/* Only show Select if there are multiple providers */}
123+
{ticketingProviders.length > 1 ? (
124+
<div>
125+
<Text className="mb-2">
126+
Select Ticketing Provider <span className="text-red-500">*</span>
127+
</Text>
128+
<Select
129+
placeholder="Select a ticketing provider"
130+
value={selectedProviderId}
131+
onValueChange={setSelectedProviderId}
132+
>
133+
{ticketingProviders.map((provider) => (
134+
<SelectItem key={provider.id} value={provider.id}>
135+
<div className="flex items-center gap-2">
136+
<DynamicImageProviderIcon
137+
src={`/icons/${provider.type}-icon.png`}
138+
width={20}
139+
height={20}
140+
alt={provider.type}
141+
providerType={provider.type}
142+
/>
143+
<span>
144+
{provider.display_name || provider.id}
145+
{provider.details?.authentication && (
146+
<span className="text-gray-500 ml-2">
147+
({getProviderBaseUrl(provider)})
148+
</span>
149+
)}
150+
</span>
151+
</div>
152+
</SelectItem>
153+
))}
154+
</Select>
155+
</div>
156+
) : null}
157+
158+
{/* Ticket Title Input */}
159+
<div>
160+
<Text className="mb-2">
161+
Ticket Title <span className="text-red-500">*</span>
162+
</Text>
163+
<TextInput
164+
placeholder="Enter ticket title"
165+
value={ticketTitle}
166+
onChange={(e) => setTicketTitle(e.target.value)}
167+
/>
168+
</div>
169+
170+
{/* Ticket Description Input */}
171+
<div>
172+
<Text className="mb-2">
173+
Ticket Description
174+
</Text>
175+
<Textarea
176+
placeholder="Enter ticket description"
177+
value={ticketDescription}
178+
onChange={(e) => setTicketDescription(e.target.value.replace(/<[^>]*>/g, ''))}
179+
rows={4}
180+
/>
181+
</div>
182+
183+
{/* Show selected provider info */}
184+
{selectedProvider && (
185+
<>
186+
<Text className="text-sm font-medium mb-1">Selected Provider</Text>
187+
188+
<div className="bg-gray-50 p-3 rounded-md space-y-2">
189+
<div className="flex items-center gap-3">
190+
<DynamicImageProviderIcon
191+
src={`/icons/${selectedProvider.type}-icon.png`}
192+
width={30}
193+
height={30}
194+
alt={selectedProvider.type}
195+
providerType={selectedProvider.type}
196+
/>
197+
<Text className="text-base text-gray-600">
198+
{selectedProvider.display_name || selectedProvider.id}
199+
</Text>
200+
201+
</div>
202+
<Text className="text-xsm text-gray-500 ml-2 break-all">
203+
{getProviderBaseUrl(selectedProvider)}
204+
</Text>
205+
</div>
206+
<div className="mt-1 p-2 bg-blue-50 border border-blue-200 rounded-md">
207+
<Text className="text-sm text-blue-700">
208+
<strong>Note:</strong> After creating the ticket, you&apos;ll need to manually link it back to this incident using the ticket URL.
209+
</Text>
210+
</div>
211+
<Text className="text-sm text-orange-500 mt-1">
212+
You will be redirected to the {selectedProvider.display_name || selectedProvider.id} instance with the details above.
213+
</Text>
214+
</>
215+
)}
216+
217+
218+
<div className="flex justify-end gap-2 pt-2">
219+
<Button
220+
variant="secondary"
221+
onClick={handleCancel}
222+
>
223+
Cancel
224+
</Button>
225+
<Button
226+
variant="primary"
227+
color="orange"
228+
onClick={handleCreateTicket}
229+
disabled={!selectedProviderId || !ticketTitle.trim()}
230+
>
231+
Create Ticket
232+
</Button>
233+
</div>
234+
</div>
235+
</Modal>
236+
);
237+
}

keep-ui/app/(keep)/incidents/[id]/incident-header.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import { useIncident } from "@/utils/hooks/useIncidents";
1818
import { IncidentOverview } from "./incident-overview";
1919
import { CopilotKit } from "@copilotkit/react-core";
2020
import { TbInfoCircle, TbTopologyStar3 } from "react-icons/tb";
21+
import { useConfig } from "@/utils/hooks/useConfig";
22+
import { TicketingIncidentOptions } from "./ticketing-incident-options";
2123

2224
export function IncidentHeader({
2325
incident: initialIncidentData,
@@ -30,6 +32,7 @@ export function IncidentHeader({
3032
});
3133
const { deleteIncident, confirmPredictedIncident } = useIncidentActions();
3234
const incident = fetchedIncident || initialIncidentData;
35+
const { data: config } = useConfig();
3336

3437
const router = useRouter();
3538
const pathname = usePathname();
@@ -80,6 +83,11 @@ export function IncidentHeader({
8083

8184
{!incident.is_candidate && (
8285
<div className="flex">
86+
{config?.KEEP_TICKETING_ENABLED && (
87+
<TicketingIncidentOptions
88+
incident={incident}
89+
/>
90+
)}
8391
<Button
8492
color="orange"
8593
size="xs"

0 commit comments

Comments
 (0)