diff --git a/App/FeatureSet/Dashboard/src/Components/LogPipeline/FilterQueryBuilder.tsx b/App/FeatureSet/Dashboard/src/Components/LogPipeline/FilterQueryBuilder.tsx index f2d75e6f27..795f24092f 100644 --- a/App/FeatureSet/Dashboard/src/Components/LogPipeline/FilterQueryBuilder.tsx +++ b/App/FeatureSet/Dashboard/src/Components/LogPipeline/FilterQueryBuilder.tsx @@ -14,6 +14,9 @@ import Dropdown, { DropdownOption, DropdownValue, } from "Common/UI/Components/Dropdown/Dropdown"; +import Modal, { ModalWidth } from "Common/UI/Components/Modal/Modal"; +import Pill from "Common/UI/Components/Pill/Pill"; +import { Blue, Green } from "Common/Types/BrandColors"; import IconProp from "Common/Types/Icon/IconProp"; import ObjectID from "Common/Types/ObjectID"; import BaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel"; @@ -32,6 +35,31 @@ export interface ComponentProps { type LogicalConnector = "AND" | "OR"; +// Field label map for display +const fieldLabels: Record = { + severityText: "Severity", + body: "Log Body", + serviceId: "Service ID", +}; + +const operatorLabels: Record = { + "=": "equals", + "!=": "does not equal", + LIKE: "contains", + IN: "is one of", +}; + +function getFieldLabel(field: string): string { + if (field.startsWith("attributes.")) { + return `attributes.${field.replace("attributes.", "")}`; + } + return fieldLabels[field] || field; +} + +function getOperatorLabel(operator: string): string { + return operatorLabels[operator] || operator; +} + // Parse a filterQuery string into structured conditions function parseFilterQuery(query: string): { conditions: Array; @@ -49,7 +77,6 @@ function parseFilterQuery(query: string): { return defaultResult; } - // Detect connector const connector: LogicalConnector = query.includes(" OR ") ? "OR" : "AND"; const connectorRegex: RegExp = connector === "AND" ? / AND /i : / OR /i; const parts: Array = query.split(connectorRegex); @@ -59,7 +86,6 @@ function parseFilterQuery(query: string): { for (const part of parts) { const trimmed: string = part.trim().replace(/^\(|\)$/g, ""); - // Try to match: field OPERATOR 'value' or field OPERATOR value const likeMatch: RegExpMatchArray | null = trimmed.match( /^(\S+)\s+(LIKE)\s+'([^']*)'$/i, ); @@ -149,8 +175,15 @@ const FilterQueryBuilder: FunctionComponent = ( const [isSaving, setIsSaving] = useState(false); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); - const [successMessage, setSuccessMessage] = useState(""); + const [_successMessage, _setSuccessMessage] = useState(""); const [originalQuery, setOriginalQuery] = useState(""); + const [showModal, setShowModal] = useState(false); + + // Snapshot of conditions/connector when modal opens (for cancel/revert) + const [modalConditions, setModalConditions] = useState< + Array + >([]); + const [modalConnector, setModalConnector] = useState("AND"); const loadModel: () => Promise = useCallback(async (): Promise => { @@ -189,7 +222,7 @@ const FilterQueryBuilder: FunctionComponent = ( setError(""); setSuccessMessage(""); - const query: string = buildFilterQuery(conditions, connector); + const query: string = buildFilterQuery(modalConditions, modalConnector); try { await ModelAPI.updateById({ @@ -197,8 +230,11 @@ const FilterQueryBuilder: FunctionComponent = ( id: props.modelId, data: { filterQuery: query || "" }, }); + setConditions(modalConditions); + setConnector(modalConnector); setOriginalQuery(query); - setSuccessMessage("Filter conditions saved successfully."); + setSuccessMessage("Filter conditions saved."); + setShowModal(false); setTimeout(() => { setSuccessMessage(""); }, 3000); @@ -209,19 +245,31 @@ const FilterQueryBuilder: FunctionComponent = ( } }; - const handleClear: () => void = (): void => { - setConditions([{ field: "severityText", operator: "=", value: "" }]); - setConnector("AND"); + const openModal: () => void = (): void => { + setModalConditions(conditions.map((c: FilterConditionData) => ({ ...c }))); + setModalConnector(connector); + setError(""); + setShowModal(true); }; - const currentQuery: string = buildFilterQuery(conditions, connector); - const hasChanges: boolean = currentQuery !== originalQuery; + const closeModal: () => void = (): void => { + setShowModal(false); + setError(""); + }; const cardTitle: string = props.title || "Filter Conditions"; const cardDescription: string = props.description || "Define which logs this rule applies to. Only logs that match these conditions will be affected. Leave empty to match all logs."; + // Check if there are valid saved conditions + const savedConditions: Array = conditions.filter( + (c: FilterConditionData) => { + return c.field && c.operator && c.value; + }, + ); + const hasConditions: boolean = savedConditions.length > 0; + if (isLoading) { return ( @@ -233,150 +281,242 @@ const FilterQueryBuilder: FunctionComponent = ( } return ( - -
- {error && ( -
- { - setError(""); - }} - /> -
- )} + <> + {/* Read-only card view */} + +
+ {successMessage && ( +
+ { + setSuccessMessage(""); + }} + /> +
+ )} - {successMessage && ( -
- { - setSuccessMessage(""); - }} - /> -
- )} + {hasConditions ? ( +
+ {/* Show connector label */} + {savedConditions.length > 1 && ( +
+ + Match{" "} + + {connector === "AND" + ? "all conditions" + : "any condition"} + + +
+ )} - {/* Connector selector (only show if more than 1 condition) */} - {conditions.length > 1 && ( -
-
- Match -
- { - return opt.value === connector; - })} - onChange={( - value: DropdownValue | Array | null, - ) => { - setConnector( - (value?.toString() as LogicalConnector) || "AND", + {/* Read-only condition list */} +
+ {savedConditions.map( + (condition: FilterConditionData, index: number) => { + return ( +
+ {index > 0 && ( +
+ + {connector} + +
+ )} +
+ + + {getOperatorLabel(condition.operator)} + + +
+
); - }} - /> + }, + )}
-
- )} + ) : ( +
+ No filter conditions configured — matches all logs. +
+ )} +
+ - {/* Condition rows */} -
- {conditions.map((condition: FilterConditionData, index: number) => { - return ( -
- {index > 0 && ( -
-
- - {connector} - -
-
- )} - 1} - onChange={(updated: FilterConditionData) => { - const newConditions: Array = [ - ...conditions, - ]; - newConditions[index] = updated; - setConditions(newConditions); - }} - onDelete={() => { - const newConditions: Array = - conditions.filter((_: FilterConditionData, i: number) => { - return i !== index; - }); - setConditions(newConditions); + {/* Edit modal */} + {showModal && ( + { + handleSave().catch(() => { + // error handled inside handleSave + }); + }} + isLoading={isSaving} + disableSubmitButton={isSaving} + > +
+ {error && ( +
+ { + setError(""); }} />
- ); - })} -
+ )} - {/* Add condition + Clear buttons */} -
-
+ {/* Connector selector */} + {modalConditions.length > 1 && ( +
+
+ + Match + +
+ { + return opt.value === modalConnector; + })} + onChange={( + value: DropdownValue | Array | null, + ) => { + setModalConnector( + (value?.toString() as LogicalConnector) || "AND", + ); + }} + /> +
+
+
+ )} - {/* Preview of generated query */} - {currentQuery && ( -
-

- Generated Filter Query -

- - {currentQuery} - + {/* Condition rows */} +
+ {modalConditions.map( + (condition: FilterConditionData, index: number) => { + return ( +
+ {index > 0 && ( +
+
+ + {modalConnector} + +
+
+ )} + 1} + onChange={(updated: FilterConditionData) => { + const newConditions: Array = [ + ...modalConditions, + ]; + newConditions[index] = updated; + setModalConditions(newConditions); + }} + onDelete={() => { + const newConditions: Array = + modalConditions.filter( + (_: FilterConditionData, i: number) => { + return i !== index; + }, + ); + setModalConditions(newConditions); + }} + /> +
+ ); + }, + )} +
+ + {/* Add condition + Clear buttons */} +
+
+ + {/* Preview of generated query */} + {buildFilterQuery(modalConditions, modalConnector) && ( +
+

+ Generated Filter Query +

+ + {buildFilterQuery(modalConditions, modalConnector)} + +
+ )}
- )} -
- + + )} + ); };