diff --git a/keep-ui/app/(keep)/rules/CorrelationSidebar/AlertsFoundBadge.tsx b/keep-ui/app/(keep)/rules/CorrelationSidebar/AlertsFoundBadge.tsx index 93cd6a0cc7..3bf7467293 100644 --- a/keep-ui/app/(keep)/rules/CorrelationSidebar/AlertsFoundBadge.tsx +++ b/keep-ui/app/(keep)/rules/CorrelationSidebar/AlertsFoundBadge.tsx @@ -3,12 +3,14 @@ import { AlertDto } from "@/entities/alerts/model"; import { DynamicImageProviderIcon } from "@/components/ui"; type AlertsFoundBadgeProps = { + totalAlertsFound: number; alertsFound: AlertDto[]; isLoading: boolean; role: "ruleCondition" | "correlationRuleConditions"; }; export const AlertsFoundBadge = ({ + totalAlertsFound, alertsFound, isLoading, role, @@ -17,15 +19,15 @@ export const AlertsFoundBadge = ({ if (role === "ruleCondition") { return ( <> - {alertsFound.length} alert{alertsFound.length > 1 ? "s" : ""} were - found matching this condition + {totalAlertsFound} alert{totalAlertsFound > 1 ? "s" : ""} were found + matching this condition ); } return ( <> - {alertsFound.length} alert{alertsFound.length > 1 ? "s" : ""} were found + {totalAlertsFound} alert{totalAlertsFound > 1 ? "s" : ""} were found matching correlation rule conditions ); @@ -39,7 +41,7 @@ export const AlertsFoundBadge = ({ return "No alerts were found with these correlation rule conditions. Please try something else."; } - if (alertsFound.length === 0) { + if (totalAlertsFound === 0) { return ( {isLoading ? "Getting your alerts..." : getNotFoundText()} diff --git a/keep-ui/app/(keep)/rules/CorrelationSidebar/CorrelationSidebarBody.tsx b/keep-ui/app/(keep)/rules/CorrelationSidebar/CorrelationSidebarBody.tsx index 4e75371920..634cff5047 100644 --- a/keep-ui/app/(keep)/rules/CorrelationSidebar/CorrelationSidebarBody.tsx +++ b/keep-ui/app/(keep)/rules/CorrelationSidebar/CorrelationSidebarBody.tsx @@ -10,13 +10,13 @@ import { Link } from "@/components/ui"; import { ArrowUpRightIcon } from "@heroicons/react/24/outline"; import { useRules } from "utils/hooks/useRules"; import { useRouter, useSearchParams } from "next/navigation"; -import { useSearchAlerts } from "utils/hooks/useSearchAlerts"; import { AlertsFoundBadge } from "./AlertsFoundBadge"; import { useApi } from "@/shared/lib/hooks/useApi"; import { useConfig } from "@/utils/hooks/useConfig"; import { showErrorToast } from "@/shared/ui"; import { CorrelationFormType } from "./types"; import { TIMEFRAME_UNITS_TO_SECONDS } from "./timeframe-constants"; +import { useMatchingAlerts } from "./useMatchingAlerts"; type CorrelationSidebarBodyProps = { toggle: VoidFunction; @@ -46,10 +46,11 @@ export const CorrelationSidebarBody = ({ const searchParams = useSearchParams(); const selectedId = searchParams ? searchParams.get("id") : null; - const { data: alertsFound = [], isLoading } = useSearchAlerts({ - query: methods.watch("query"), - timeframe: timeframeInSeconds, - }); + const { + data: alertsFound = [], + totalCount: totalAlertsFound, + isLoading, + } = useMatchingAlerts(methods.watch("query")); const [isCalloutShown, setIsCalloutShown] = useLocalStorage( "correlation-callout", @@ -85,7 +86,7 @@ export const CorrelationSidebarBody = ({ celQuery: formatQuery(query, "cel"), timeframeInSeconds, timeUnit: timeUnit, - groupingCriteria: alertsFound.length ? groupedAttributes : [], + groupingCriteria: totalAlertsFound ? groupedAttributes : [], requireApprove: requireApprove, resolveOn: resolveOn, createOn: createOn, @@ -169,8 +170,9 @@ export const CorrelationSidebarBody = ({
- {alertsFound.length > 0 && ( + {totalAlertsFound > 0 && ( [ @@ -50,7 +50,7 @@ const OPERATORS_FORCE_TYPE_CAST = { "<=": "number", "<": "number", ">": "number", -} +}; const DEFAULT_FIELDS: QueryField[] = [ { name: "source", label: "source", datatype: "text" }, @@ -117,9 +117,13 @@ const Field = ({ }; const castValueToOperationType = (value: string) => { - const castTo: string = get(OPERATORS_FORCE_TYPE_CAST, ruleField.operator, "text"); + const castTo: string = get( + OPERATORS_FORCE_TYPE_CAST, + ruleField.operator, + "text" + ); return castTo === "number" ? Number(value) : value; - } + }; return (
@@ -158,7 +162,9 @@ const Field = ({ {isValueEnabled && (
onFieldChange("value", castValueToOperationType(newValue))} + onValueChange={(newValue) => + onFieldChange("value", castValueToOperationType(newValue)) + } defaultValue={ruleField.value} required error={!ruleField.value} @@ -279,10 +285,11 @@ export const RuleFields = ({ ? TIMEFRAME_UNITS_TO_SECONDS[watch("timeUnit")](+watch("timeAmount")) : 0; - const { data: alertsFound = [], isLoading } = useSearchAlerts({ - query: { combinator: "and", rules: ruleFields }, - timeframe: timeframeInSeconds, - }); + const { + data: alertsFound = [], + totalCount: totalAlertsFound, + isLoading, + } = useMatchingAlerts({ combinator: "and", rules: ruleFields }); return (
@@ -346,6 +353,7 @@ export const RuleFields = ({
{ + it("should convert a LogicalNode with AND operator", () => { + const logicalNode: CelAst.LogicalNode = { + node_type: "LogicalNode", + operator: CelAst.LogicalNodeOperator.AND, + left: { + node_type: "ComparisonNode", + first_operand: { path: ["field1"] } as CelAst.PropertyAccessNode, + operator: CelAst.ComparisonNodeOperator.EQ, + second_operand: { value: "value1" }, + } as CelAst.ComparisonNode, + right: { + node_type: "ComparisonNode", + first_operand: { path: ["field2"] } as CelAst.PropertyAccessNode, + operator: CelAst.ComparisonNodeOperator.NE, + second_operand: { value: "value2" }, + } as CelAst.ComparisonNode, + }; + + const result = convertCelAstToQueryBuilderAst(logicalNode); + + expect(result).toEqual({ + combinator: "and", + rules: [ + { + field: "field1", + operator: "=", + value: "value1", + id: expect.any(String), + }, + { + field: "field2", + operator: "!=", + value: "value2", + id: expect.any(String), + }, + ], + }); + }); + + it("should convert a LogicalNode with OR operator", () => { + const logicalNode: CelAst.LogicalNode = { + node_type: "LogicalNode", + operator: CelAst.LogicalNodeOperator.OR, + left: { + node_type: "ComparisonNode", + first_operand: { path: ["field1"] } as CelAst.PropertyAccessNode, + operator: CelAst.ComparisonNodeOperator.GT, + second_operand: { value: 10 }, + } as CelAst.ComparisonNode, + right: { + node_type: "ComparisonNode", + first_operand: { path: ["field2"] } as CelAst.PropertyAccessNode, + operator: CelAst.ComparisonNodeOperator.LT, + second_operand: { value: 20 }, + } as CelAst.ComparisonNode, + }; + + const result = convertCelAstToQueryBuilderAst(logicalNode); + + expect(result).toEqual({ + combinator: "or", + rules: [ + { + field: "field1", + operator: ">", + value: 10, + id: expect.any(String), + }, + { + field: "field2", + operator: "<", + value: 20, + id: expect.any(String), + }, + ], + }); + }); + + it("should convert a ComparisonNode with EQ operator and null value to 'null' operator", () => { + const comparisonNode: CelAst.ComparisonNode = { + node_type: "ComparisonNode", + first_operand: { path: ["field1"] } as CelAst.PropertyAccessNode, + operator: CelAst.ComparisonNodeOperator.EQ, + second_operand: { value: null }, + }; + + const result = convertCelAstToQueryBuilderAst(comparisonNode); + + expect(result).toEqual({ + combinator: "and", + rules: [ + { + field: "field1", + operator: "null", + id: expect.any(String), + }, + ], + }); + }); + + it.each([ + [CelAst.ComparisonNodeOperator.EQ, "="], + [CelAst.ComparisonNodeOperator.NE, "!="], + [CelAst.ComparisonNodeOperator.CONTAINS, "contains"], + [CelAst.ComparisonNodeOperator.STARTS_WITH, "beginsWith"], + [CelAst.ComparisonNodeOperator.ENDS_WITH, "endsWith"], + ])( + "should convert %s operator to %s", + (celOperator, queryBuilderOperator) => { + const comparisonNode: CelAst.ComparisonNode = { + node_type: "ComparisonNode", + first_operand: { + path: ["field1", "field2"], + } as CelAst.PropertyAccessNode, + operator: celOperator, + second_operand: { value: "testValue" }, + }; + + const result = convertCelAstToQueryBuilderAst(comparisonNode); + + expect(result).toEqual({ + combinator: "and", + rules: [ + { + field: "field1.field2", + operator: queryBuilderOperator, + value: "testValue", + id: expect.any(String), + }, + ], + }); + } + ); + + it("should convert a ComparisonNode with NE operator and null value to 'notNull' operator", () => { + const comparisonNode: CelAst.ComparisonNode = { + node_type: "ComparisonNode", + first_operand: { + path: ["field1", "field2"], + } as CelAst.PropertyAccessNode, + operator: CelAst.ComparisonNodeOperator.NE, + second_operand: { value: null }, + }; + + const result = convertCelAstToQueryBuilderAst(comparisonNode); + + expect(result).toEqual({ + combinator: "and", + rules: [ + { + field: "field1.field2", + operator: "notNull", + id: expect.any(String), + }, + ], + }); + }); + + it("should convert a UnaryNode with NOT IN operator to notIn opearator", () => { + const unaryNode: CelAst.UnaryNode = { + node_type: "UnaryNode", + operator: CelAst.UnaryNodeOperator.NOT, + operand: { + node_type: "ComparisonNode", + first_operand: { path: ["field1"] } as CelAst.PropertyAccessNode, + operator: CelAst.ComparisonNodeOperator.IN, + second_operand: { value: [1, 2, 3] }, + } as CelAst.ComparisonNode, + }; + + const result = convertCelAstToQueryBuilderAst(unaryNode); + + expect(result).toEqual({ + combinator: "and", + rules: [ + { + field: "field1", + operator: "notIn", + value: [1, 2, 3], + id: expect.any(String), + }, + ], + }); + }); + + it.each([ + [CelAst.ComparisonNodeOperator.CONTAINS, "doesNotContain"], + [CelAst.ComparisonNodeOperator.STARTS_WITH, "doesNotBeginWith"], + [CelAst.ComparisonNodeOperator.ENDS_WITH, "doesNotEndWith"], + ])( + "should convert unary not with %s operator to %s operator", + (celOperator, queryBuilderOperator) => { + const unaryNode: CelAst.UnaryNode = { + node_type: "UnaryNode", + operator: CelAst.UnaryNodeOperator.NOT, + operand: { + node_type: "ComparisonNode", + first_operand: { path: ["field1"] } as CelAst.PropertyAccessNode, + operator: celOperator, + second_operand: { value: "testValue" }, + } as CelAst.ComparisonNode, + }; + + const result = convertCelAstToQueryBuilderAst(unaryNode); + + expect(result).toEqual({ + combinator: "and", + rules: [ + { + field: "field1", + operator: queryBuilderOperator, + value: "testValue", + id: expect.any(String), + }, + ], + }); + } + ); + + it("should throw an error for unsupported node type", () => { + const unsupportedNode: any = { + node_type: "UnsupportedNode", + }; + + expect(() => convertCelAstToQueryBuilderAst(unsupportedNode)).toThrow( + "Unsupported node type: UnsupportedNode" + ); + }); +}); diff --git a/keep-ui/app/(keep)/rules/CorrelationSidebar/convert-cel-ast-to-query-builder-ast/convert-cel-ast-to-query-builder-ast.function.ts b/keep-ui/app/(keep)/rules/CorrelationSidebar/convert-cel-ast-to-query-builder-ast/convert-cel-ast-to-query-builder-ast.function.ts new file mode 100644 index 0000000000..f255f236ce --- /dev/null +++ b/keep-ui/app/(keep)/rules/CorrelationSidebar/convert-cel-ast-to-query-builder-ast/convert-cel-ast-to-query-builder-ast.function.ts @@ -0,0 +1,176 @@ +import { v4 as uuidv4 } from "uuid"; +import { CelAst } from "@/utils/cel-ast"; +import { DefaultRuleGroupType } from "react-querybuilder"; + +function mapOperator(op: string): string { + switch (op) { + case "==": + return "="; + case "!=": + return "!="; + case ">": + return ">"; + case "<": + return "<"; + case ">=": + return ">="; + case "<=": + return "<="; + case "contains": + return "contains"; + case "startsWith": + return "beginsWith"; + case "endsWith": + return "endsWith"; + default: + return op; + } +} + +function visitUnaryNode(node: CelAst.UnaryNode): DefaultRuleGroupType { + if (node.operator !== CelAst.UnaryNodeOperator.NOT) { + throw new Error("Unsupported operator: " + node.operator); + } + + let operand = (node as CelAst.UnaryNode).operand; + + if (operand?.node_type === "ParenthesisNode") { + operand = (operand as CelAst.ParenthesisNode).expression; + } + + if (operand?.node_type === "ComparisonNode") { + const field = ( + (operand as CelAst.ComparisonNode) + .first_operand as CelAst.PropertyAccessNode + )?.path.join("."); + const value = ( + (operand as CelAst.ComparisonNode).second_operand as CelAst.ConstantNode + )?.value; + let operator: string = ""; + switch ((operand as CelAst.ComparisonNode).operator) { + case CelAst.ComparisonNodeOperator.IN: + operator = "notIn"; + break; + case CelAst.ComparisonNodeOperator.CONTAINS: + operator = "doesNotContain"; + break; + case CelAst.ComparisonNodeOperator.STARTS_WITH: + operator = "doesNotBeginWith"; + break; + case CelAst.ComparisonNodeOperator.ENDS_WITH: + operator = "doesNotEndWith"; + break; + } + + return { + combinator: "and", + rules: [ + { + field, + operator, + value, + id: uuidv4(), + } as any, + ], + }; + } + + throw new Error("UnaryNode with unknown operand: " + node.node_type); +} + +function visitComparisonNode( + node: CelAst.ComparisonNode +): DefaultRuleGroupType { + const field = ( + (node as CelAst.ComparisonNode).first_operand as CelAst.PropertyAccessNode + )?.path.join("."); + const operator = (node as CelAst.ComparisonNode).operator; + const value = ( + (node as CelAst.ComparisonNode).second_operand as CelAst.ConstantNode + )?.value; + let queryBuilderField = null; + + if (operator == CelAst.ComparisonNodeOperator.NE && value == null) { + queryBuilderField = { + field, + operator: "notNull", + id: uuidv4(), + } as any; + } else if (operator == CelAst.ComparisonNodeOperator.EQ && value == null) { + queryBuilderField = { + field, + operator: "null", + id: uuidv4(), + } as any; + } else { + queryBuilderField = { + field, + operator: mapOperator((node as CelAst.ComparisonNode).operator), + value, + id: uuidv4(), + } as any; + } + + return { + combinator: "and", + rules: [queryBuilderField], + }; +} + +function visitLogicalNode(node: CelAst.LogicalNode): DefaultRuleGroupType { + const left = convertCelAstToQueryBuilderAst( + ((node as CelAst.LogicalNode).left as any).expression ?? + (node as CelAst.LogicalNode).left + ); + const right = convertCelAstToQueryBuilderAst( + ((node as CelAst.LogicalNode).right as any).expression ?? + (node as CelAst.LogicalNode).right + ); + const combinator = + (node as CelAst.LogicalNode).operator === CelAst.LogicalNodeOperator.OR + ? "or" + : "and"; + + const rules = []; + + if (left.combinator == combinator || left.rules.length <= 1) { + rules.push(...left.rules); + } else { + rules.push(left); + } + + if (right.combinator == combinator || right.rules.length <= 1) { + rules.push(...right.rules); + } else { + rules.push(right); + } + + return { + combinator, + rules: rules, + }; +} + +export function convertCelAstToQueryBuilderAst( + node: CelAst.Node +): DefaultRuleGroupType { + switch (node.node_type) { + case "LogicalNode": { + return visitLogicalNode(node as CelAst.LogicalNode); + } + case "ParenthesisNode": { + return convertCelAstToQueryBuilderAst( + (node as CelAst.ParenthesisNode).expression + ); + } + case "ComparisonNode": { + return visitComparisonNode(node as CelAst.ComparisonNode); + } + case "UnaryNode": { + return visitUnaryNode(node as CelAst.UnaryNode); + } + + default: + throw new Error(`Unsupported node type: ${node.node_type}`); + } +} diff --git a/keep-ui/app/(keep)/rules/CorrelationSidebar/index.tsx b/keep-ui/app/(keep)/rules/CorrelationSidebar/index.tsx index 0a57ca4903..e2dc3d378e 100644 --- a/keep-ui/app/(keep)/rules/CorrelationSidebar/index.tsx +++ b/keep-ui/app/(keep)/rules/CorrelationSidebar/index.tsx @@ -1,9 +1,18 @@ -import { Fragment } from "react"; -import { Dialog, Transition } from "@headlessui/react"; +import { useMemo } from "react"; import { CorrelationSidebarHeader } from "./CorrelationSidebarHeader"; import { CorrelationSidebarBody } from "./CorrelationSidebarBody"; import { CorrelationFormType } from "./types"; import { Drawer } from "@/shared/ui/Drawer"; +import { Rule } from "@/utils/hooks/useRules"; +import { DefaultRuleGroupType, parseCEL } from "react-querybuilder"; +import { convertCelAstToQueryBuilderAst } from "./convert-cel-ast-to-query-builder-ast/convert-cel-ast-to-query-builder-ast.function"; + +const TIMEFRAME_UNITS_FROM_SECONDS = { + seconds: (amount: number) => amount, + minutes: (amount: number) => amount / 60, + hours: (amount: number) => amount / 3600, + days: (amount: number) => amount / 86400, +} as const; export const DEFAULT_CORRELATION_FORM_VALUES: CorrelationFormType = { name: "", @@ -36,22 +45,72 @@ export const DEFAULT_CORRELATION_FORM_VALUES: CorrelationFormType = { type CorrelationSidebarProps = { isOpen: boolean; toggle: VoidFunction; + selectedRule?: Rule; defaultValue?: CorrelationFormType; }; export const CorrelationSidebar = ({ isOpen, toggle, - defaultValue = DEFAULT_CORRELATION_FORM_VALUES, -}: CorrelationSidebarProps) => ( - -
- - -
-
-); + selectedRule, +}: CorrelationSidebarProps) => { + const correlationFormFromRule: CorrelationFormType = useMemo(() => { + if (selectedRule) { + const query = convertCelAstToQueryBuilderAst( + selectedRule.definition_cel_ast + ); + const anyCombinator = query.rules?.some((rule) => "combinator" in rule); + + const queryInGroup: DefaultRuleGroupType = { + ...query, + rules: anyCombinator + ? query.rules + : [ + { + combinator: "and", + rules: query.rules, + }, + ], + }; + + const timeunit = selectedRule.timeunit ?? "seconds"; + + return { + name: selectedRule.name, + description: selectedRule.group_description ?? "", + timeAmount: TIMEFRAME_UNITS_FROM_SECONDS[timeunit]( + selectedRule.timeframe + ), + timeUnit: timeunit, + groupedAttributes: selectedRule.grouping_criteria, + requireApprove: selectedRule.require_approve, + resolveOn: selectedRule.resolve_on, + createOn: selectedRule.create_on, + query: queryInGroup, + incidents: selectedRule.incidents, + incidentNameTemplate: selectedRule.incident_name_template || "", + incidentPrefix: selectedRule.incident_prefix || "", + multiLevel: selectedRule.multi_level, + multiLevelPropertyName: selectedRule.multi_level_property_name || "", + }; + } + + return DEFAULT_CORRELATION_FORM_VALUES; + }, [selectedRule]); + + return ( + +
+ + +
+
+ ); +}; diff --git a/keep-ui/app/(keep)/rules/CorrelationSidebar/useMatchingAlerts.ts b/keep-ui/app/(keep)/rules/CorrelationSidebar/useMatchingAlerts.ts new file mode 100644 index 0000000000..1dce2e73e1 --- /dev/null +++ b/keep-ui/app/(keep)/rules/CorrelationSidebar/useMatchingAlerts.ts @@ -0,0 +1,21 @@ +import { AlertsQuery, useAlerts } from "@/entities/alerts/model"; +import { useDebouncedValue } from "@/utils/hooks/useDebouncedValue"; +import { useEffect, useState } from "react"; +import { formatQuery, RuleGroupType } from "react-querybuilder"; + +export function useMatchingAlerts(rules: RuleGroupType | undefined) { + const { useLastAlerts } = useAlerts(); + const [debouncedRules] = useDebouncedValue(rules, 2000); + const [alertsQuery, setAlertsQuery] = useState(); + useEffect(() => { + if (rules) { + setAlertsQuery({ + cel: formatQuery(debouncedRules as RuleGroupType, "cel"), + limit: 1000, + offset: 0, + }); + } + }, [debouncedRules]); + + return useLastAlerts(alertsQuery); +} diff --git a/keep-ui/app/(keep)/rules/CorrelationTable.tsx b/keep-ui/app/(keep)/rules/CorrelationTable.tsx index 78df736abb..88835ba798 100644 --- a/keep-ui/app/(keep)/rules/CorrelationTable.tsx +++ b/keep-ui/app/(keep)/rules/CorrelationTable.tsx @@ -2,7 +2,6 @@ import { Badge, Button, Card, - Icon, Table, TableBody, TableCell, @@ -10,34 +9,21 @@ import { TableHeaderCell, TableRow, } from "@tremor/react"; -import { useEffect, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { Rule } from "utils/hooks/useRules"; -import { - CorrelationSidebar, - DEFAULT_CORRELATION_FORM_VALUES, -} from "./CorrelationSidebar"; +import { CorrelationSidebar } from "./CorrelationSidebar"; import { createColumnHelper, flexRender, getCoreRowModel, useReactTable, } from "@tanstack/react-table"; -import { DefaultRuleGroupType } from "react-querybuilder"; -import { parseCEL } from "react-querybuilder/parseCEL"; import { useRouter, useSearchParams } from "next/navigation"; -import { FormattedQueryCell } from "./FormattedQueryCell"; import { DeleteRuleCell } from "./CorrelationSidebar/DeleteRule"; -import { CorrelationFormType } from "./CorrelationSidebar/types"; import { PageSubtitle, PageTitle } from "@/shared/ui"; import { PlusIcon } from "@heroicons/react/20/solid"; import { GroupedByCell } from "./GroupedByCel"; - -const TIMEFRAME_UNITS_FROM_SECONDS = { - seconds: (amount: number) => amount, - minutes: (amount: number) => amount / 60, - hours: (amount: number) => amount / 3600, - days: (amount: number) => amount / 86400, -} as const; +import CelInput from "@/features/cel-input/cel-input"; const columnHelper = createColumnHelper(); @@ -51,47 +37,6 @@ export const CorrelationTable = ({ rules }: CorrelationTableProps) => { const selectedId = searchParams ? searchParams.get("id") : null; const selectedRule = rules.find((rule) => rule.id === selectedId); - const correlationFormFromRule: CorrelationFormType = useMemo(() => { - if (selectedRule) { - const query = parseCEL(selectedRule.definition_cel); - const anyCombinator = query.rules.some((rule) => "combinator" in rule); - - const queryInGroup: DefaultRuleGroupType = { - ...query, - rules: anyCombinator - ? query.rules - : [ - { - combinator: "and", - rules: query.rules, - }, - ], - }; - - const timeunit = selectedRule.timeunit ?? "seconds"; - - return { - name: selectedRule.name, - description: selectedRule.group_description ?? "", - timeAmount: TIMEFRAME_UNITS_FROM_SECONDS[timeunit]( - selectedRule.timeframe - ), - timeUnit: timeunit, - groupedAttributes: selectedRule.grouping_criteria, - requireApprove: selectedRule.require_approve, - resolveOn: selectedRule.resolve_on, - createOn: selectedRule.create_on, - query: queryInGroup, - incidents: selectedRule.incidents, - incidentNameTemplate: selectedRule.incident_name_template || "", - incidentPrefix: selectedRule.incident_prefix || "", - multiLevel: selectedRule.multi_level, - multiLevelPropertyName: selectedRule.multi_level_property_name || "", - }; - } - - return DEFAULT_CORRELATION_FORM_VALUES; - }, [selectedRule]); const [isRuleCreation, setIsRuleCreation] = useState(false); @@ -141,9 +86,20 @@ export const CorrelationTable = ({ rules }: CorrelationTableProps) => { }), columnHelper.accessor("definition_cel", { header: "Description", - cell: (context) => ( - - ), + cell: (context) => { + let cel = context.getValue(); + + return ( +
{ + e.preventDefault(); + e.stopPropagation(); + }} + > + +
+ ); + }, }), columnHelper.accessor("grouping_criteria", { header: "Grouped by", @@ -233,7 +189,7 @@ export const CorrelationTable = ({ rules }: CorrelationTableProps) => { )}
diff --git a/keep-ui/app/(keep)/rules/FormattedQueryCell.tsx b/keep-ui/app/(keep)/rules/FormattedQueryCell.tsx deleted file mode 100644 index 6f54ddfe54..0000000000 --- a/keep-ui/app/(keep)/rules/FormattedQueryCell.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { PlusIcon } from "@radix-ui/react-icons"; -import { Badge, Icon } from "@tremor/react"; -import { Fragment } from "react"; -import { RuleGroupArray, RuleGroupType, RuleType } from "react-querybuilder"; -import * as Tooltip from "@radix-ui/react-tooltip"; - -type FormattedQueryCellProps = { - query: RuleGroupType; -}; - -export const FormattedQueryCell = ({ query }: FormattedQueryCellProps) => { - let displayedRules: any[] = query.rules; - let rulesInTooltip: any[] = []; - - if (query.rules.length > 2) { - displayedRules = query.rules.slice(0, 1); - rulesInTooltip = query.rules.slice(1); - } - - // tb: this is a patch to make it work, needs refactor - const anyCombinator = query.rules.some((rule) => "combinator" in rule); - - function renderRules( - rules: RuleGroupArray< - RuleGroupType, string>, - RuleType - > - ): JSX.Element[] | JSX.Element { - return anyCombinator ? ( - rules.map((group, groupI) => ( - -
- {"combinator" in group - ? group.rules.map((rule, ruleI) => ( - - {"field" in rule ? ( - - {rule.field}{" "} - {rule.operator} - - {rule.value} - - - ) : undefined} - - )) - : null} -
- {rules.length !== groupI + 1 && ( - - )} -
- )) - ) : ( - -
- {rules.map((rule, ruleI) => { - return ( - - {"field" in rule ? ( - - {rule.field}{" "} - {rule.operator} - {rule.value && {rule.value}} - - ) : undefined} - - ); - })} -
-
- ); - } - - return ( -
- {renderRules(displayedRules)} - {rulesInTooltip.length > 0 && ( - <> - - - - - - {rulesInTooltip.length} more - - - - -
- {renderRules(rulesInTooltip)} -
- -
-
-
-
- - )} -
- ); -}; diff --git a/keep-ui/app/(keep)/rules/flatten-cel-ast.ts b/keep-ui/app/(keep)/rules/flatten-cel-ast.ts new file mode 100644 index 0000000000..daf5e89344 --- /dev/null +++ b/keep-ui/app/(keep)/rules/flatten-cel-ast.ts @@ -0,0 +1,9 @@ +// import { CelAst } from "@/utils/cel-ast"; + +// interface { +// first_operand +// } + +// export function flattenCelAst(celAst:CelAst.Node): any { + +// } diff --git a/keep-ui/entities/alerts/model/useAlerts.ts b/keep-ui/entities/alerts/model/useAlerts.ts index 8ed0624987..21d747e648 100644 --- a/keep-ui/entities/alerts/model/useAlerts.ts +++ b/keep-ui/entities/alerts/model/useAlerts.ts @@ -171,7 +171,7 @@ export const useAlerts = () => { const requestUrl = `/alerts/query`; const swrKey = () => // adding "/alerts/query" so global revalidation works - api.isReady() + api.isReady() && query ? requestUrl + Object.entries(queryToPost) .sort(([fstKey], [scdKey]) => fstKey.localeCompare(scdKey)) @@ -199,9 +199,9 @@ export const useAlerts = () => { data: swrValue.data?.queryResult?.results as AlertDto[], queryTimeInSeconds: swrValue.data?.queryTimeInSeconds, isLoading: swrValue.isLoading || !swrValue.data?.queryResult, - totalCount: swrValue.data?.queryResult?.count, - limit: swrValue.data?.queryResult?.limit, - offset: swrValue.data?.queryResult?.offset, + totalCount: swrValue.data?.queryResult?.count as number, + limit: swrValue.data?.queryResult?.limit as number, + offset: swrValue.data?.queryResult?.offset as number, }; }; diff --git a/keep-ui/features/cel-input/cel-input.tsx b/keep-ui/features/cel-input/cel-input.tsx index 19fb1fa725..aa99422941 100644 --- a/keep-ui/features/cel-input/cel-input.tsx +++ b/keep-ui/features/cel-input/cel-input.tsx @@ -3,6 +3,7 @@ import type { editor } from "monaco-editor"; import { MonacoCelEditor } from "@/shared/ui/MonacoCELEditor"; import { IoSearchOutline } from "react-icons/io5"; import { TrashIcon, XMarkIcon } from "@heroicons/react/24/outline"; +import clsx from "clsx"; interface CelInputProps { id?: string; @@ -15,6 +16,7 @@ interface CelInputProps { onIsValidChange?: (isValid: boolean) => void; placeholder?: string; disabled?: boolean; + readOnly?: boolean; } const CelInput: FC = ({ @@ -27,28 +29,37 @@ const CelInput: FC = ({ onKeyDown, onFocus, placeholder = "Enter value", + readOnly = false, disabled = false, }) => { return ( -
+
{})} onIsValidChange={onIsValidChange} onKeyDown={onKeyDown} onFocus={onFocus} /> - + {!readOnly && ( + + )} {placeholder && !value && (
{placeholder}
)} - {value && ( + {!readOnly && value && (