Skip to content

Commit

Permalink
feat: working
Browse files Browse the repository at this point in the history
  • Loading branch information
talboren committed Sep 23, 2024
1 parent 34ee9d5 commit a3d9cc2
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 101 deletions.
129 changes: 105 additions & 24 deletions keep-ui/app/incidents/[id]/incident-chat.tsx
Original file line number Diff line number Diff line change
@@ -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 <Loading />;
if (!alerts?.items || alerts.items.length === 0)
Expand All @@ -24,28 +102,31 @@ export default function IncidentChat({ incident }: { incident: IncidentDto }) {
);

return (
<CopilotChat
className="h-full overflow-y-hidden"
instructions={`You are an expert incident responder.
You are responsible for resolving incidents and helping the incident responder team.
<div
style={
{
"--copilot-kit-primary-color":
"rgb(249 115 22 / var(--tw-bg-opacity))",
} as CopilotKitCSSProperties
}
>
<CopilotChat
className="h-full overflow-y-hidden"
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 by providing insights and recommendations.
If you are not sure about the answer, you NEED to say you don't know and lack more context.
Here is the incident JSON with all the details: "${JSON.stringify(
incident
)}"
Here is the list of alerts related to the incident: "${JSON.stringify(
alerts.items
)}"`}
labels={{
title: "Incident Assitant",
initial:
"Hi! 👋 Lets work together to resolve thins incident! What can I help you with?",
placeholder:
"What do you think the root cause of this incident might be?",
}}
/>
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>
);
}
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
8 changes: 4 additions & 4 deletions keep-ui/app/incidents/[id]/incident.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,16 @@ export default function IncidentView({ incidentId }: Props) {
</div>
<div
id="incidentTabs"
className="w-full xl:pl-2.5 overflow-x-scroll h-full overflow-hidden"
className="w-full xl:pl-2.5 overflow-x-scroll h-full"
>
<TabGroup defaultIndex={0} className="h-full">
<TabGroup defaultIndex={0}>
<TabList variant="line" color="orange">
<Tab icon={CiBellOn}>Alerts</Tab>
<Tab icon={CiViewTimeline}>Timeline</Tab>
<Tab icon={IoIosGitNetwork}>Topology</Tab>
<Tab icon={CiChat2}>Chat</Tab>
</TabList>
<TabPanels className="h-full">
<TabPanels>
<TabPanel>
<IncidentAlerts incident={incident} />
</TabPanel>
Expand All @@ -85,7 +85,7 @@ export default function IncidentView({ incidentId }: Props) {
/>
</div>
</TabPanel>
<TabPanel className="h-[calc(100%-50px)]">
<TabPanel>
<IncidentChat incident={incident} />
</TabPanel>
</TabPanels>
Expand Down
44 changes: 31 additions & 13 deletions keep-ui/app/incidents/create-or-update-incident.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,37 @@ 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;
createCallback?: (id: string) => void;
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,
Expand Down Expand Up @@ -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();
Expand Down
8 changes: 5 additions & 3 deletions keep-ui/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
],
Expand Down
Loading

0 comments on commit a3d9cc2

Please sign in to comment.