diff --git a/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-menu.tsx b/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-menu.tsx index 799afbc28..2edd7a36f 100644 --- a/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-menu.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-menu.tsx @@ -1,10 +1,10 @@ -import { Icon } from "@tremor/react"; +import { Badge, Icon } from "@tremor/react"; import { AlertDto } from "@/app/(keep)/alerts/models"; import { useHydratedSession as useSession } from "@/shared/lib/hooks/useHydratedSession"; import { toast } from "react-toastify"; import { useApiUrl } from "utils/hooks/useConfig"; import { useIncidentAlerts } from "utils/hooks/useIncidents"; -import { LinkSlashIcon } from "@heroicons/react/24/outline"; +import { LiaUnlinkSolid } from "react-icons/lia"; interface Props { incidentId: string; @@ -44,13 +44,15 @@ export default function IncidentAlertMenu({ incidentId, alert }: Props) { return (
- + > + Unlink +
); } diff --git a/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alerts.tsx b/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alerts.tsx index 7d3d828e8..6679ff361 100644 --- a/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alerts.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alerts.tsx @@ -114,11 +114,7 @@ export default function IncidentAlerts({ incident }: Props) { id: "severity", header: "Severity", minSize: 80, - cell: (context) => ( -
- -
- ), + cell: (context) => , }), columnHelper.display({ id: "name", @@ -161,7 +157,7 @@ export default function IncidentAlerts({ incident }: Props) { }), columnHelper.accessor("lastReceived", { id: "lastReceived", - header: "Last Received", + header: "Last Event Time", minSize: 100, cell: (context) => ( {getAlertLastReceieved(context.getValue())} diff --git a/keep-ui/app/(keep)/incidents/[id]/incident-header.tsx b/keep-ui/app/(keep)/incidents/[id]/incident-header.tsx index d6ff1a10b..9b039922f 100644 --- a/keep-ui/app/(keep)/incidents/[id]/incident-header.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/incident-header.tsx @@ -17,6 +17,7 @@ import { IncidentSeverityBadge } from "@/entities/incidents/ui"; import { getIncidentName } from "@/entities/incidents/lib/utils"; import { useIncident } from "@/utils/hooks/useIncidents"; import { IncidentOverview } from "./incident-overview"; +import { CopilotKit } from "@copilotkit/react-core"; export function IncidentHeader({ incident: initialIncidentData, @@ -53,7 +54,7 @@ export function IncidentHeader({ }; return ( - <> +
All Incidents{" "} @@ -70,26 +71,28 @@ export function IncidentHeader({ size="xs" variant="secondary" icon={MdPlayArrow} - tooltip="Run Workflow" onClick={(e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); handleRunWorkflow(); }} - /> + > + Run Workflow + {incident.is_confirmed && ( )} {!incident.is_confirmed && (
@@ -143,6 +146,6 @@ export function IncidentHeader({ incident={runWorkflowModalIncident} handleClose={() => setRunWorkflowModalIncident(null)} /> - + ); } diff --git a/keep-ui/app/(keep)/incidents/[id]/incident-overview.tsx b/keep-ui/app/(keep)/incidents/[id]/incident-overview.tsx index f91f2ef90..6612d5fa5 100644 --- a/keep-ui/app/(keep)/incidents/[id]/incident-overview.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/incident-overview.tsx @@ -1,15 +1,19 @@ "use client"; -import type { IncidentDto } from "@/entities/incidents/model"; -import React from "react"; -import { useIncident } from "@/utils/hooks/useIncidents"; +import { + useIncidentActions, + type IncidentDto, + type PaginatedIncidentAlertsDto, +} from "@/entities/incidents/model"; +import React, { useState } from "react"; +import { useIncident, useIncidentAlerts } from "@/utils/hooks/useIncidents"; import { Disclosure } from "@headlessui/react"; import { IoChevronDown } from "react-icons/io5"; import remarkRehype from "remark-rehype"; import rehypeRaw from "rehype-raw"; import Markdown from "react-markdown"; import { Badge, Callout } from "@tremor/react"; -import { Link } from "@/components/ui"; +import { Button, Link } from "@/components/ui"; import { IncidentChangeStatusSelect } from "@/features/change-incident-status"; import { getIncidentName } from "@/entities/incidents/lib/utils"; import { DateTimeField, FieldHeader } from "@/shared/ui"; @@ -19,6 +23,16 @@ import { } from "@/features/same-incidents-in-the-past/"; import { StatusIcon } from "@/entities/incidents/ui/statuses"; import clsx from "clsx"; +import { TbSparkles } from "react-icons/tb"; +import { + CopilotTask, + useCopilotAction, + useCopilotContext, + useCopilotReadable, +} from "@copilotkit/react-core"; +import { IncidentOverviewSkeleton } from "../incident-overview-skeleton"; +import { AlertDto } from "../../alerts/models"; +import { useRouter } from "next/navigation"; interface Props { incident: IncidentDto; @@ -29,15 +43,58 @@ function Summary({ summary, collapsable, className, + alerts, + incident, }: { title: string; summary: string; collapsable?: boolean; className?: string; + alerts: AlertDto[]; + incident: IncidentDto; }) { + const [generatedSummary, setGeneratedSummary] = useState(""); + const { updateIncident } = useIncidentActions(); + const context = useCopilotContext(); + useCopilotReadable({ + description: "The incident alerts", + value: alerts, + }); + useCopilotReadable({ + description: "The incident title", + value: incident.user_generated_name ?? incident.ai_generated_name, + }); + useCopilotAction({ + name: "setGeneratedSummary", + description: "Set the generated summary", + parameters: [ + { name: "summary", type: "string", description: "The generated summary" }, + ], + handler: async ({ summary }) => { + await updateIncident( + incident.id, + { + user_summary: summary, + }, + true + ); + setGeneratedSummary(summary); + }, + }); + const task = new CopilotTask({ + instructions: + "Generate a short concise summary of the incident based on the context of the alerts and the title of the incident. Don't repeat prompt.", + }); + const [generatingSummary, setGeneratingSummary] = useState(false); + const executeTask = async () => { + setGeneratingSummary(true); + await task.run(context); + setGeneratingSummary(false); + }; + const formatedSummary = ( - {summary} + {summary ?? generatedSummary} ); @@ -62,9 +119,20 @@ function Summary({ ); } - return ( - //TODO: suggest generate summary if it's empty - summary ?
{formatedSummary}
:

No summary yet

+ return summary || generatedSummary ? ( +
{formatedSummary}
+ ) : ( + ); } @@ -104,6 +172,7 @@ function MergedCallout({ } export function IncidentOverview({ incident: initialIncidentData }: Props) { + const router = useRouter(); const { data: fetchedIncident } = useIncident(initialIncidentData.id, { fallbackData: initialIncidentData, revalidateOnMount: false, @@ -114,6 +183,28 @@ export function IncidentOverview({ incident: initialIncidentData }: Props) { const notNullServices = incident.services.filter( (service) => service !== "null" ); + const { + data: alerts, + isLoading: _alertsLoading, + error: alertsError, + } = useIncidentAlerts(incident.id, 20, 0); + const environments = Array.from( + new Set( + alerts?.items + .filter((alert) => alert.environment) + .map((alert) => alert.environment) + ) + ); + + if (!alerts || _alertsLoading) { + return ; + } + + const filterBy = (key: string, value: string) => { + router.push( + `/alerts/feed?cel=${key}%3D%3D${encodeURIComponent(`"${value}"`)}` + ); + }; return ( // Adding padding bottom to visually separate from the tabs @@ -122,12 +213,19 @@ export function IncidentOverview({ incident: initialIncidentData }: Props) {
Summary - + {incident.user_summary && incident.generated_summary ? ( ) : null} {incident.merged_into_incident_id && ( @@ -142,7 +240,12 @@ export function IncidentOverview({ incident: initialIncidentData }: Props) { {notNullServices.length > 0 ? (
{notNullServices.map((service) => ( - + filterBy("service", service)} + > {service} ))} @@ -150,6 +253,23 @@ export function IncidentOverview({ incident: initialIncidentData }: Props) { ) : ( "No services involved" )} + Affected environments + {environments.length > 0 ? ( +
+ {environments.map((env) => ( + filterBy("environment", env)} + > + {env} + + ))} +
+ ) : ( + "No environments involved" + )}
diff --git a/keep-ui/app/(keep)/incidents/incident-overview-skeleton.tsx b/keep-ui/app/(keep)/incidents/incident-overview-skeleton.tsx new file mode 100644 index 000000000..450732001 --- /dev/null +++ b/keep-ui/app/(keep)/incidents/incident-overview-skeleton.tsx @@ -0,0 +1,53 @@ +import Skeleton from "react-loading-skeleton"; +import { FieldHeader } from "@/shared/ui"; + +export function IncidentOverviewSkeleton() { + return ( +
+
+
+
+ Summary + +
+
+ Involved services +
+ + + +
+
+
+ +
+
+ +
+
+
+
+
+ Status + +
+
+ Last Incident Activity + +
+
+ Started at + +
+
+ Assignee + +
+
+ Group by value + +
+
+
+ ); +}