diff --git a/keep-ui/app/incidents/[id]/incident-chat.tsx b/keep-ui/app/incidents/[id]/incident-chat.tsx index 7d1dc5f41..1cdb45ac7 100644 --- a/keep-ui/app/incidents/[id]/incident-chat.tsx +++ b/keep-ui/app/incidents/[id]/incident-chat.tsx @@ -1,16 +1,94 @@ -import { CopilotChat } from "@copilotkit/react-ui"; +import { + CopilotChat, + CopilotKitCSSProperties, + useCopilotChatSuggestions, +} from "@copilotkit/react-ui"; import { IncidentDto } from "../models"; -import { useIncidentAlerts } from "utils/hooks/useIncidents"; +import { + useIncident, + useIncidentAlerts, + useIncidents, +} from "utils/hooks/useIncidents"; import { EmptyStateCard } from "@/components/ui/EmptyStateCard"; import { useRouter } from "next/navigation"; import "./incident-chat.css"; import Loading from "app/loading"; +import { useCopilotAction, useCopilotReadable } from "@copilotkit/react-core"; +import { updateIncidentRequest } from "../create-or-update-incident"; +import { useSession } from "next-auth/react"; +import { toast } from "react-toastify"; export default function IncidentChat({ incident }: { incident: IncidentDto }) { const router = useRouter(); + const { mutate } = useIncidents(true, 20); + const { mutate: mutateIncident } = useIncident(incident.id); const { data: alerts, isLoading: alertsLoading } = useIncidentAlerts( incident.id ); + const { data: session } = useSession(); + + useCopilotReadable({ + description: "incidentDetails", + value: incident, + }); + useCopilotReadable({ + description: "alerts", + value: alerts?.items, + }); + + useCopilotChatSuggestions({ + instructions: `The following incident is on going: ${JSON.stringify( + incident + )}. Provide good question suggestions for the incident responder team.`, + }); + + useCopilotAction({ + name: "gotoAlert", + description: "Select an alert and filter the feed by the alert fingerprint", + parameters: [ + { + name: "fingerprint", + type: "string", + description: + "The fingerprint of the alert. You can extract it using the alert name as well.", + required: true, + }, + ], + handler: async ({ fingerprint }) => { + window.open(`/alerts/feed?fingerprint=${fingerprint}`, "_blank"); + }, + }); + + useCopilotAction({ + name: "updateIncidentNameAndSummary", + description: "Update incident name and summary", + parameters: [ + { + name: "name", + type: "string", + description: "The new name for the incident", + }, + { + name: "summary", + type: "string", + description: "The new summary for the incident", + }, + ], + handler: async ({ name, summary }) => { + const response = await updateIncidentRequest( + session, + incident.id, + name, + summary, + incident.assignee + ); + if (response.ok) { + mutate(); + mutateIncident(); + toast.success("Incident updated successfully"); + } + }, + }); if (alertsLoading) return ; if (!alerts?.items || alerts.items.length === 0) @@ -24,28 +102,31 @@ export default function IncidentChat({ incident }: { incident: IncidentDto }) { ); return ( - + + Your job is to help the incident responder team to resolve the incident as soon as possible by providing insights and recommendations. + + Use the incident details and alerts context to give good, meaningful answers. + If you do not know the answer or lack context, share that with the end user and ask for more context.`} + labels={{ + title: "Incident Assitant", + initial: + "Hi! 👋 Lets work together to resolve this incident! Ask me anything", + placeholder: + "For example: What do you think the root cause of this incident might be?", + }} + /> + ); } diff --git a/keep-ui/app/incidents/[id]/incident-timeline.tsx b/keep-ui/app/incidents/[id]/incident-timeline.tsx index d2400f6d4..94b3b5c5f 100644 --- a/keep-ui/app/incidents/[id]/incident-timeline.tsx +++ b/keep-ui/app/incidents/[id]/incident-timeline.tsx @@ -58,7 +58,7 @@ const AlertEventInfo: React.FC<{ event: AuditEvent; alert: AlertDto }> = ({

Date:

- {format(parseISO(event.timestamp), "dd, MMM yyyy - HH:mm.ss 'UTC'")} + {format(parseISO(event.timestamp), "dd, MMM yyyy - HH:mm:ss 'UTC'")}

Action:

@@ -134,7 +134,7 @@ interface AlertBarProps { auditEvents: AuditEvent[]; startTime: Date; endTime: Date; - timeScale: "minutes" | "hours" | "days"; + timeScale: "seconds" | "minutes" | "hours" | "days"; onEventClick: (event: AuditEvent | null) => void; selectedEventId: string | null; isFirstRow: boolean; @@ -285,14 +285,13 @@ export default function IncidentTimeline({ const startTime = new Date(Math.min(...allTimestamps)); const endTime = new Date(Math.max(...allTimestamps)); - // Add padding to start and end times - const paddedStartTime = new Date(startTime.getTime() - 1000 * 60 * 10); // 10 minutes before - const paddedEndTime = new Date(endTime.getTime() + 1000 * 60 * 10); // 10 minutes after + // Add padding to end time only + const paddedEndTime = new Date(endTime.getTime() + 1000 * 60); // 1 minute after - const totalDuration = paddedEndTime.getTime() - paddedStartTime.getTime(); + const totalDuration = paddedEndTime.getTime() - startTime.getTime(); const pixelsPerMillisecond = 5000 / totalDuration; // Assuming 5000px minimum width - let timeScale: "minutes" | "hours" | "days"; + let timeScale: "seconds" | "minutes" | "hours" | "days"; let intervalDuration: number; let formatString: string; @@ -304,21 +303,25 @@ export default function IncidentTimeline({ timeScale = "hours"; intervalDuration = 60 * 60 * 1000; formatString = "HH:mm"; - } else { + } else if (totalDuration > 60 * 60 * 1000) { timeScale = "minutes"; intervalDuration = 5 * 60 * 1000; // 5-minute intervals + formatString = "HH:mm"; + } else { + timeScale = "seconds"; + intervalDuration = 10 * 1000; // 10-second intervals formatString = "HH:mm:ss"; } const intervals: Date[] = []; - let currentTime = paddedStartTime; + let currentTime = startTime; while (currentTime <= paddedEndTime) { intervals.push(new Date(currentTime)); currentTime = new Date(currentTime.getTime() + intervalDuration); } return { - startTime: paddedStartTime, + startTime, endTime: paddedEndTime, intervals, formatString, @@ -374,22 +377,11 @@ export default function IncidentTimeline({ pixelsPerMillisecond }px`, transform: "translateX(-50%)", - visibility: index === 0 ? "hidden" : "visible", // Hide the first label }} > {format(time, formatString)}
))} - {/* Add an extra label for the first time, positioned at the start */} -
- {format(intervals[0], formatString)} -
{/* Alert bars */} diff --git a/keep-ui/app/incidents/[id]/incident.tsx b/keep-ui/app/incidents/[id]/incident.tsx index bb2327a6d..75da1cacb 100644 --- a/keep-ui/app/incidents/[id]/incident.tsx +++ b/keep-ui/app/incidents/[id]/incident.tsx @@ -59,16 +59,16 @@ export default function IncidentView({ incidentId }: Props) {
- + Alerts Timeline Topology Chat - + @@ -85,7 +85,7 @@ export default function IncidentView({ incidentId }: Props) { />
- + diff --git a/keep-ui/app/incidents/create-or-update-incident.tsx b/keep-ui/app/incidents/create-or-update-incident.tsx index d5c5be2c4..4b863a97a 100644 --- a/keep-ui/app/incidents/create-or-update-incident.tsx +++ b/keep-ui/app/incidents/create-or-update-incident.tsx @@ -14,6 +14,7 @@ import { toast } from "react-toastify"; import { getApiURL } from "utils/apiUrl"; import { IncidentDto } from "./models"; import { useIncidents } from "utils/hooks/useIncidents"; +import { Session } from "next-auth"; interface Props { incidentToEdit: IncidentDto | null; @@ -21,6 +22,29 @@ interface Props { exitCallback?: () => void; } +export const updateIncidentRequest = async ( + session: Session | null, + incidentId: string, + incidentName: string, + incidentUserSummary: string, + incidentAssignee: string +) => { + const apiUrl = getApiURL(); + const response = await fetch(`${apiUrl}/incidents/${incidentId}`, { + method: "PUT", + headers: { + Authorization: `Bearer ${session?.accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + user_generated_name: incidentName, + user_summary: incidentUserSummary, + assignee: incidentAssignee, + }), + }); + return response; +}; + export default function CreateOrUpdateIncident({ incidentToEdit, createCallback, @@ -88,19 +112,13 @@ export default function CreateOrUpdateIncident({ // This is the function that will be called on submitting the form in the editMode, it sends a PUT request to the backend. const updateIncident = async (e: FormEvent) => { e.preventDefault(); - const apiUrl = getApiURL(); - const response = await fetch(`${apiUrl}/incidents/${incidentToEdit?.id}`, { - method: "PUT", - headers: { - Authorization: `Bearer ${session?.accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - user_generated_name: incidentName, - user_summary: incidentUserSummary, - assignee: incidentAssignee, - }), - }); + const response = await updateIncidentRequest( + session, + incidentToEdit?.id!, + incidentName, + incidentUserSummary, + incidentAssignee + ); if (response.ok) { exitEditMode(); await mutate(); diff --git a/keep-ui/next.config.js b/keep-ui/next.config.js index ab20e577a..4589bc81f 100644 --- a/keep-ui/next.config.js +++ b/keep-ui/next.config.js @@ -49,13 +49,15 @@ const nextConfig = { async headers() { // Allow Keycloak Server as a CORS origin since we use SSO wizard as iframe const keycloakIssuer = process.env.KEYCLOAK_ISSUER; - const keycloakServer = keycloakIssuer ? keycloakIssuer.split('/auth')[0] : 'http://localhost:8181'; + const keycloakServer = keycloakIssuer + ? keycloakIssuer.split("/auth")[0] + : "http://localhost:8181"; return [ { - source: '/:path*', + source: "/:path*", headers: [ { - key: 'Access-Control-Allow-Origin', + key: "Access-Control-Allow-Origin", value: keycloakServer, }, ], diff --git a/keep-ui/package-lock.json b/keep-ui/package-lock.json index bf3dcf28c..1294c2bba 100644 --- a/keep-ui/package-lock.json +++ b/keep-ui/package-lock.json @@ -5405,6 +5405,16 @@ "node": ">=12.4.0" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@panva/hkdf": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.1.1.tgz", @@ -8636,11 +8646,11 @@ "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==" }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -8651,11 +8661,6 @@ } } }, - "node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, "node_modules/decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -9582,22 +9587,6 @@ } } }, - "node_modules/eslint-import-resolver-typescript/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/eslint-module-utils": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.11.0.tgz", diff --git a/keep-ui/tsconfig.json b/keep-ui/tsconfig.json index 581abba93..8bf6718b0 100644 --- a/keep-ui/tsconfig.json +++ b/keep-ui/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "es6", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -25,9 +21,7 @@ ], "baseUrl": ".", "paths": { - "@/components/*": [ - "./components/*" - ] + "@/components/*": ["./components/*"] } }, "include": [ @@ -38,7 +32,5 @@ "pages/signin.tsx", "./.next/types/**/*.ts" ], - "exclude": [ - "node_modules" - ] + "exclude": ["node_modules"] }