Skip to content
Open
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
78 changes: 61 additions & 17 deletions keep-ui/features/incidents/incident-list/ui/incident-list.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";
import { Card, Title, Subtitle, Button, Badge } from "@tremor/react";
import React, { useMemo, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import type {
IncidentDto,
PaginatedIncidentsDto,
Expand All @@ -16,13 +16,14 @@ import { InitialFacetsData } from "@/features/filter/api";
import { FacetsPanelServerSide } from "@/features/filter/facet-panel-server-side";
import { Icon } from "@tremor/react";
import {
EmptyStateCard,
KeepLoader,
PageSubtitle,
PageTitle,
SeverityBorderIcon,
UISeverity,
} from "@/shared/ui";
import { BellIcon, BellSlashIcon } from "@heroicons/react/24/outline";
import { BellIcon, BellSlashIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { UserStatefulAvatar } from "@/entities/users/ui";
import { getStatusIcon, getStatusColor } from "@/shared/lib/status-utils";
import { useUser } from "@/entities/users/model/useUser";
Expand Down Expand Up @@ -52,6 +53,7 @@ import EnhancedDateRangePickerV2, {
} from "@/components/ui/DateRangePickerV2";
import { useTimeframeState } from "@/components/ui/useTimeframeState";
import { PaginationState } from "@/features/filter/pagination";
import { AlertsRulesBuilder } from "@/features/presets/presets-manager";

const AssigneeLabel = ({ email }: { email: string }) => {
const user = useUser(email);
Expand All @@ -74,7 +76,8 @@ export function IncidentList({
DEFAULT_INCIDENTS_SORTING,
]);

const [filterCel, setFilterCel] = useState<string | null>(null);
const [filterCel, setFilterCel] = useState<string | null>("");
const [searchCel, setSearchCel] = useState<string | null>(null);

const [dateRange, setDateRange] = useTimeframeState({
enableQueryParams: true,
Expand All @@ -98,6 +101,7 @@ export function IncidentList({
limit: incidentsPagination.limit,
offset: incidentsPagination.offset,
sorting: incidentsSorting[0],
searchCel: searchCel,
filterCel: filterCel,
timeFrame: dateRange,
});
Expand Down Expand Up @@ -227,14 +231,44 @@ export function IncidentList({
limit: DEFAULT_INCIDENTS_PAGE_SIZE,
offset: 0,
});
setSearchCel("");
setClearFiltersToken(uuidV4());
};

useEffect(() => {
setIncidentsPagination((current) => ({
...current,
offset: 0,
}));
}, [filterCel, searchCel]);

function renderIncidents() {
if (incidentsLoading) {
return <KeepLoader></KeepLoader>;
}

const hasFilterCel = !!filterCel;
const hasSearchCel = !!searchCel;
const showSearchEmptyState =
incidents?.items.length === 0 && hasSearchCel;
const showFilterEmptyState =
incidents?.items.length === 0 && hasFilterCel && !hasSearchCel;

if (showSearchEmptyState) {
return (
<div className="flex-1 flex items-center h-full w-full">
<div className="flex flex-col justify-center items-center w-full p-4">
<EmptyStateCard
noCard
title="No Incidents Matching Your CEL Query"
description="Check your CEL query and try again"
icon={MagnifyingGlassIcon}
/>
</div>
</div>
);
}

if (incidents && incidents.items.length > 0) {
return (
<IncidentsTable
Expand All @@ -253,7 +287,7 @@ export function IncidentList({
return <IncidentsNotFoundPlaceholder />;
}

if (facetsCel && incidents?.items.length === 0) {
if (facetsCel && showFilterEmptyState) {
return (
<IncidentsNotFoundForFiltersPlaceholder
onClearFilters={handleClearFilters}
Expand Down Expand Up @@ -333,20 +367,30 @@ export function IncidentList({
<IncidentListError incidentError={incidentsError} />
) : null}
{incidentsError ? null : (
<div className="flex flex-row gap-5">
<FacetsPanelServerSide
className="mt-14"
entityName={"incidents"}
facetsConfig={facetsConfig}
facetOptionsCel={facetsCel}
usePropertyPathsSuggestions={true}
clearFiltersToken={clearFiltersToken}
initialFacetsData={initialFacetsData}
onCelChange={setFilterCel}
revalidationToken={filterRevalidationToken}
<div className="flex flex-col gap-3">
<AlertsRulesBuilder
key={clearFiltersToken || "default"}
defaultQuery=""
entityName="incidents"
showSave={false}
showSqlImport={false}
shouldSetQueryParam={false}
onCelChanges={setSearchCel}
/>
<div className="flex flex-col gap-5 flex-1 min-w-0">
{renderIncidents()}
<div className="flex flex-row gap-5">
<FacetsPanelServerSide
entityName={"incidents"}
facetsConfig={facetsConfig}
facetOptionsCel={facetsCel}
usePropertyPathsSuggestions={true}
clearFiltersToken={clearFiltersToken}
initialFacetsData={initialFacetsData}
onCelChange={setFilterCel}
revalidationToken={filterRevalidationToken}
/>
<div className="flex flex-col gap-5 flex-1 min-w-0">
{renderIncidents()}
</div>
</div>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface IncidentsTableDataQuery {
limit: number;
offset: number;
sorting: { id: string; desc: boolean };
searchCel: string | null;
filterCel: string | null;
timeFrame: TimeFrameV2 | null;
}
Expand Down Expand Up @@ -129,25 +130,38 @@ export const useIncidentsTableData = (query: IncidentsTableDataQuery) => {
}, [JSON.stringify(query)]);

const mainCelQuery = useMemo(() => {
const filterArray = ["is_candidate == false", dateRangeCel];
if (query.searchCel === null) {
return null;
}

return filterArray.filter(Boolean).join(" && ");
}, [dateRangeCel]);
const filterArray = ["is_candidate == false", query.searchCel, dateRangeCel];
return filterArray
.filter(Boolean)
.map((cel) => `(${cel})`)
.join(" && ");
}, [dateRangeCel, query.searchCel]);

useEffect(() => {
if (query.filterCel === null) {
if (query.filterCel === null || mainCelQuery === null) {
return;
}

const filterCel = query.filterCel ? `(${query.filterCel})` : "";
setIncidentsQueryState({
candidate: null,
predicted: null,
limit: query.limit,
offset: query.offset,
sorting: query.sorting,
cel: [mainCelQuery, query.filterCel].filter(Boolean).join(" && "),
cel: [mainCelQuery, filterCel].filter(Boolean).join(" && "),
});
}, [query.sorting, query.filterCel, query.limit, query.offset, mainCelQuery]);
}, [
query.sorting,
query.filterCel,
query.limit,
query.offset,
mainCelQuery,
]);

const {
data: paginatedIncidentsFromHook,
Expand Down Expand Up @@ -214,7 +228,7 @@ export const useIncidentsTableData = (query: IncidentsTableDataQuery) => {
isEmptyState: defaultIncidents?.count === 0,
predictedIncidents,
isPredictedLoading,
facetsCel: mainCelQuery,
facetsCel: mainCelQuery ?? "",
incidentChangeToken,
incidentsError,
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// TODO: Separate or fix naming — this file powers CEL search for both alerts and incidents
// (via entityName). Consider a shared CelRulesBuilder or entity-specific wrappers later.

import { useCallback, useEffect, useRef, useState } from "react";
import Modal from "@/components/ui/Modal";
import { Button, Textarea } from "@tremor/react";
Expand Down Expand Up @@ -37,6 +40,13 @@ const staticOptions = [
{ value: 'message.contains("CPU")', label: 'message.contains("CPU")' },
];

const incidentStaticOptions = [
{ value: 'severity == "critical"', label: 'severity == "critical"' },
{ value: 'status == "firing"', label: 'status == "firing"' },
{ value: 'name.contains("database")', label: 'name.contains("database")' },
{ value: 'assignee != ""', label: 'assignee != ""' },
];

const CustomOption = (props: any) => {
return (
<components.Option {...props}>
Expand Down Expand Up @@ -145,6 +155,8 @@ type AlertsRulesBuilderProps = {
minimal?: boolean;
showToast?: boolean;
shouldSetQueryParam?: boolean;
/** @todo shared with incidents — see file-level TODO about splitting/renaming */
entityName?: "alerts" | "incidents";
};

const SQL_QUERY_PLACEHOLDER = `SELECT *
Expand Down Expand Up @@ -191,6 +203,7 @@ export const AlertsRulesBuilder = ({
showToast = false,
shouldSetQueryParam = true,
onCelChanges,
entityName = "alerts",
}: AlertsRulesBuilderProps) => {
const router = useRouter();
const pathname = usePathname();
Expand All @@ -199,7 +212,15 @@ export const AlertsRulesBuilder = ({

const { deletePreset } = usePresetActions();

const { data: alertFields } = useFacetPotentialFields("alerts");
const { data: alertFields } = useFacetPotentialFields(entityName);
const staticCelOptions =
entityName === "incidents" ? incidentStaticOptions : staticOptions;
const celPlaceholder =
entityName === "incidents"
? 'Use CEL to filter incidents e.g. name.contains("database").'
: 'Use CEL to filter your alerts e.g. source.contains("kibana").';
const celInputId =
entityName === "incidents" ? "incidents-cel-input" : "alerts-cel-input";

const [isGUIOpen, setIsGUIOpen] = useState(false);
const [isImportSQLOpen, setImportSQLOpen] = useState(false);
Expand Down Expand Up @@ -366,8 +387,8 @@ export const AlertsRulesBuilder = ({
<div className="flex-grow relative" ref={wrapperRef}>
<div className="relative">
<CelInput
id="alerts-cel-input"
placeholder='Use CEL to filter your alerts e.g. source.contains("kibana").'
id={celInputId}
placeholder={celPlaceholder}
value={celRules}
fieldsForSuggestions={alertFields}
onValueChange={setCELRules}
Expand All @@ -380,7 +401,7 @@ export const AlertsRulesBuilder = ({
{showSuggestions && (
<div className="absolute z-10 w-full">
<Select
options={staticOptions}
options={staticCelOptions}
onChange={handleSelectChange}
menuIsOpen={true}
components={
Expand Down
5 changes: 5 additions & 0 deletions keep/api/core/incidents.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from keep.api.models.incident import IncidentSorting
from keep.api.models.query import SortOptionsDto
from keep.api.core.cel_to_sql.ast_nodes import DataType
from keep.api.utils.cel_utils import preprocess_cel_expression

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -204,6 +205,7 @@ def __build_base_incident_query(
is_visible_filter_present = False

if cel:
cel = preprocess_cel_expression(cel)
cel_to_sql_result = cel_to_sql_instance.convert_to_sql_str_v2(cel)
sql_filter = cel_to_sql_result.sql
involved_fields = cel_to_sql_result.involved_fields
Expand Down Expand Up @@ -568,6 +570,9 @@ def get_incident_facets_data(
else:
facets = static_facets

if facet_options_query and facet_options_query.cel:
facet_options_query.cel = preprocess_cel_expression(facet_options_query.cel)

def base_query_factory(
facet_property_path: str,
involved_fields: PropertyMetadataInfo,
Expand Down
18 changes: 17 additions & 1 deletion keep/api/utils/cel_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,20 @@ def replace_matched(match):
pattern, replace_matched, cel_expression, flags=re.IGNORECASE
)

return modified_expression
known_severities = {severity.value.lower(): severity.order for severity in AlertSeverity}
remaining_pattern = (
r'(\bseverity\b)\s*([=><!]=?)\s*(?:"([^"]*)"|\'([^\']*)\'|(\w+))'
)

def replace_remaining(match):
value = (match.group(3) or match.group(4) or match.group(5) or "").lower()
order = known_severities.get(value)
if order is not None:
return f"{match.group(1)} {match.group(2)} {order}"
if value.isdigit():
return match.group(0)
return "false"

return re.sub(
remaining_pattern, replace_remaining, modified_expression, flags=re.IGNORECASE
)
Loading