diff --git a/keep-ui/app/(keep)/incidents/page.tsx b/keep-ui/app/(keep)/incidents/page.tsx
index 67ca38e1b..de8a52c88 100644
--- a/keep-ui/app/(keep)/incidents/page.tsx
+++ b/keep-ui/app/(keep)/incidents/page.tsx
@@ -2,13 +2,14 @@ import { IncidentList } from "@/features/incident-list";
import { getIncidents, GetIncidentsParams } from "@/entities/incidents/api";
import { PaginatedIncidentsDto } from "@/entities/incidents/model";
import { createServerApiClient } from "@/shared/api/server";
+import {DefaultIncidentFilters} from "@/entities/incidents/model/models";
const defaultIncidentsParams: GetIncidentsParams = {
confirmed: true,
limit: 20,
offset: 0,
sorting: { id: "creation_time", desc: true },
- filters: {},
+ filters: DefaultIncidentFilters,
};
export default async function Page() {
diff --git a/keep-ui/entities/incidents/model/models.ts b/keep-ui/entities/incidents/model/models.ts
index ffd3b201e..1f8f3a5a2 100644
--- a/keep-ui/entities/incidents/model/models.ts
+++ b/keep-ui/entities/incidents/model/models.ts
@@ -6,6 +6,12 @@ export enum Status {
Resolved = "resolved",
Acknowledged = "acknowledged",
Merged = "merged",
+ Deleted = "deleted",
+}
+
+export const DefaultIncidentFilteredStatuses: string[] = [Status.Firing, Status.Acknowledged, Status.Merged];
+export const DefaultIncidentFilters: object = {
+ "status": DefaultIncidentFilteredStatuses,
}
export interface IncidentDto {
diff --git a/keep-ui/entities/incidents/ui/statuses.tsx b/keep-ui/entities/incidents/ui/statuses.tsx
index 9e049d36d..8f2dbe2c9 100644
--- a/keep-ui/entities/incidents/ui/statuses.tsx
+++ b/keep-ui/entities/incidents/ui/statuses.tsx
@@ -5,7 +5,7 @@ import {
ExclamationCircleIcon,
PauseIcon,
} from "@heroicons/react/24/outline";
-import { IoIosGitPullRequest } from "react-icons/io";
+import {IoIosGitPullRequest, IoIosTrash} from "react-icons/io";
import React from "react";
import { capitalize } from "@/utils/helpers";
@@ -49,6 +49,14 @@ export const STATUS_ICONS = {
className="w-4 h-4 mr-2"
/>
),
+ [Status.Deleted]: (
+
+ ),
};
export function StatusIcon({
diff --git a/keep-ui/features/change-incident-status/ui/incident-change-status-select.tsx b/keep-ui/features/change-incident-status/ui/incident-change-status-select.tsx
index 8b1c574a2..41d7febd0 100644
--- a/keep-ui/features/change-incident-status/ui/incident-change-status-select.tsx
+++ b/keep-ui/features/change-incident-status/ui/incident-change-status-select.tsx
@@ -48,7 +48,7 @@ export function IncidentChangeStatusSelect({
const { changeStatus } = useIncidentActions();
const statusOptions = useMemo(
() =>
- Object.values(Status).map((status) => ({
+ Object.values(Status).filter((status) => status != Status.Deleted || value == Status.Deleted).map((status) => ({
value: status,
label: (
@@ -57,7 +57,7 @@ export function IncidentChangeStatusSelect({
),
})),
- []
+ [value]
);
const handleChange = useCallback(
diff --git a/keep-ui/features/incident-list/ui/incident-table-filters-context.tsx b/keep-ui/features/incident-list/ui/incident-table-filters-context.tsx
index 8fd6549e4..51767f62b 100644
--- a/keep-ui/features/incident-list/ui/incident-table-filters-context.tsx
+++ b/keep-ui/features/incident-list/ui/incident-table-filters-context.tsx
@@ -10,6 +10,7 @@ import { createContext, useState, FC, PropsWithChildren } from "react";
import { useSearchParams, useRouter, usePathname } from "next/navigation";
import { useIncidentsMeta } from "../../../utils/hooks/useIncidents";
import type { IncidentsMetaDto } from "@/entities/incidents/model";
+import { DefaultIncidentFilteredStatuses } from "@/entities/incidents/model/models";
interface IIncidentFilterContext {
meta: IncidentsMetaDto | undefined;
@@ -43,16 +44,19 @@ export const IncidentFilterContextProvider: FC = ({
const { data: incidentsMeta, isLoading } = useIncidentsMeta();
const setFilterValue = useCallback(
- (filterName: string) => {
+ (filterName: string, defaultValues: string[] | undefined = undefined) => {
return () => {
if (incidentsMeta === undefined) return [];
const values = searchParams?.get(filterName);
- const valuesArray = values
- ?.split(",")
- .filter((value) =>
- incidentsMeta[filterName as keyof IncidentsMetaDto]?.includes(value)
- );
+ let valuesArray = values?.split(",")
+ if (!valuesArray) {
+ valuesArray = defaultValues;
+ }
+
+ valuesArray = valuesArray?.filter((value) =>
+ incidentsMeta[filterName as keyof IncidentsMetaDto]?.includes(value)
+ );
return (valuesArray || []) as string[];
};
@@ -61,7 +65,7 @@ export const IncidentFilterContextProvider: FC = ({
);
const [statuses, setStatuses] = useState(
- setFilterValue("statuses")
+ setFilterValue("statuses", incidentsMeta?.statuses.filter((status) => DefaultIncidentFilteredStatuses.includes(status)))
);
const [severities, setSeverities] = useState(
setFilterValue("severities")
@@ -76,7 +80,7 @@ export const IncidentFilterContextProvider: FC = ({
useEffect(() => {
if (!isLoading) {
- setStatuses(setFilterValue("statuses"));
+ setStatuses(setFilterValue("statuses", incidentsMeta?.statuses.filter((status) => DefaultIncidentFilteredStatuses.includes(status))));
setSeverities(setFilterValue("severities"));
setAssignees(setFilterValue("assignees"));
setServices(setFilterValue("services"));
diff --git a/keep/api/bl/incidents_bl.py b/keep/api/bl/incidents_bl.py
index 7feb07bb6..c5525e7cd 100644
--- a/keep/api/bl/incidents_bl.py
+++ b/keep/api/bl/incidents_bl.py
@@ -43,9 +43,11 @@
class IncidentBl:
def __init__(
- self, tenant_id: str, session: Session, pusher_client: Optional[Pusher] = None
+ self, tenant_id: str, session: Session, pusher_client: Optional[Pusher] = None,
+ user: str = None
):
self.tenant_id = tenant_id
+ self.user = user
self.session = session
self.pusher_client = pusher_client
self.logger = logging.getLogger(__name__)
diff --git a/keep/api/core/db.py b/keep/api/core/db.py
index 158972d6a..859076b56 100644
--- a/keep/api/core/db.py
+++ b/keep/api/core/db.py
@@ -37,7 +37,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, selectinload, subqueryload
+from sqlalchemy.orm import joinedload, subqueryload
from sqlalchemy.sql import exists, expression
from sqlmodel import Session, SQLModel, col, or_, select, text
@@ -1786,6 +1786,7 @@ def get_incident_for_grouping_rule(
.where(Incident.rule_id == rule.id)
.where(Incident.rule_fingerprint == rule_fingerprint)
.where(Incident.status != IncidentStatus.RESOLVED.value)
+ .where(Incident.status != IncidentStatus.DELETED.value)
.order_by(Incident.creation_time.desc())
).first()
@@ -2921,25 +2922,16 @@ def is_alert_assigned_to_incident(
with Session(engine) as session:
assigned = session.exec(
select(LastAlertToIncident)
+ .join(Incident, LastAlertToIncident.incident_id == Incident.id)
.where(LastAlertToIncident.fingerprint == fingerprint)
.where(LastAlertToIncident.incident_id == incident_id)
.where(LastAlertToIncident.tenant_id == tenant_id)
.where(LastAlertToIncident.deleted_at == NULL_FOR_DELETED_AT)
+ .where(Incident.status != IncidentStatus.DELETED.value)
).first()
return assigned is not None
-def get_incidents(tenant_id) -> List[Incident]:
- with Session(engine) as session:
- incidents = session.exec(
- select(Incident)
- .options(selectinload(Incident.alerts))
- .where(Incident.tenant_id == tenant_id)
- .order_by(desc(Incident.creation_time))
- ).all()
- return incidents
-
-
def get_alert_audit(
tenant_id: str, fingerprint: str | list[str], limit: int = 50
) -> List[AlertAudit]:
@@ -3493,27 +3485,25 @@ def delete_incident_by_id(
if isinstance(incident_id, str):
incident_id = __convert_to_uuid(incident_id)
with Session(engine) as session:
- incident = (
- session.query(Incident)
+ incident = session.exec(
+ select(Incident)
.filter(
Incident.tenant_id == tenant_id,
Incident.id == incident_id,
)
- .first()
- )
-
- # Delete all associations with alerts:
+ ).first()
- (
- session.query(LastAlertToIncident)
+ session.execute(
+ update(Incident)
.where(
- LastAlertToIncident.tenant_id == tenant_id,
- LastAlertToIncident.incident_id == incident.id,
+ Incident.tenant_id == tenant_id,
+ Incident.id == incident.id,
)
- .delete()
+ .values({
+ "status": IncidentStatus.DELETED.value
+ })
)
- session.delete(incident)
session.commit()
return True
diff --git a/keep/api/models/alert.py b/keep/api/models/alert.py
index e4d2dac26..71d120070 100644
--- a/keep/api/models/alert.py
+++ b/keep/api/models/alert.py
@@ -113,6 +113,8 @@ class IncidentStatus(Enum):
ACKNOWLEDGED = "acknowledged"
# Incident was merged with another incident
MERGED = "merged"
+ # Incident was removed
+ DELETED = "deleted"
class IncidentSeverity(SeverityBaseInterface):
diff --git a/tests/test_incidents.py b/tests/test_incidents.py
index da35ce3c1..21f80d92b 100644
--- a/tests/test_incidents.py
+++ b/tests/test_incidents.py
@@ -227,7 +227,7 @@ def test_add_remove_alert_to_incidents(db_session, setup_stress_alerts_no_elasti
def test_get_last_incidents(db_session, create_alert):
severity_cycle = cycle([s.order for s in IncidentSeverity])
- status_cycle = cycle([s.value for s in IncidentStatus])
+ status_cycle = cycle([s.value for s in IncidentStatus if s not in [IncidentStatus.MERGED, IncidentStatus.DELETED]])
services_cycle = cycle(["keep", None])
for i in range(60):
@@ -244,33 +244,31 @@ def test_get_last_incidents(db_session, create_alert):
"status": status,
},
)
- # Merged incidents don't have alerts
- if status != IncidentStatus.MERGED.value:
- create_alert(
- f"alert-test-{i}",
- AlertStatus(status),
- datetime.utcnow(),
- {
- "severity": AlertSeverity.from_number(severity),
- "service": service,
- },
- )
- alert = db_session.query(Alert).order_by(Alert.timestamp.desc()).first()
+ create_alert(
+ f"alert-test-{i}",
+ AlertStatus(status),
+ datetime.utcnow(),
+ {
+ "severity": AlertSeverity.from_number(severity),
+ "service": service,
+ },
+ )
+ alert = db_session.query(Alert).order_by(Alert.timestamp.desc()).first()
- create_alert(
- f"alert-test-2-{i}",
- AlertStatus(status),
- datetime.utcnow(),
- {
- "severity": AlertSeverity.from_number(severity),
- "service": service,
- },
- )
- alert2 = db_session.query(Alert).order_by(Alert.timestamp.desc()).first()
+ create_alert(
+ f"alert-test-2-{i}",
+ AlertStatus(status),
+ datetime.utcnow(),
+ {
+ "severity": AlertSeverity.from_number(severity),
+ "service": service,
+ },
+ )
+ alert2 = db_session.query(Alert).order_by(Alert.timestamp.desc()).first()
- add_alerts_to_incident_by_incident_id(
- SINGLE_TENANT_UUID, incident.id, [alert.fingerprint, alert2.fingerprint]
- )
+ add_alerts_to_incident_by_incident_id(
+ SINGLE_TENANT_UUID, incident.id, [alert.fingerprint, alert2.fingerprint]
+ )
incidents_default, incidents_default_count = get_last_incidents(SINGLE_TENANT_UUID)
assert len(incidents_default) == 0
@@ -333,8 +331,8 @@ def test_get_last_incidents(db_session, create_alert):
SINGLE_TENANT_UUID, is_confirmed=True, filters=filters_2, limit=100
)
assert (
- len(incidents_with_filters_2) == 15 + 15
- ) # 15 confirmed, 15 acknowledged because 60 incidents with cycled status
+ len(incidents_with_filters_2) == 20 + 20
+ ) # 20 confirmed, 20 acknowledged because 60 incidents with cycled status
assert all(
[i.status in ["firing", "acknowledged"] for i in incidents_with_filters_2]
)
@@ -343,7 +341,7 @@ def test_get_last_incidents(db_session, create_alert):
incidents_with_filters_3, _ = get_last_incidents(
SINGLE_TENANT_UUID, is_confirmed=True, filters=filters_3, limit=100
)
- assert len(incidents_with_filters_3) == 45 # 60 minus 15 merged with no alerts
+ assert len(incidents_with_filters_3) == 60
assert all(["keep" in i.sources for i in incidents_with_filters_3])
filters_4 = {"sources": ["grafana"]}
@@ -458,7 +456,7 @@ def test_incident_metadata(
db_session, client, test_app, setup_stress_alerts_no_elastic
):
severity_cycle = cycle([s.order for s in IncidentSeverity])
- status_cycle = cycle([s.value for s in IncidentStatus])
+ status_cycle = cycle([s.value for s in IncidentStatus if s != IncidentStatus.DELETED.value])
sources_cycle = cycle(["keep", "keep-test", "keep-test-2"])
services_cycle = cycle(["keep", "keep-test", "keep-test-2"])
@@ -1177,12 +1175,12 @@ def test_incident_bl_delete_incident(db_session):
incident_dto = incident_bl.create_incident(incident_dto_in)
- incidents_count = db_session.query(Incident).count()
+ incidents_count = db_session.query(Incident).filter(Incident.status != IncidentStatus.DELETED.value).count()
assert incidents_count == 1
incident_bl.delete_incident(incident_dto.id)
- incidents_count = db_session.query(Incident).count()
+ incidents_count = db_session.query(Incident).filter(Incident.status != IncidentStatus.DELETED.value).count()
assert incidents_count == 0
# Check pusher