diff --git a/keep-ui/features/incidents/incident-list/ui/incident-list.tsx b/keep-ui/features/incidents/incident-list/ui/incident-list.tsx index f4d2b40402..8ead9478d5 100644 --- a/keep-ui/features/incidents/incident-list/ui/incident-list.tsx +++ b/keep-ui/features/incidents/incident-list/ui/incident-list.tsx @@ -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, @@ -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"; @@ -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); @@ -74,7 +76,8 @@ export function IncidentList({ DEFAULT_INCIDENTS_SORTING, ]); - const [filterCel, setFilterCel] = useState(null); + const [filterCel, setFilterCel] = useState(""); + const [searchCel, setSearchCel] = useState(null); const [dateRange, setDateRange] = useTimeframeState({ enableQueryParams: true, @@ -98,6 +101,7 @@ export function IncidentList({ limit: incidentsPagination.limit, offset: incidentsPagination.offset, sorting: incidentsSorting[0], + searchCel: searchCel, filterCel: filterCel, timeFrame: dateRange, }); @@ -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 ; } + const hasFilterCel = !!filterCel; + const hasSearchCel = !!searchCel; + const showSearchEmptyState = + incidents?.items.length === 0 && hasSearchCel; + const showFilterEmptyState = + incidents?.items.length === 0 && hasFilterCel && !hasSearchCel; + + if (showSearchEmptyState) { + return ( +
+
+ +
+
+ ); + } + if (incidents && incidents.items.length > 0) { return ( ; } - if (facetsCel && incidents?.items.length === 0) { + if (facetsCel && showFilterEmptyState) { return ( ) : null} {incidentsError ? null : ( -
- + -
- {renderIncidents()} +
+ +
+ {renderIncidents()} +
)} diff --git a/keep-ui/features/incidents/incident-list/ui/useIncidentsTableData.tsx b/keep-ui/features/incidents/incident-list/ui/useIncidentsTableData.tsx index 8b2b975cff..3f2d7f0b5d 100644 --- a/keep-ui/features/incidents/incident-list/ui/useIncidentsTableData.tsx +++ b/keep-ui/features/incidents/incident-list/ui/useIncidentsTableData.tsx @@ -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; } @@ -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, @@ -214,7 +228,7 @@ export const useIncidentsTableData = (query: IncidentsTableDataQuery) => { isEmptyState: defaultIncidents?.count === 0, predictedIncidents, isPredictedLoading, - facetsCel: mainCelQuery, + facetsCel: mainCelQuery ?? "", incidentChangeToken, incidentsError, }; diff --git a/keep-ui/features/presets/presets-manager/ui/alerts-rules-builder.tsx b/keep-ui/features/presets/presets-manager/ui/alerts-rules-builder.tsx index ecc571bc13..e6bc2d91d7 100644 --- a/keep-ui/features/presets/presets-manager/ui/alerts-rules-builder.tsx +++ b/keep-ui/features/presets/presets-manager/ui/alerts-rules-builder.tsx @@ -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"; @@ -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 ( @@ -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 * @@ -191,6 +203,7 @@ export const AlertsRulesBuilder = ({ showToast = false, shouldSetQueryParam = true, onCelChanges, + entityName = "alerts", }: AlertsRulesBuilderProps) => { const router = useRouter(); const pathname = usePathname(); @@ -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); @@ -366,8 +387,8 @@ export const AlertsRulesBuilder = ({