Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: general incident improvements #2612

Merged
merged 4 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Button, 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;
Expand Down Expand Up @@ -43,18 +43,16 @@ export default function IncidentAlertMenu({ incidentId, alert }: Props) {
}

return (
<div className="">
<Button
variant="light"
size="xs"
icon={LinkSlashIcon}
<div className="flex flex-col">
<Badge
icon={LiaUnlinkSolid}
color="red"
tooltip="Remove correlation"
className="cursor-pointer"
onClick={onRemove}
>
Remove correlation
</Button>
Unlink
</Badge>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export default function IncidentAlerts({ incident }: Props) {
}),
columnHelper.accessor("lastReceived", {
id: "lastReceived",
header: "Last Received",
header: "Last Event Time",
minSize: 100,
cell: (context) => (
<span>{getAlertLastReceieved(context.getValue())}</span>
Expand Down
15 changes: 9 additions & 6 deletions keep-ui/app/(keep)/incidents/[id]/incident-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -53,7 +54,7 @@ export function IncidentHeader({
};

return (
<>
<CopilotKit runtimeUrl="/api/copilotkit">
<header className="flex flex-col gap-4">
<Subtitle className="text-sm">
<Link href="/incidents">All Incidents</Link>{" "}
Expand All @@ -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
</Button>
{incident.is_confirmed && (
<Button
color="orange"
size="xs"
variant="secondary"
icon={MdModeEdit}
tooltip="Edit Incident"
onClick={(e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
handleStartEdit();
}}
/>
>
Edit Incident
</Button>
)}
{!incident.is_confirmed && (
<div className="space-x-1 flex flex-row items-center justify-center">
Expand Down Expand Up @@ -143,6 +146,6 @@ export function IncidentHeader({
incident={runWorkflowModalIncident}
handleClose={() => setRunWorkflowModalIncident(null)}
/>
</>
</CopilotKit>
);
}
140 changes: 130 additions & 10 deletions keep-ui/app/(keep)/incidents/[id]/incident-overview.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
Expand All @@ -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 = (
<Markdown remarkPlugins={[remarkRehype]} rehypePlugins={[rehypeRaw]}>
{summary}
{summary ?? generatedSummary}
</Markdown>
);

Expand All @@ -62,9 +119,20 @@ function Summary({
);
}

return (
//TODO: suggest generate summary if it's empty
summary ? <div>{formatedSummary}</div> : <p>No summary yet</p>
return summary || generatedSummary ? (
<div>{formatedSummary}</div>
) : (
<Button
variant="secondary"
onClick={executeTask}
className="mt-2.5"
disabled={generatingSummary}
loading={generatingSummary}
icon={TbSparkles}
size="xs"
>
AI Summary
</Button>
);
}

Expand Down Expand Up @@ -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,
Expand All @@ -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 <IncidentOverviewSkeleton />;
}

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
Expand All @@ -122,12 +213,19 @@ export function IncidentOverview({ incident: initialIncidentData }: Props) {
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
<div className="max-w-2xl">
<FieldHeader>Summary</FieldHeader>
<Summary title="Summary" summary={summary} />
<Summary
title="Summary"
summary={summary}
alerts={alerts.items}
incident={incident}
/>
{incident.user_summary && incident.generated_summary ? (
<Summary
title="AI version"
summary={incident.generated_summary}
collapsable={true}
alerts={alerts.items}
incident={incident}
/>
) : null}
{incident.merged_into_incident_id && (
Expand All @@ -142,14 +240,36 @@ export function IncidentOverview({ incident: initialIncidentData }: Props) {
{notNullServices.length > 0 ? (
<div className="flex flex-wrap gap-1">
{notNullServices.map((service) => (
<Badge key={service} size="sm">
<Badge
key={service}
size="sm"
className="cursor-pointer"
onClick={() => filterBy("service", service)}
>
{service}
</Badge>
))}
</div>
) : (
"No services involved"
)}
<FieldHeader>Affected environments</FieldHeader>
{environments.length > 0 ? (
<div className="flex flex-wrap gap-1">
{environments.map((env) => (
<Badge
key={env}
size="sm"
className="cursor-pointer"
onClick={() => filterBy("environment", env)}
>
{env}
</Badge>
))}
</div>
) : (
"No environments involved"
)}
</div>
<div>
<SameIncidentField incident={incident} />
Expand Down
53 changes: 53 additions & 0 deletions keep-ui/app/(keep)/incidents/incident-overview-skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Skeleton from "react-loading-skeleton";
import { FieldHeader } from "@/shared/ui";

export function IncidentOverviewSkeleton() {
return (
<div className="flex gap-6 items-start w-full pb-4 text-tremor-default">
<div className="basis-2/3 grow">
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
<div className="max-w-2xl">
<FieldHeader>Summary</FieldHeader>
<Skeleton count={3} />
</div>
<div className="flex flex-col gap-2">
<FieldHeader>Involved services</FieldHeader>
<div className="flex flex-wrap gap-1">
<Skeleton width={80} />
<Skeleton width={100} />
<Skeleton width={90} />
</div>
</div>
<div>
<Skeleton count={2} />
</div>
<div>
<Skeleton count={2} />
</div>
</div>
</div>
<div className="pr-10 grid grid-cols-1 xl:grid-cols-2 gap-4">
<div className="xl:col-span-2">
<FieldHeader>Status</FieldHeader>
<Skeleton height={38} />
</div>
<div>
<FieldHeader>Last Incident Activity</FieldHeader>
<Skeleton />
</div>
<div>
<FieldHeader>Started at</FieldHeader>
<Skeleton />
</div>
<div>
<FieldHeader>Assignee</FieldHeader>
<Skeleton />
</div>
<div>
<FieldHeader>Group by value</FieldHeader>
<Skeleton />
</div>
</div>
</div>
);
}
Loading