Skip to content
Closed
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
71 changes: 56 additions & 15 deletions keep-ui/app/(keep)/incidents/[id]/incident-overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ import {
type IncidentDto,
} from "@/entities/incidents/model";
import React, { useState } from "react";
import { useIncident, useIncidentAlerts } from "@/utils/hooks/useIncidents";
import {
useIncident,
useIncidentAlerts,
useIncidentsByIds,
} 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, Icon, TextInput } from "@tremor/react";
import { Badge, Callout } from "@tremor/react";
import { Button, DynamicImageProviderIcon, Link } from "@/components/ui";
import Modal from "@/components/ui/Modal";
import { IncidentChangeStatusSelect } from "features/incidents/change-incident-status";
import { getIncidentName } from "@/entities/incidents/lib/utils";
import { DateTimeField, FieldHeader } from "@/shared/ui";
Expand Down Expand Up @@ -166,7 +169,7 @@ function Summary({
);
}

function MergedCallout({
function ThisIncidentMergedToCallout({
merged_into_incident_id,
className,
}: {
Expand Down Expand Up @@ -201,6 +204,48 @@ function MergedCallout({
);
}

function IncidentsMergedIntoThisOneCallout({
merged_incidents,
className,
}: {
merged_incidents: string[];
className?: string;
}) {
const incidents = useIncidentsByIds(merged_incidents);

if (!incidents) {
return null;
}

return (
<Callout
// @ts-ignore
title={
<div>
<p>Those incidents merged into this one:</p>
{incidents?.data?.map((incident: IncidentDto) => (
<Link
key={incident.id}
icon={() => (
<StatusIcon
className="!p-0"
status={incident.status}
size="xs"
/>
)}
href={`/incidents/${incident?.id}`}
>
{getIncidentName(incident)}
</Link>
)) || null}
</div>
}
color="purple"
className={className}
/>
);
}

export function IncidentOverview({ incident: initialIncidentData }: Props) {
const router = useRouter();
const { data: fetchedIncident, mutate } = useIncident(
Expand Down Expand Up @@ -311,22 +356,18 @@ export function IncidentOverview({ incident: initialIncidentData }: Props) {
alerts={alerts.items}
incident={incident}
/>
{/* @tb: not sure how we use this, but leaving it here for now
{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 && (
<MergedCallout
<ThisIncidentMergedToCallout
className="inline-block mt-2"
merged_into_incident_id={incident.merged_into_incident_id}
/>
)}
{incident.merged_incidents?.length > 0 && (
<IncidentsMergedIntoThisOneCallout
className="inline-block mt-2"
merged_incidents={incident.merged_incidents}
/>
)}
<div className="mt-2">
<SameIncidentField incident={incident} />
</div>
Expand Down
1 change: 1 addition & 0 deletions keep-ui/entities/incidents/model/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export interface IncidentDto {
merged_into_incident_id: string;
merged_by: string;
merged_at: Date;
merged_incidents: string[];
fingerprint: string;
enrichments: { [key: string]: any };
incident_type?: string;
Expand Down
21 changes: 21 additions & 0 deletions keep-ui/utils/hooks/useIncidents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,27 @@ export const useIncidents = (
};
};

export const useIncidentsByIds = (
incidentIds: string[],
options: SWRConfiguration = {
revalidateOnFocus: false,
}
) => {
const api = useApi();

return useSWR(
() =>
api.isReady() && incidentIds.length
? incidentIds.map((id) => `/incidents/${id}`)
: null,
async (urls) => {
const results = await Promise.all(urls.map((url) => api.get(url)));
return results;
},
options
);
};

export const useIncidentAlerts = (
incidentId: string,
limit: number = 20,
Expand Down
30 changes: 16 additions & 14 deletions keep/api/core/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
from sqlalchemy.exc import IntegrityError, OperationalError
from sqlalchemy.orm import joinedload, subqueryload, foreign
from sqlalchemy.orm import joinedload, subqueryload
from sqlalchemy.orm.exc import StaleDataError
from sqlalchemy.sql import exists, expression
from sqlmodel import Session, SQLModel, col, or_, select, text
Expand Down Expand Up @@ -3679,20 +3679,22 @@ def get_incident_by_id(
if isinstance(incident_id, str):
incident_id = __convert_to_uuid(incident_id, should_raise=True)
with existed_or_new_session(session) as session:
query = session.query(
Incident,
AlertEnrichment,
).outerjoin(
AlertEnrichment,
and_(
Incident.tenant_id == AlertEnrichment.tenant_id,
cast(col(Incident.id), String) == foreign(AlertEnrichment.alert_fingerprint),
),
).filter(
Incident.tenant_id == tenant_id,
Incident.id == incident_id,
query = (
select(Incident, AlertEnrichment)
.join(
AlertEnrichment,
and_(
Incident.tenant_id == AlertEnrichment.tenant_id,
cast(col(Incident.id), String) == AlertEnrichment.alert_fingerprint,
),
isouter=True,
)
.where(Incident.tenant_id == tenant_id, Incident.id == incident_id)
.options(joinedload(Incident.merged_incidents))
)
incident_with_enrichments = query.first()

incident_with_enrichments = session.exec(query).first()

if incident_with_enrichments:
incident, enrichments = incident_with_enrichments
if with_alerts:
Expand Down
11 changes: 11 additions & 0 deletions keep/api/models/incident.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from pydantic import BaseModel, Extra, Field, PrivateAttr, root_validator
from sqlmodel import col, desc
from sqlalchemy.orm.exc import DetachedInstanceError

from keep.api.models.db.incident import Incident, IncidentSeverity, IncidentStatus
from keep.api.models.db.rule import ResolveOn, Rule
Expand Down Expand Up @@ -73,6 +74,7 @@ class IncidentDto(IncidentDtoIn):
merged_into_incident_id: UUID | None
merged_by: str | None
merged_at: datetime.datetime | None
merged_incidents: list[UUID] | None = []

enrichments: dict | None = {}
incident_type: str | None
Expand Down Expand Up @@ -158,6 +160,14 @@ def from_db_incident(cls, db_incident: "Incident", rule: "Rule" = None):
else db_incident.severity
)

# merged_incidents are joined only when needed
try:
merged_incidents = db_incident.merged_incidents or []
if isinstance(merged_incidents, list):
merged_incidents = [incident.id for incident in merged_incidents]
except DetachedInstanceError:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when could this happen?

merged_incidents = None

# some default value for resolve_on
if not db_incident.resolve_on:
db_incident.resolve_on = ResolveOn.ALL.value
Expand Down Expand Up @@ -193,6 +203,7 @@ def from_db_incident(cls, db_incident: "Incident", rule: "Rule" = None):
rule_id=rule.id if rule else None,
rule_name=rule.name if rule else None,
rule_is_deleted=rule.is_deleted if rule else None,
merged_incidents=merged_incidents,
)

# This field is required for getting alerts when required
Expand Down
Loading