Skip to content

Commit

Permalink
Merge branch 'main' into feature/1968-topology-applications
Browse files Browse the repository at this point in the history
  • Loading branch information
talboren authored Sep 24, 2024
2 parents 7f69917 + 1f229e9 commit eb93b1d
Show file tree
Hide file tree
Showing 21 changed files with 5,033 additions and 769 deletions.
1 change: 1 addition & 0 deletions keep-ui/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
21 changes: 21 additions & 0 deletions keep-ui/app/incidents/[id]/incident-chat.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.copilotKitInput {
@apply sticky bottom-0 bg-white !important;
@apply flex items-center outline-none rounded-tremor-default px-3 py-2 mx-2 text-tremor-default focus:ring-2 transition duration-100 border shadow-tremor-input focus:border-tremor-brand-subtle focus:ring-tremor-brand-muted dark:shadow-dark-tremor-input focus:dark:border-dark-tremor-brand-subtle focus:dark:ring-dark-tremor-brand-muted bg-tremor-background dark:bg-dark-tremor-background hover:bg-tremor-background-muted dark:hover:bg-dark-tremor-background-muted text-tremor-content-emphasis dark:text-dark-tremor-content-emphasis border-tremor-border dark:border-dark-tremor-border placeholder:text-tremor-content dark:placeholder:text-dark-tremor-content !important;
}

.copilotKitInput:hover textarea {
@apply bg-tremor-background-muted dark:bg-dark-tremor-background-muted transition duration-100 !important;
}

.copilotKitInput textarea {
height: unset !important;
max-height: unset !important;
}

.copilotKitMessages {
@apply px-4 !important;
}

.copilotKitMessages .suggestion {
@apply bg-white text-black scale-100 border-tremor-border border-2 hover:border-tremor-brand hover:text-tremor-brand hover:bg-tremor-brand-muted dark:border-dark-tremor-brand dark:hover:bg-dark-tremor-brand-muted transition !important;
}
132 changes: 132 additions & 0 deletions keep-ui/app/incidents/[id]/incident-chat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import {
CopilotChat,
CopilotKitCSSProperties,
useCopilotChatSuggestions,
} from "@copilotkit/react-ui";
import { IncidentDto } from "../models";
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 <Loading />;
if (!alerts?.items || alerts.items.length === 0)
return (
<EmptyStateCard
title="Chat not available"
description="No alerts found for this incident. Go to the alerts feed and assign alerts to interact with the incident."
buttonText="Assign alerts to this incident"
onClick={() => router.push("/alerts/feed")}
/>
);

return (
<div
style={
{
"--copilot-kit-primary-color":
"rgb(249 115 22 / var(--tw-bg-opacity))",
} as CopilotKitCSSProperties
}
>
<CopilotChat
className="-mx-2"
instructions={`You now act as an expert incident responder.
You are responsible for resolving incidents and helping the incident responding team.
The information you are provided with is a JSON representing all the data about the incident and a list of alerts that are related to the incident.
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?",
}}
/>
</div>
);
}
71 changes: 46 additions & 25 deletions keep-ui/app/incidents/[id]/incident-info.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button, Title } from "@tremor/react";
import { Button, Icon, Title } from "@tremor/react";
import { IncidentDto } from "../models";
import CreateOrUpdateIncident from "../create-or-update-incident";
import Modal from "@/components/ui/Modal";
Expand All @@ -12,7 +12,7 @@ import {
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { format } from "date-fns";
// import { RiSparkling2Line } from "react-icons/ri";
import { ArrowUturnLeftIcon } from "@heroicons/react/24/outline";

interface Props {
incident: IncidentDto;
Expand All @@ -38,13 +38,14 @@ export default function IncidentInformation({ incident }: Props) {
};

const formatString = "dd, MMM yyyy - HH:mm.ss 'UTC'";
const summary = incident.user_summary || incident.generated_summary;

return (
<div className="flex h-full flex-col justify-between">
<div>
<div className="flex justify-between mb-2.5">
<div className="flex flex-col gap-2">
<div className="flex justify-between text-sm">
<Title className="">
{incident.is_confirmed ? "⚔️ " : "Possible "}Incident Information
{incident.is_confirmed ? "⚔️ " : "Possible "}Incident
</Title>
{incident.is_confirmed && (
<Button
Expand All @@ -60,9 +61,7 @@ export default function IncidentInformation({ incident }: Props) {
/>
)}
{!incident.is_confirmed && (
<div
className={"space-x-1 flex flex-row items-center justify-center"}
>
<div className="space-x-1 flex flex-row items-center justify-center">
<Button
color="orange"
size="xs"
Expand Down Expand Up @@ -104,24 +103,46 @@ export default function IncidentInformation({ incident }: Props) {
</div>
)}
</div>
<div className="prose-2xl">
{incident.user_generated_name || incident.ai_generated_name}
<div className="prose-2xl flex gap-2 items-start">
<Icon
icon={ArrowUturnLeftIcon}
tooltip="Go Back"
variant="shadow"
className="cursor-pointer"
onClick={() => router.back()}
/>
<span>
{incident.user_generated_name || incident.ai_generated_name}
</span>
</div>
<div>
<h3 className="text-gray-500 text-sm">Summary</h3>
{summary ? <p>{summary}</p> : <p>No summary yet</p>}
</div>
<div className="flex gap-4">
{!!incident.start_time && (
<div>
<h3 className="text-gray-500 text-sm">Started at</h3>
<p className="">
{format(new Date(incident.start_time), formatString)}
</p>
</div>
)}
{!!incident.last_seen_time && (
<div>
<h3 className="text-gray-500 text-sm">Last seen at</h3>
<p>{format(new Date(incident.last_seen_time), formatString)}</p>
</div>
)}
</div>
<div>
{!!incident.rule_fingerprint && (
<>
<h3 className="text-sm text-gray-500">Group by value</h3>
<p>{incident.rule_fingerprint}</p>
</>
)}
</div>
<p>Summary: {incident.user_summary || incident.generated_summary}</p>
{!!incident.start_time && (
<p>
Started at: {format(new Date(incident.start_time), formatString)}
</p>
)}
{!!incident.last_seen_time && (
<p>
Last seen at:{" "}
{format(new Date(incident.last_seen_time), formatString)}
</p>
)}
{!!incident.rule_fingerprint && (
<p>Group by value: {incident.rule_fingerprint}</p>
)}
</div>
<Modal
isOpen={isFormOpen}
Expand Down
34 changes: 13 additions & 21 deletions keep-ui/app/incidents/[id]/incident-timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const AlertEventInfo: React.FC<{ event: AuditEvent; alert: AlertDto }> = ({
<div className="grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
<p className="text-gray-400">Date:</p>
<p>
{format(parseISO(event.timestamp), "dd, MMM yyyy - HH:mm.ss 'UTC'")}
{format(parseISO(event.timestamp), "dd, MMM yyyy - HH:mm:ss 'UTC'")}
</p>

<p className="text-gray-400">Action:</p>
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -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,
Expand Down Expand Up @@ -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)}
</div>
))}
{/* Add an extra label for the first time, positioned at the start */}
<div
className="text-xs px-2 text-gray-400 absolute whitespace-nowrap"
style={{
left: "0px",
transform: "translateX(0)",
}}
>
{format(intervals[0], formatString)}
</div>
</div>

{/* Alert bars */}
Expand Down
Loading

0 comments on commit eb93b1d

Please sign in to comment.