mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 08:09:27 +00:00
feat: revamp Conditions in alerts and pipelines (#9267)
Co-authored-by: Uddhav Dave <uddhav@MacBook-Pro.local> Co-authored-by: sai nikhil kethe <nikhil@openobserve.ai> Co-authored-by: Shrinath Rao <shnath@openobserve.ai>
This commit is contained in:
co-authored by
Uddhav Dave
sai nikhil kethe
Shrinath Rao
parent
fa9fca0c55
commit
9056a2c92d
@@ -193,6 +193,7 @@ jobs:
|
||||
- testfolder: "Pipelines"
|
||||
browser: "chrome"
|
||||
run_files: [
|
||||
"pipeline-conditions.spec.js",
|
||||
"pipelines.spec.js",
|
||||
"pipeline-dynamic.spec.js",
|
||||
"pipeline-core.spec.js",
|
||||
|
||||
@@ -341,7 +341,7 @@ pub struct QueryCondition {
|
||||
#[serde(rename = "type")]
|
||||
pub query_type: QueryType,
|
||||
#[schema(value_type = Option<Object>)]
|
||||
pub conditions: Option<ConditionList>,
|
||||
pub conditions: Option<AlertConditionParams>,
|
||||
pub sql: Option<String>,
|
||||
pub promql: Option<String>, // (cpu usage / cpu total)
|
||||
pub promql_condition: Option<Condition>, // value >= 80
|
||||
@@ -634,6 +634,155 @@ impl std::fmt::Display for Operator {
|
||||
}
|
||||
}
|
||||
|
||||
// Condition system v2 for pipeline conditions with linear evaluation
|
||||
// This provides an alternative to the tree-based ConditionList (v1) for expressing
|
||||
// mixed boolean operations with natural left-to-right ordering
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize, ToSchema)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum LogicalOperator {
|
||||
And,
|
||||
Or,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize, ToSchema)]
|
||||
#[serde(tag = "filterType", rename_all = "lowercase")]
|
||||
pub enum ConditionItem {
|
||||
Condition {
|
||||
column: String,
|
||||
operator: Operator,
|
||||
#[schema(value_type = Object)]
|
||||
value: Value,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
ignore_case: Option<bool>,
|
||||
#[serde(rename = "logicalOperator")]
|
||||
logical_operator: LogicalOperator,
|
||||
},
|
||||
Group {
|
||||
#[serde(rename = "logicalOperator")]
|
||||
logical_operator: LogicalOperator,
|
||||
conditions: Vec<ConditionItem>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ConditionItem {
|
||||
/// Get the logical operator for this condition item
|
||||
pub fn logical_operator(&self) -> &LogicalOperator {
|
||||
match self {
|
||||
ConditionItem::Condition {
|
||||
logical_operator, ..
|
||||
} => logical_operator,
|
||||
ConditionItem::Group {
|
||||
logical_operator, ..
|
||||
} => logical_operator,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ConditionGroup {
|
||||
pub filter_type: String, // Always "group"
|
||||
#[serde(rename = "logicalOperator")]
|
||||
pub logical_operator: LogicalOperator,
|
||||
pub conditions: Vec<ConditionItem>,
|
||||
}
|
||||
|
||||
impl ConditionGroup {
|
||||
/// Checks if the condition group has conditions
|
||||
pub fn has_conditions(&self) -> bool {
|
||||
!self.conditions.is_empty()
|
||||
}
|
||||
|
||||
/// Validates the condition group structure
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if self.filter_type != "group" {
|
||||
return Err(format!(
|
||||
"Invalid filterType: expected 'group', got '{}'",
|
||||
self.filter_type
|
||||
));
|
||||
}
|
||||
|
||||
if !self.has_conditions() {
|
||||
return Err("Condition Group should have atleast 1 condition".to_owned());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Versioned condition parameters for alerts
|
||||
/// Supports both v1 (tree-based ConditionList) and v2 (linear ConditionGroup)
|
||||
#[derive(Clone, Debug, PartialEq, ToSchema)]
|
||||
pub enum AlertConditionParams {
|
||||
/// v1 format: Tree-based ConditionList (default when no version field)
|
||||
V1(ConditionList),
|
||||
/// v2 format: Linear ConditionGroup (version: 2)
|
||||
V2(ConditionGroup),
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for AlertConditionParams {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
use serde_json::Value;
|
||||
|
||||
let value = Value::deserialize(deserializer)?;
|
||||
|
||||
// Get version field, default to 1 if not present
|
||||
let version = value.get("version").and_then(|v| v.as_u64()).unwrap_or(1);
|
||||
|
||||
match version {
|
||||
1 => {
|
||||
let conditions: ConditionList = serde_json::from_value(value).map_err(|e| {
|
||||
D::Error::custom(format!("Failed to parse V1 conditions: {}", e))
|
||||
})?;
|
||||
Ok(AlertConditionParams::V1(conditions))
|
||||
}
|
||||
2 => {
|
||||
// Get conditions field
|
||||
let conditions_value = value
|
||||
.get("conditions")
|
||||
.ok_or_else(|| D::Error::custom("conditions field missing for v2"))?;
|
||||
|
||||
let conditions: ConditionGroup = serde_json::from_value(conditions_value.clone())
|
||||
.map_err(|e| {
|
||||
D::Error::custom(format!("Failed to parse V2 conditions: {}", e))
|
||||
})?;
|
||||
Ok(AlertConditionParams::V2(conditions))
|
||||
}
|
||||
_ => Err(D::Error::custom(format!(
|
||||
"Unsupported version: {}",
|
||||
version
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for AlertConditionParams {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
use serde::ser::SerializeStruct;
|
||||
|
||||
match self {
|
||||
AlertConditionParams::V1(conditions) => {
|
||||
// V1: Serialize directly as ConditionList (no version field)
|
||||
conditions.serialize(serializer)
|
||||
}
|
||||
AlertConditionParams::V2(conditions) => {
|
||||
// V2: Include version field
|
||||
let mut state = serializer.serialize_struct("AlertConditionParams", 2)?;
|
||||
state.serialize_field("version", &2)?;
|
||||
state.serialize_field("conditions", conditions)?;
|
||||
state.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use chrono::{DateTime, FixedOffset, TimeZone};
|
||||
@@ -1390,6 +1539,165 @@ mod test {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_condition_group_serialization() {
|
||||
// Test simple ConditionGroup serialization
|
||||
let condition_json = r#"{
|
||||
"filterType": "group",
|
||||
"logicalOperator": "AND",
|
||||
"conditions": [
|
||||
{
|
||||
"filterType": "condition",
|
||||
"column": "status",
|
||||
"operator": "=",
|
||||
"value": "error",
|
||||
"logicalOperator": "AND"
|
||||
},
|
||||
{
|
||||
"filterType": "condition",
|
||||
"column": "level",
|
||||
"operator": "=",
|
||||
"value": "critical",
|
||||
"logicalOperator": "OR"
|
||||
}
|
||||
]
|
||||
}"#;
|
||||
|
||||
let condition_group: ConditionGroup = serde_json::from_str(condition_json).unwrap();
|
||||
assert_eq!(condition_group.filter_type, "group");
|
||||
assert_eq!(condition_group.logical_operator, LogicalOperator::And);
|
||||
assert_eq!(condition_group.conditions.len(), 2);
|
||||
|
||||
// Test serialization back to JSON
|
||||
let serialized = serde_json::to_value(&condition_group).unwrap();
|
||||
assert_eq!(serialized["filterType"], "group");
|
||||
assert_eq!(serialized["logicalOperator"], "AND");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_condition_group_with_nested_groups() {
|
||||
let condition_json = r#"{
|
||||
"filterType": "group",
|
||||
"logicalOperator": "AND",
|
||||
"conditions": [
|
||||
{
|
||||
"filterType": "condition",
|
||||
"column": "status",
|
||||
"operator": "=",
|
||||
"value": "error",
|
||||
"logicalOperator": "AND"
|
||||
},
|
||||
{
|
||||
"filterType": "group",
|
||||
"logicalOperator": "AND",
|
||||
"conditions": [
|
||||
{
|
||||
"filterType": "condition",
|
||||
"column": "service",
|
||||
"operator": "=",
|
||||
"value": "api",
|
||||
"logicalOperator": "OR"
|
||||
},
|
||||
{
|
||||
"filterType": "condition",
|
||||
"column": "service",
|
||||
"operator": "=",
|
||||
"value": "web",
|
||||
"logicalOperator": "AND"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"#;
|
||||
|
||||
let condition_group: ConditionGroup = serde_json::from_str(condition_json).unwrap();
|
||||
assert_eq!(condition_group.conditions.len(), 2);
|
||||
|
||||
// Check nested group
|
||||
if let ConditionItem::Group { conditions, .. } = &condition_group.conditions[1] {
|
||||
assert_eq!(conditions.len(), 2);
|
||||
} else {
|
||||
panic!("Expected nested group");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_condition_item_logical_operator() {
|
||||
let condition_item = ConditionItem::Condition {
|
||||
column: "status".to_string(),
|
||||
operator: Operator::EqualTo,
|
||||
value: Value::String("error".to_string()),
|
||||
ignore_case: None,
|
||||
logical_operator: LogicalOperator::And,
|
||||
};
|
||||
|
||||
assert_eq!(*condition_item.logical_operator(), LogicalOperator::And);
|
||||
|
||||
let group_item = ConditionItem::Group {
|
||||
logical_operator: LogicalOperator::Or,
|
||||
conditions: vec![],
|
||||
};
|
||||
|
||||
assert_eq!(*group_item.logical_operator(), LogicalOperator::Or);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_condition_group_validation() {
|
||||
// Valid condition group (needs at least 2 conditions)
|
||||
let condition_group = ConditionGroup {
|
||||
filter_type: "group".to_string(),
|
||||
logical_operator: LogicalOperator::And,
|
||||
conditions: vec![
|
||||
ConditionItem::Condition {
|
||||
column: "status".to_string(),
|
||||
operator: Operator::EqualTo,
|
||||
value: Value::String("error".to_string()),
|
||||
ignore_case: None,
|
||||
logical_operator: LogicalOperator::And,
|
||||
},
|
||||
ConditionItem::Condition {
|
||||
column: "level".to_string(),
|
||||
operator: Operator::EqualTo,
|
||||
value: Value::String("critical".to_string()),
|
||||
ignore_case: None,
|
||||
logical_operator: LogicalOperator::And,
|
||||
},
|
||||
],
|
||||
};
|
||||
assert!(condition_group.validate().is_ok());
|
||||
|
||||
// Invalid filter type
|
||||
let invalid_condition_group = ConditionGroup {
|
||||
filter_type: "invalid".to_string(),
|
||||
logical_operator: LogicalOperator::And,
|
||||
conditions: vec![
|
||||
ConditionItem::Condition {
|
||||
column: "status".to_string(),
|
||||
operator: Operator::EqualTo,
|
||||
value: Value::String("error".to_string()),
|
||||
ignore_case: None,
|
||||
logical_operator: LogicalOperator::And,
|
||||
},
|
||||
ConditionItem::Condition {
|
||||
column: "level".to_string(),
|
||||
operator: Operator::EqualTo,
|
||||
value: Value::String("critical".to_string()),
|
||||
ignore_case: None,
|
||||
logical_operator: LogicalOperator::And,
|
||||
},
|
||||
],
|
||||
};
|
||||
assert!(invalid_condition_group.validate().is_err());
|
||||
|
||||
// Empty conditions (should fail - needs at least 2)
|
||||
let empty_condition_group = ConditionGroup {
|
||||
filter_type: "group".to_string(),
|
||||
logical_operator: LogicalOperator::And,
|
||||
conditions: vec![],
|
||||
};
|
||||
assert!(empty_condition_group.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backward_compatibility_with_fixed_offset() {
|
||||
// Test that alerts without timezone string still work with FixedOffset
|
||||
@@ -1476,4 +1784,266 @@ mod test {
|
||||
// London is close to UTC (0 or +60 during BST)
|
||||
assert!(london_offset >= 0 && london_offset <= 60);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_alert_condition_params_v2_deserialization() {
|
||||
// Test V2 format deserialization
|
||||
let v2_json = r#"{
|
||||
"version": 2,
|
||||
"conditions": {
|
||||
"filterType": "group",
|
||||
"logicalOperator": "AND",
|
||||
"conditions": [
|
||||
{
|
||||
"filterType": "condition",
|
||||
"column": "level",
|
||||
"operator": "=",
|
||||
"value": "error",
|
||||
"logicalOperator": "AND"
|
||||
},
|
||||
{
|
||||
"filterType": "condition",
|
||||
"column": "service",
|
||||
"operator": "=",
|
||||
"value": "api",
|
||||
"logicalOperator": "OR"
|
||||
}
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
let result: Result<AlertConditionParams, _> = serde_json::from_str(v2_json);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"V2 deserialization failed: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
let params = result.unwrap();
|
||||
match params {
|
||||
AlertConditionParams::V2(group) => {
|
||||
assert_eq!(group.filter_type, "group");
|
||||
assert_eq!(group.logical_operator, LogicalOperator::And);
|
||||
assert_eq!(group.conditions.len(), 2);
|
||||
}
|
||||
_ => panic!("Expected V2 variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_alert_condition_params_v1_deserialization() {
|
||||
// Test V1 format deserialization (without version field)
|
||||
let v1_json = r#"{
|
||||
"and": [
|
||||
{
|
||||
"column": "level",
|
||||
"operator": "=",
|
||||
"value": "error",
|
||||
"ignore_case": false
|
||||
},
|
||||
{
|
||||
"column": "service",
|
||||
"operator": "=",
|
||||
"value": "api",
|
||||
"ignore_case": false
|
||||
}
|
||||
]
|
||||
}"#;
|
||||
|
||||
let result: Result<AlertConditionParams, _> = serde_json::from_str(v1_json);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"V1 deserialization failed: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
let params = result.unwrap();
|
||||
match params {
|
||||
AlertConditionParams::V1(conditions) => {
|
||||
assert!(matches!(conditions, ConditionList::AndNode { .. }));
|
||||
}
|
||||
_ => panic!("Expected V1 variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_condition_v2_deserialization() {
|
||||
// Test full QueryCondition with V2 format
|
||||
let query_condition_json = r#"{
|
||||
"type": "custom",
|
||||
"conditions": {
|
||||
"version": 2,
|
||||
"conditions": {
|
||||
"filterType": "group",
|
||||
"logicalOperator": "AND",
|
||||
"conditions": [
|
||||
{
|
||||
"filterType": "condition",
|
||||
"column": "level",
|
||||
"operator": "=",
|
||||
"value": "error",
|
||||
"logicalOperator": "AND"
|
||||
},
|
||||
{
|
||||
"filterType": "condition",
|
||||
"column": "service",
|
||||
"operator": "=",
|
||||
"value": "api",
|
||||
"logicalOperator": "OR"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"sql": null,
|
||||
"promql": null,
|
||||
"aggregation": null,
|
||||
"promql_condition": null,
|
||||
"vrl_function": null,
|
||||
"multi_time_range": null
|
||||
}"#;
|
||||
|
||||
let result: Result<QueryCondition, _> = serde_json::from_str(query_condition_json);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"QueryCondition deserialization failed: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
let query_cond = result.unwrap();
|
||||
assert_eq!(query_cond.query_type, QueryType::Custom);
|
||||
assert!(query_cond.conditions.is_some());
|
||||
|
||||
match query_cond.conditions.unwrap() {
|
||||
AlertConditionParams::V2(group) => {
|
||||
assert_eq!(group.filter_type, "group");
|
||||
assert_eq!(group.logical_operator, LogicalOperator::And);
|
||||
assert_eq!(group.conditions.len(), 2);
|
||||
}
|
||||
_ => panic!("Expected V2 variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_condition_v2_with_empty_strings() {
|
||||
// Test with empty strings for optional fields (like the user's curl payload)
|
||||
let query_condition_json = r#"{
|
||||
"type": "custom",
|
||||
"conditions": {
|
||||
"version": 2,
|
||||
"conditions": {
|
||||
"filterType": "group",
|
||||
"logicalOperator": "AND",
|
||||
"conditions": [
|
||||
{
|
||||
"filterType": "condition",
|
||||
"column": "level",
|
||||
"operator": "=",
|
||||
"value": "error",
|
||||
"logicalOperator": "AND"
|
||||
},
|
||||
{
|
||||
"filterType": "condition",
|
||||
"column": "service",
|
||||
"operator": "=",
|
||||
"value": "api",
|
||||
"logicalOperator": "OR"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"sql": "",
|
||||
"promql": "",
|
||||
"aggregation": null,
|
||||
"promql_condition": null,
|
||||
"vrl_function": null,
|
||||
"multi_time_range": []
|
||||
}"#;
|
||||
|
||||
let result: Result<QueryCondition, _> = serde_json::from_str(query_condition_json);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"QueryCondition deserialization with empty strings failed: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
let query_cond = result.unwrap();
|
||||
assert_eq!(query_cond.query_type, QueryType::Custom);
|
||||
assert!(query_cond.conditions.is_some());
|
||||
|
||||
match query_cond.conditions.unwrap() {
|
||||
AlertConditionParams::V2(group) => {
|
||||
assert_eq!(group.filter_type, "group");
|
||||
assert_eq!(group.logical_operator, LogicalOperator::And);
|
||||
assert_eq!(group.conditions.len(), 2);
|
||||
}
|
||||
_ => panic!("Expected V2 variant"),
|
||||
}
|
||||
|
||||
// Verify empty strings are treated as Some("") not None
|
||||
assert!(query_cond.sql.is_some());
|
||||
assert_eq!(query_cond.sql.unwrap(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exact_user_payload() {
|
||||
// Test the exact structure from the user's curl command
|
||||
let user_json = r#"{
|
||||
"type": "custom",
|
||||
"conditions": {
|
||||
"version": 2,
|
||||
"conditions": {
|
||||
"filterType": "group",
|
||||
"logicalOperator": "AND",
|
||||
"conditions": [
|
||||
{
|
||||
"filterType": "condition",
|
||||
"column": "level",
|
||||
"operator": "=",
|
||||
"value": "error",
|
||||
"logicalOperator": "AND"
|
||||
},
|
||||
{
|
||||
"filterType": "condition",
|
||||
"column": "service",
|
||||
"operator": "=",
|
||||
"value": "api",
|
||||
"logicalOperator": "OR"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"sql": null,
|
||||
"promql": null,
|
||||
"aggregation": null,
|
||||
"promql_condition": null,
|
||||
"vrl_function": null,
|
||||
"search_event_type": null,
|
||||
"multi_time_range": null
|
||||
}"#;
|
||||
|
||||
println!("Testing user JSON...");
|
||||
let result: Result<QueryCondition, _> = serde_json::from_str(user_json);
|
||||
|
||||
if let Err(ref e) = result {
|
||||
println!("Deserialization error: {}", e);
|
||||
|
||||
// Try parsing just the conditions field
|
||||
let value: serde_json::Value = serde_json::from_str(user_json).unwrap();
|
||||
if let Some(conditions) = value.get("conditions") {
|
||||
println!("Conditions value: {:#?}", conditions);
|
||||
let cond_result: Result<AlertConditionParams, _> =
|
||||
serde_json::from_value(conditions.clone());
|
||||
println!(
|
||||
"Direct AlertConditionParams parse result: {:?}",
|
||||
cond_result
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"User payload deserialization failed: {:?}",
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::meta::{
|
||||
alerts::{ConditionList, QueryCondition, TriggerCondition},
|
||||
alerts::{ConditionGroup, ConditionList, QueryCondition, TriggerCondition},
|
||||
stream::{RemoteStreamParams, StreamParams, StreamType},
|
||||
};
|
||||
|
||||
@@ -165,9 +165,79 @@ pub struct FunctionParams {
|
||||
pub num_args: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
|
||||
pub struct ConditionParams {
|
||||
pub conditions: ConditionList,
|
||||
#[derive(Debug, Clone, PartialEq, ToSchema)]
|
||||
pub enum ConditionParams {
|
||||
/// v1 format: Tree-based ConditionList (default when no version field)
|
||||
V1 { conditions: ConditionList },
|
||||
/// v2 format: Linear ConditionGroup (version: 2)
|
||||
V2 { conditions: ConditionGroup },
|
||||
}
|
||||
|
||||
// Custom deserializer to handle missing version field (defaults to V1 for backward compatibility)
|
||||
impl<'de> Deserialize<'de> for ConditionParams {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
use serde_json::Value;
|
||||
|
||||
let value = Value::deserialize(deserializer)?;
|
||||
|
||||
// If version field is missing, default to 1 for backward compatibility
|
||||
let version = value.get("version").and_then(|v| v.as_u64()).unwrap_or(1);
|
||||
|
||||
let Some(conditions) = value.get("conditions") else {
|
||||
return Err(D::Error::missing_field("conditions"));
|
||||
};
|
||||
|
||||
match version {
|
||||
1 => {
|
||||
let conditions: ConditionList = serde_json::from_value(conditions.clone())
|
||||
.map_err(|e| {
|
||||
D::Error::custom(format!("Failed to parse v1 conditions: {}", e))
|
||||
})?;
|
||||
Ok(ConditionParams::V1 { conditions })
|
||||
}
|
||||
2 => {
|
||||
let conditions: ConditionGroup = serde_json::from_value(conditions.clone())
|
||||
.map_err(|e| {
|
||||
D::Error::custom(format!("Failed to parse v2 conditions: {}", e))
|
||||
})?;
|
||||
Ok(ConditionParams::V2 { conditions })
|
||||
}
|
||||
_ => Err(D::Error::custom(format!(
|
||||
"Unsupported version: {}",
|
||||
version
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom serializer to omit version field for V1 (backward compatibility)
|
||||
impl Serialize for ConditionParams {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
use serde::ser::SerializeStruct;
|
||||
|
||||
match self {
|
||||
ConditionParams::V1 { conditions } => {
|
||||
// V1: Omit version field for backward compatibility
|
||||
let mut state = serializer.serialize_struct("ConditionParams", 1)?;
|
||||
state.serialize_field("conditions", conditions)?;
|
||||
state.end()
|
||||
}
|
||||
ConditionParams::V2 { conditions } => {
|
||||
// V2: Include version field
|
||||
let mut state = serializer.serialize_struct("ConditionParams", 2)?;
|
||||
state.serialize_field("version", &2)?;
|
||||
state.serialize_field("conditions", conditions)?;
|
||||
state.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
|
||||
@@ -325,4 +395,151 @@ mod tests {
|
||||
let or_node_data = json::from_value::<NodeData>(or_payload);
|
||||
assert!(or_node_data.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_condition_node_v2_serialization() {
|
||||
// Test v2 condition format with version field
|
||||
let payload = json::json!({
|
||||
"node_type": "condition",
|
||||
"version": 2,
|
||||
"conditions": {
|
||||
"filterType": "group",
|
||||
"logicalOperator": "AND",
|
||||
"conditions": [
|
||||
{
|
||||
"filterType": "condition",
|
||||
"column": "status",
|
||||
"operator": "=",
|
||||
"value": "error",
|
||||
"logicalOperator": "AND"
|
||||
},
|
||||
{
|
||||
"filterType": "condition",
|
||||
"column": "level",
|
||||
"operator": "=",
|
||||
"value": "critical",
|
||||
"logicalOperator": "OR"
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
let node_data = json::from_value::<NodeData>(payload);
|
||||
assert!(node_data.is_ok());
|
||||
|
||||
// Verify it's V2
|
||||
if let Ok(NodeData::Condition(params)) = node_data {
|
||||
assert!(matches!(params, ConditionParams::V2 { .. }));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_condition_node_v2_nested_groups() {
|
||||
// Test with nested groups
|
||||
let nested_payload = json::json!({
|
||||
"node_type": "condition",
|
||||
"version": 2,
|
||||
"conditions": {
|
||||
"filterType": "group",
|
||||
"logicalOperator": "AND",
|
||||
"conditions": [
|
||||
{
|
||||
"filterType": "condition",
|
||||
"column": "status",
|
||||
"operator": "=",
|
||||
"value": "error",
|
||||
"logicalOperator": "AND"
|
||||
},
|
||||
{
|
||||
"filterType": "group",
|
||||
"logicalOperator": "OR",
|
||||
"conditions": [
|
||||
{
|
||||
"filterType": "condition",
|
||||
"column": "service",
|
||||
"operator": "=",
|
||||
"value": "api",
|
||||
"logicalOperator": "OR"
|
||||
},
|
||||
{
|
||||
"filterType": "condition",
|
||||
"column": "service",
|
||||
"operator": "=",
|
||||
"value": "web",
|
||||
"logicalOperator": "AND"
|
||||
}
|
||||
],
|
||||
"logicalOperator": "AND"
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
let nested_node_data = json::from_value::<NodeData>(nested_payload);
|
||||
assert!(nested_node_data.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_v2_nested_condition_nodes() {
|
||||
let json_str = r#"[
|
||||
{
|
||||
"id": "7035edaa-e96c-4114-8b3c-6c77db44418d",
|
||||
"data": {
|
||||
"node_type": "condition",
|
||||
"version": 2,
|
||||
"conditions": {
|
||||
"filterType": "group",
|
||||
"logicalOperator": "AND",
|
||||
"conditions": [
|
||||
{
|
||||
"filterType": "condition",
|
||||
"column": "kubernetes_annotations_prometheus_io_path",
|
||||
"operator": "=",
|
||||
"value": "asdfa",
|
||||
"logicalOperator": "AND"
|
||||
},
|
||||
{
|
||||
"filterType": "group",
|
||||
"logicalOperator": "AND",
|
||||
"conditions": [
|
||||
{
|
||||
"filterType": "condition",
|
||||
"column": "distinct_field_hc_0",
|
||||
"operator": "=",
|
||||
"value": "123",
|
||||
"logicalOperator": "OR"
|
||||
},
|
||||
{
|
||||
"filterType": "condition",
|
||||
"column": "kubernetes_annotations_prometheus_io_path",
|
||||
"operator": "=",
|
||||
"value": "123",
|
||||
"logicalOperator": "AND"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"filterType": "condition",
|
||||
"column": "kubernetes_container_hash",
|
||||
"operator": "=",
|
||||
"value": "dasfg",
|
||||
"logicalOperator": "AND"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"position": {
|
||||
"x": 271.25,
|
||||
"y": 335
|
||||
},
|
||||
"io_type": "default"
|
||||
}
|
||||
]"#;
|
||||
let result: Result<Vec<Node>, _> = json::from_str(json_str);
|
||||
match &result {
|
||||
Ok(nodes) => println!("✓ Successfully deserialized {} nodes", nodes.len()),
|
||||
Err(e) => println!("✗ Deserialization error: {}", e),
|
||||
}
|
||||
assert!(result.is_ok(), "Failed to deserialize: {:?}", result.err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,10 +128,19 @@ impl Pipeline {
|
||||
|
||||
for node in self.nodes.iter_mut() {
|
||||
// ck 4
|
||||
if matches!(&node.data, NodeData::Condition(condition_params) if !condition_params.conditions.has_conditions())
|
||||
{
|
||||
return Err(anyhow!("ConditionNode must have non-empty conditions"));
|
||||
} else if let NodeData::Stream(stream_params) = &mut node.data {
|
||||
if let NodeData::Condition(condition_params) = &node.data {
|
||||
let has_empty_conditions = match condition_params {
|
||||
components::ConditionParams::V1 { conditions } => !conditions.has_conditions(),
|
||||
components::ConditionParams::V2 { conditions } => {
|
||||
conditions.conditions.is_empty()
|
||||
}
|
||||
};
|
||||
if has_empty_conditions {
|
||||
return Err(anyhow!("ConditionNode must have non-empty conditions"));
|
||||
}
|
||||
}
|
||||
|
||||
if let NodeData::Stream(stream_params) = &mut node.data {
|
||||
// ck 8
|
||||
if stream_params.stream_type == StreamType::EnrichmentTables
|
||||
&& matches!(&self.source, PipelineSource::Realtime(_))
|
||||
|
||||
@@ -163,7 +163,7 @@ pub struct QueryCondition {
|
||||
#[serde(rename = "type")]
|
||||
pub query_type: QueryType,
|
||||
#[schema(value_type = Option<Object>)]
|
||||
pub conditions: Option<meta_alerts::ConditionList>,
|
||||
pub conditions: Option<meta_alerts::AlertConditionParams>,
|
||||
pub sql: Option<String>,
|
||||
pub promql: Option<String>,
|
||||
pub promql_condition: Option<Condition>,
|
||||
@@ -353,6 +353,7 @@ impl From<meta_alerts::QueryCondition> for QueryCondition {
|
||||
fn from(value: meta_alerts::QueryCondition) -> Self {
|
||||
Self {
|
||||
query_type: value.query_type.into(),
|
||||
// Pass AlertConditionParams directly (supports both V1 and V2)
|
||||
conditions: value.conditions,
|
||||
sql: value.sql,
|
||||
promql: value.promql,
|
||||
@@ -531,6 +532,7 @@ impl From<QueryCondition> for meta_alerts::QueryCondition {
|
||||
fn from(value: QueryCondition) -> Self {
|
||||
Self {
|
||||
query_type: value.query_type.into(),
|
||||
// Pass AlertConditionParams directly (supports both V1 and V2)
|
||||
conditions: value.conditions,
|
||||
sql: value.sql,
|
||||
promql: value.promql,
|
||||
@@ -639,3 +641,115 @@ impl From<StreamType> for meta_stream::StreamType {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_query_condition_v2_deserialization() {
|
||||
// Test that the API layer can deserialize V2 conditions
|
||||
let json = r#"{
|
||||
"type": "custom",
|
||||
"conditions": {
|
||||
"version": 2,
|
||||
"conditions": {
|
||||
"filterType": "group",
|
||||
"logicalOperator": "AND",
|
||||
"conditions": [
|
||||
{
|
||||
"filterType": "condition",
|
||||
"type": "condition",
|
||||
"column": "level",
|
||||
"operator": "=",
|
||||
"value": "error",
|
||||
"logicalOperator": "AND"
|
||||
},
|
||||
{
|
||||
"filterType": "condition",
|
||||
"type": "condition",
|
||||
"column": "service",
|
||||
"operator": "=",
|
||||
"value": "api",
|
||||
"logicalOperator": "OR"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"sql": null,
|
||||
"promql": null,
|
||||
"aggregation": null,
|
||||
"promql_condition": null,
|
||||
"vrl_function": null,
|
||||
"search_event_type": null,
|
||||
"multi_time_range": null
|
||||
}"#;
|
||||
|
||||
let result: Result<QueryCondition, _> = serde_json::from_str(json);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"V2 deserialization failed: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
let query_cond = result.unwrap();
|
||||
assert_eq!(query_cond.query_type, QueryType::Custom);
|
||||
assert!(query_cond.conditions.is_some());
|
||||
|
||||
// Verify it's V2
|
||||
match query_cond.conditions.unwrap() {
|
||||
meta_alerts::AlertConditionParams::V2(group) => {
|
||||
assert_eq!(group.filter_type, "group");
|
||||
assert_eq!(group.conditions.len(), 2);
|
||||
}
|
||||
_ => panic!("Expected V2 variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_condition_v1_deserialization() {
|
||||
// Test that the API layer still supports V1 conditions
|
||||
let json = r#"{
|
||||
"type": "custom",
|
||||
"conditions": {
|
||||
"and": [
|
||||
{
|
||||
"column": "level",
|
||||
"operator": "=",
|
||||
"value": "error",
|
||||
"ignore_case": false
|
||||
},
|
||||
{
|
||||
"column": "service",
|
||||
"operator": "=",
|
||||
"value": "api",
|
||||
"ignore_case": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"sql": null,
|
||||
"promql": null,
|
||||
"aggregation": null,
|
||||
"promql_condition": null,
|
||||
"vrl_function": null,
|
||||
"search_event_type": null,
|
||||
"multi_time_range": null
|
||||
}"#;
|
||||
|
||||
let result: Result<QueryCondition, _> = serde_json::from_str(json);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"V1 deserialization failed: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
let query_cond = result.unwrap();
|
||||
assert!(query_cond.conditions.is_some());
|
||||
|
||||
// Verify it's V1
|
||||
match query_cond.conditions.unwrap() {
|
||||
meta_alerts::AlertConditionParams::V1(_) => {}
|
||||
_ => panic!("Expected V1 variant"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,7 @@ use std::str::FromStr;
|
||||
use chrono::{DateTime, FixedOffset, TimeZone, Utc};
|
||||
use config::meta::{
|
||||
alerts::{
|
||||
ConditionList, QueryCondition as MetaQueryCondition,
|
||||
TriggerCondition as MetaTriggerCondition,
|
||||
QueryCondition as MetaQueryCondition, TriggerCondition as MetaTriggerCondition,
|
||||
alert::{Alert as MetaAlert, ListAlertsParams},
|
||||
deduplication::DeduplicationConfig as MetaDeduplicationConfig,
|
||||
},
|
||||
@@ -77,7 +76,7 @@ impl TryFrom<alerts::Model> for MetaAlert {
|
||||
.context_attributes
|
||||
.map(serde_json::from_value)
|
||||
.transpose()?;
|
||||
let query_conditions: Option<ConditionList> = value
|
||||
let query_conditions: Option<config::meta::alerts::AlertConditionParams> = value
|
||||
.query_conditions
|
||||
.map(serde_json::from_value)
|
||||
.transpose()?;
|
||||
|
||||
@@ -241,7 +241,7 @@ async fn migrate_pipelines() -> Result<(), anyhow::Error> {
|
||||
pos_y += pos_offset;
|
||||
let condition_node = Node::new(
|
||||
ider::uuid(),
|
||||
NodeData::Condition(ConditionParams {
|
||||
NodeData::Condition(ConditionParams::V1 {
|
||||
conditions: ConditionList::LegacyConditions(routing_conditions),
|
||||
}),
|
||||
pos_x,
|
||||
|
||||
+1052
-3
File diff suppressed because it is too large
Load Diff
@@ -45,7 +45,7 @@ use tokio::{
|
||||
use crate::{
|
||||
common::infra::config::QUERY_FUNCTIONS,
|
||||
service::{
|
||||
alerts::ConditionExt,
|
||||
alerts::{ConditionExt, ConditionGroupExt},
|
||||
ingestion::{apply_vrl_fn, compile_vrl_function},
|
||||
self_reporting::publish_error,
|
||||
},
|
||||
@@ -651,12 +651,21 @@ async fn process_node(
|
||||
};
|
||||
flattened = true;
|
||||
}
|
||||
|
||||
// Evaluate based on condition version
|
||||
let passes = match condition_params {
|
||||
config::meta::pipeline::components::ConditionParams::V1 { conditions } => {
|
||||
// v1: Use tree-based ConditionList evaluation
|
||||
conditions.evaluate(record.as_object().unwrap()).await
|
||||
}
|
||||
config::meta::pipeline::components::ConditionParams::V2 { conditions } => {
|
||||
// v2: Use linear ConditionGroup evaluation
|
||||
conditions.evaluate(record.as_object().unwrap()).await
|
||||
}
|
||||
};
|
||||
|
||||
// only send to children when passing all condition evaluations
|
||||
if condition_params
|
||||
.conditions
|
||||
.evaluate(record.as_object().unwrap())
|
||||
.await
|
||||
{
|
||||
if passes {
|
||||
send_to_children(
|
||||
&mut child_senders,
|
||||
PipelineItem {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// pipelinesPage.js
|
||||
const { expect } = require('@playwright/test')
|
||||
const testLogger = require('../../playwright-tests/utils/test-logger.js');
|
||||
|
||||
const randomNodeName = `remote-node-${Math.floor(Math.random() * 1000)}`;
|
||||
|
||||
@@ -110,6 +111,36 @@ export class PipelinesPage {
|
||||
this.deletedSuccessfullyText = page.getByText('deleted successfully');
|
||||
this.conditionDropdown = page.locator("div:nth-child(2) > div:nth-child(2) > .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native");
|
||||
this.deleteButtonNth1 = page.locator("button").filter({ hasText: "delete" }).nth(1);
|
||||
|
||||
// Condition-specific locators for comprehensive testing
|
||||
this.addConditionSection = page.locator('[data-test="add-condition-section"]');
|
||||
this.addConditionGroupBtn = page.locator('[data-test="alert-conditions-add-condition-group-btn"]');
|
||||
this.toggleOperatorBtn = page.locator('[data-test="alert-conditions-toggle-operator-btn"]');
|
||||
this.deleteConditionBtn = page.locator('[data-test="alert-conditions-delete-condition-btn"]');
|
||||
this.reorderBtn = page.locator('[data-test="alert-conditions-reorder-btn"]');
|
||||
this.addConditionCancelBtn = page.locator('[data-test="add-condition-cancel-btn"]');
|
||||
this.addConditionDeleteBtn = page.locator('[data-test="add-condition-delete-btn"]');
|
||||
this.scheduledAlertTabs = page.locator('[data-test="scheduled-alert-tabs"]');
|
||||
this.nestedGroups = page.locator('.el-border');
|
||||
this.operatorLabels = page.locator('span.tw-lowercase');
|
||||
this.firstConditionLabel = page.locator('.tw-flex.tw-items-start.tw-gap-1').first().locator('span').first();
|
||||
this.noteContainer = page.locator('.note-container');
|
||||
this.noteHeading = page.locator('.note-heading');
|
||||
this.noteInfo = page.locator('.note-info');
|
||||
this.qDialog = page.locator('.q-dialog');
|
||||
this.qDialogBackdrop = page.locator('.q-dialog__backdrop');
|
||||
this.qNotificationMessage = page.locator('.q-notification__message');
|
||||
this.qMenu = page.locator('.q-menu');
|
||||
|
||||
// Pipeline node locators
|
||||
this.pipelineNodeOutputStreamNode = page.locator('[data-test="pipeline-node-output-stream-node"]');
|
||||
this.pipelineNodeOutputDeleteBtn = page.locator('[data-test="pipeline-node-output-delete-btn"]');
|
||||
this.pipelineNodeDefaultConditionNode = page.locator('[data-test="pipeline-node-default-condition-node"]');
|
||||
this.pipelineNodeInputOutputHandle = page.locator('[data-test="pipeline-node-input-output-handle"]');
|
||||
this.pipelineNodeDefaultInputHandle = page.locator('[data-test="pipeline-node-default-input-handle"]');
|
||||
this.pipelineNodeDefaultOutputHandle = page.locator('[data-test="pipeline-node-default-output-handle"]');
|
||||
this.pipelineNodeOutputInputHandle = page.locator('[data-test="pipeline-node-output-input-handle"]');
|
||||
this.addPipelineBackBtn = page.locator('[data-test="add-pipeline-back-btn"]');
|
||||
}
|
||||
|
||||
// Methods from original PipelinesPage
|
||||
@@ -390,7 +421,7 @@ export class PipelinesPage {
|
||||
|
||||
if (functionName?.trim() === fileName) {
|
||||
fileFound = true;
|
||||
console.log("Uploaded file found:", functionName);
|
||||
testLogger.debug("Uploaded file found", { functionName });
|
||||
|
||||
// Click the 'Delete Function' button
|
||||
await row.locator('[title="Delete Function"]').click();
|
||||
@@ -624,4 +655,283 @@ export class PipelinesPage {
|
||||
await this.confirmDeletePipeline();
|
||||
await this.verifyPipelineDeleted();
|
||||
}
|
||||
|
||||
// ========== Condition-specific methods ==========
|
||||
|
||||
async fillCondition(columnName, operator, value, index = 0) {
|
||||
const columnSelects = this.columnSelect;
|
||||
|
||||
// Try clicking the input first
|
||||
await columnSelects.nth(index).locator('input').click();
|
||||
await this.page.waitForTimeout(200);
|
||||
|
||||
// Check if dropdown opened, if not click the dropdown icon
|
||||
let menuVisible = await this.qMenu.isVisible().catch(() => false);
|
||||
if (!menuVisible) {
|
||||
await columnSelects.nth(index).locator('i').click();
|
||||
await this.page.waitForTimeout(200);
|
||||
}
|
||||
|
||||
await columnSelects.nth(index).locator('input').fill(columnName);
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
// Select from dropdown
|
||||
const options = this.qMenu.locator('.q-item');
|
||||
await options.first().click();
|
||||
await this.page.waitForTimeout(300);
|
||||
|
||||
// Select operator
|
||||
await this.operatorSelect.nth(index).click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
// Scope to the visible dropdown menu and select the operator
|
||||
const visibleMenu = this.qMenu.last();
|
||||
await visibleMenu.locator('.q-item').getByText(operator, { exact: true }).click();
|
||||
await this.page.waitForTimeout(300);
|
||||
|
||||
// Fill value
|
||||
await this.valueInput.nth(index).locator('input').click();
|
||||
await this.valueInput.nth(index).locator('input').fill(value);
|
||||
await this.page.waitForTimeout(300);
|
||||
}
|
||||
|
||||
async addNewCondition() {
|
||||
await this.addConditionButton.first().click();
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
async addConditionGroup() {
|
||||
await this.addConditionGroupBtn.first().click();
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
async toggleConditionOperator(index = 0) {
|
||||
await this.toggleOperatorBtn.nth(index).click();
|
||||
await this.page.waitForTimeout(300);
|
||||
}
|
||||
|
||||
async deleteConditionByIndex(index) {
|
||||
await this.deleteConditionBtn.nth(index).click();
|
||||
await this.page.waitForTimeout(300);
|
||||
}
|
||||
|
||||
async createPipelineWithCondition(streamName) {
|
||||
await this.openPipelineMenu();
|
||||
await this.page.waitForTimeout(1000);
|
||||
await this.addPipeline();
|
||||
await this.selectStream();
|
||||
await this.dragStreamToTarget(this.streamButton);
|
||||
await this.selectLogs();
|
||||
await this.enterStreamName(streamName);
|
||||
await this.page.waitForTimeout(2000);
|
||||
await this.page.getByRole("option", { name: streamName, exact: true }).click();
|
||||
await this.saveInputNodeStream();
|
||||
await this.page.waitForTimeout(2000);
|
||||
|
||||
// Remove default output stream node
|
||||
await this.pipelineNodeOutputStreamNode.first().hover();
|
||||
await this.page.waitForTimeout(500);
|
||||
await this.pipelineNodeOutputDeleteBtn.first().click();
|
||||
await this.confirmButton.click();
|
||||
|
||||
// Add condition node
|
||||
await this.selectAndDragCondition();
|
||||
await this.page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
async saveConditionAndCompletePipeline(pipelineName) {
|
||||
await this.saveCondition();
|
||||
await this.page.waitForTimeout(2000);
|
||||
|
||||
// Wait for the condition dialog to close and the condition node to appear on canvas
|
||||
await this.page.waitForSelector('[data-test="add-condition-section"]', { state: 'hidden', timeout: 5000 }).catch(() => {});
|
||||
await this.page.waitForTimeout(1000);
|
||||
|
||||
// Verify the condition node was created successfully
|
||||
await expect(this.pipelineNodeDefaultConditionNode).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Add destination stream
|
||||
await this.selectAndDragSecondStream();
|
||||
await this.streamNameInput.click();
|
||||
await this.streamNameInput.fill("destination-node");
|
||||
await this.page.waitForTimeout(1000);
|
||||
await this.clickInputNodeStreamSave();
|
||||
|
||||
await this.page.waitForTimeout(2000);
|
||||
await this.page.waitForSelector('[data-test="pipeline-node-input-output-handle"]', { state: 'visible', timeout: 10000 });
|
||||
await this.page.waitForSelector('[data-test="pipeline-node-default-input-handle"]', { state: 'visible', timeout: 10000 });
|
||||
await this.page.waitForSelector('[data-test="pipeline-node-output-input-handle"]', { state: 'visible', timeout: 10000 });
|
||||
|
||||
await this.page.waitForSelector('.q-dialog__backdrop', { state: 'hidden', timeout: 3000 }).catch(() => {});
|
||||
|
||||
// Connect nodes
|
||||
await this.pipelineNodeInputOutputHandle.hover({ force: true });
|
||||
await this.page.mouse.down();
|
||||
await this.pipelineNodeDefaultInputHandle.hover({ force: true });
|
||||
await this.page.mouse.up();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
await this.pipelineNodeDefaultOutputHandle.hover({ force: true });
|
||||
await this.page.mouse.down();
|
||||
await this.pipelineNodeOutputInputHandle.hover({ force: true });
|
||||
await this.page.mouse.up();
|
||||
await this.page.waitForTimeout(1000);
|
||||
|
||||
await this.enterPipelineName(pipelineName);
|
||||
await this.savePipeline();
|
||||
|
||||
// Wait for save to complete
|
||||
await this.page.waitForTimeout(3000);
|
||||
}
|
||||
|
||||
async deletePipelineByName(pipelineName) {
|
||||
await this.searchPipeline(pipelineName);
|
||||
await this.page.waitForTimeout(1000);
|
||||
const deletePipelineButton = this.page.locator(
|
||||
`[data-test="pipeline-list-${pipelineName}-delete-pipeline"]`
|
||||
);
|
||||
await deletePipelineButton.waitFor({ state: "visible" });
|
||||
await deletePipelineButton.click();
|
||||
await this.confirmDeletePipeline();
|
||||
await this.verifyPipelineDeleted();
|
||||
}
|
||||
|
||||
async openPipelineForEdit(pipelineName) {
|
||||
await this.page.locator(`[data-test="pipeline-list-${pipelineName}-update-pipeline"]`).click();
|
||||
await this.page.waitForTimeout(2000);
|
||||
}
|
||||
|
||||
async clickConditionNode() {
|
||||
await this.pipelineNodeDefaultConditionNode.click();
|
||||
await this.page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
async reconnectSourceToDestination() {
|
||||
await this.pipelineNodeInputOutputHandle.hover({ force: true });
|
||||
await this.page.mouse.down();
|
||||
await this.pipelineNodeOutputInputHandle.hover({ force: true });
|
||||
await this.page.mouse.up();
|
||||
await this.page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
async verifyConditionDialogOpen() {
|
||||
await expect(this.addConditionSection).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyConditionDialogClosed() {
|
||||
await expect(this.addConditionSection).not.toBeVisible();
|
||||
}
|
||||
|
||||
async verifyFirstConditionLabel(text) {
|
||||
await expect(this.firstConditionLabel).toHaveText(text);
|
||||
}
|
||||
|
||||
async verifyConditionCount(count) {
|
||||
await expect(this.valueInput).toHaveCount(count);
|
||||
}
|
||||
|
||||
async verifyNestedGroupsExist() {
|
||||
const count = await this.nestedGroups.count();
|
||||
expect(count).toBeGreaterThan(1);
|
||||
}
|
||||
|
||||
async verifyScheduledAlertTabsVisible() {
|
||||
await expect(this.scheduledAlertTabs.first()).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyOperatorLabel(text, index = 0) {
|
||||
await expect(this.operatorLabels.nth(index)).toContainText(text);
|
||||
}
|
||||
|
||||
async verifyNoteContainer() {
|
||||
await expect(this.noteContainer).toBeVisible();
|
||||
await expect(this.noteHeading).toContainText("Condition value Guidelines");
|
||||
}
|
||||
|
||||
async captureConditionValues() {
|
||||
const valueInputs = this.valueInput.locator('input');
|
||||
const values = [];
|
||||
const count = await valueInputs.count();
|
||||
for (let i = 0; i < count; i++) {
|
||||
const value = await valueInputs.nth(i).inputValue();
|
||||
values.push(value);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
async clickReorderButton(index = 1) {
|
||||
await this.reorderBtn.nth(index).click();
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
async captureConditionScreenshot() {
|
||||
return await this.addConditionSection.screenshot();
|
||||
}
|
||||
|
||||
async cancelConditionDialog() {
|
||||
await this.addConditionCancelBtn.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
async clickDeleteConditionNode() {
|
||||
const deleteButton = this.addConditionDeleteBtn;
|
||||
if (await deleteButton.count() > 0) {
|
||||
await deleteButton.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
await this.confirmButton.click();
|
||||
await this.page.waitForTimeout(1000);
|
||||
}
|
||||
}
|
||||
|
||||
async verifyConditionValueInputValue(value, index = 0) {
|
||||
await expect(this.valueInput.nth(index).locator('input')).toHaveValue(value);
|
||||
}
|
||||
|
||||
async clearAndFillConditionValue(value, index = 0) {
|
||||
await this.valueInput.nth(index).locator('input').clear();
|
||||
await this.valueInput.nth(index).locator('input').fill(value);
|
||||
}
|
||||
|
||||
async verifyDeleteButtonCount(expectedCount) {
|
||||
const count = await this.deleteConditionBtn.count();
|
||||
expect(count).toBeLessThan(expectedCount);
|
||||
}
|
||||
|
||||
async tryToSaveWithoutValidConditions() {
|
||||
await this.addConditionSaveButton.click();
|
||||
await this.page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
async verifyNotificationVisible() {
|
||||
const notification = this.qNotificationMessage;
|
||||
if (await notification.count() > 0) {
|
||||
await expect(notification.first()).toBeVisible();
|
||||
}
|
||||
await this.page.waitForTimeout(2000);
|
||||
}
|
||||
|
||||
async fillPartialCondition(columnName) {
|
||||
await this.columnSelect.first().locator('input').click();
|
||||
await this.columnSelect.first().locator('input').fill(columnName);
|
||||
await this.page.waitForTimeout(300);
|
||||
const options = this.qMenu.locator('.q-item');
|
||||
if (await options.count() > 0) {
|
||||
await options.first().click();
|
||||
}
|
||||
}
|
||||
|
||||
async selectOperatorFromMenu(operator) {
|
||||
await this.operatorSelect.first().click();
|
||||
await this.page.waitForTimeout(500);
|
||||
const visibleMenu = this.qMenu.last();
|
||||
await visibleMenu.locator('.q-item').getByText(operator, { exact: true }).click();
|
||||
}
|
||||
|
||||
async verifyConfirmationDialog() {
|
||||
const dialog = this.qDialog;
|
||||
if (await dialog.count() > 0) {
|
||||
await expect(dialog.first()).toBeVisible();
|
||||
await this.confirmButton.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
import { test, expect } from "../baseFixtures.js";
|
||||
import logData from "../../fixtures/log.json";
|
||||
import logsdata from "../../../test-data/logs_data.json";
|
||||
import PageManager from "../../pages/page-manager.js";
|
||||
const testLogger = require('../utils/test-logger.js');
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.use({
|
||||
contextOptions: {
|
||||
slowMo: 1000
|
||||
}
|
||||
});
|
||||
|
||||
async function login(page) {
|
||||
await page.goto(process.env["ZO_BASE_URL"]);
|
||||
if (await page.getByText('Login as internal user').isVisible()) {
|
||||
await page.getByText('Login as internal user').click();
|
||||
}
|
||||
await page.waitForTimeout(1000);
|
||||
await page
|
||||
.locator('[data-cy="login-user-id"]')
|
||||
.fill(process.env["ZO_ROOT_USER_EMAIL"]);
|
||||
await page.locator("label").filter({ hasText: "Password *" }).click();
|
||||
await page
|
||||
.locator('[data-cy="login-password"]')
|
||||
.fill(process.env["ZO_ROOT_USER_PASSWORD"]);
|
||||
await page.locator('[data-cy="login-sign-in"]').click();
|
||||
}
|
||||
|
||||
async function ingestion(page, streamName = "e2e_automate_conditions") {
|
||||
const orgId = process.env["ORGNAME"];
|
||||
const basicAuthCredentials = Buffer.from(
|
||||
`${process.env["ZO_ROOT_USER_EMAIL"]}:${process.env["ZO_ROOT_USER_PASSWORD"]}`
|
||||
).toString('base64');
|
||||
|
||||
const headers = {
|
||||
"Authorization": `Basic ${basicAuthCredentials}`,
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
const response = await page.evaluate(async ({ url, headers, orgId, streamName, logsdata }) => {
|
||||
const fetchResponse = await fetch(`${url}/api/${orgId}/${streamName}/_json`, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(logsdata)
|
||||
});
|
||||
return await fetchResponse.json();
|
||||
}, {
|
||||
url: process.env.INGESTION_URL,
|
||||
headers: headers,
|
||||
orgId: orgId,
|
||||
streamName: streamName,
|
||||
logsdata: logsdata
|
||||
});
|
||||
testLogger.debug('API response received', { response, streamName });
|
||||
}
|
||||
|
||||
test.describe("Pipeline Conditions - Comprehensive Tests", () => {
|
||||
let pageManager;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await login(page);
|
||||
pageManager = new PageManager(page);
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
// Ingest data to unique streams for each test to avoid conflicts
|
||||
await ingestion(page, "e2e_conditions_basic");
|
||||
await ingestion(page, "e2e_conditions_groups");
|
||||
await ingestion(page, "e2e_conditions_validation");
|
||||
await ingestion(page, "e2e_conditions_precedence");
|
||||
await ingestion(page, "e2e_conditions_multiple");
|
||||
await ingestion(page, "e2e_conditions_delete");
|
||||
await ingestion(page, "e2e_conditions_operators");
|
||||
|
||||
await page.goto(
|
||||
`${logData.logsUrl}?org_identifier=${process.env["ORGNAME"]}`
|
||||
);
|
||||
await page.waitForTimeout(2000);
|
||||
});
|
||||
|
||||
test("should create, edit, and test basic condition operations with multiple operators", async ({ page }) => {
|
||||
const pipelineName = `pipeline-basic-ops-${Math.random().toString(36).substring(7)}`;
|
||||
|
||||
// Create pipeline with condition
|
||||
await pageManager.pipelinesPage.createPipelineWithCondition("e2e_conditions_basic");
|
||||
|
||||
// Verify condition dialog is open
|
||||
await pageManager.pipelinesPage.verifyConditionDialogOpen();
|
||||
|
||||
// Test 1: Verify first condition shows "if" label
|
||||
await pageManager.pipelinesPage.verifyFirstConditionLabel("if");
|
||||
|
||||
// Test 2: Fill first condition with "Contains" operator
|
||||
await pageManager.pipelinesPage.fillCondition("kubernetes_container_name", "Contains", "prometheus", 0);
|
||||
await pageManager.pipelinesPage.verifyConditionValueInputValue("prometheus", 0);
|
||||
|
||||
// Test 3: Add second condition with "=" operator
|
||||
await pageManager.pipelinesPage.addNewCondition();
|
||||
await pageManager.pipelinesPage.fillCondition("kubernetes_host", "=", "test-host", 1);
|
||||
|
||||
// Test 4: Verify both conditions exist
|
||||
await pageManager.pipelinesPage.verifyConditionCount(2);
|
||||
|
||||
// Test 5: Toggle second condition to OR operator
|
||||
await pageManager.pipelinesPage.toggleConditionOperator(0);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Test 6: Add third condition and test other operators
|
||||
await pageManager.pipelinesPage.addNewCondition();
|
||||
await pageManager.pipelinesPage.fillCondition("kubernetes_pod_name", "!=", "excluded-pod", 2);
|
||||
await pageManager.pipelinesPage.verifyConditionCount(3);
|
||||
|
||||
// Test 7: Delete second condition (not first, not last)
|
||||
await pageManager.pipelinesPage.deleteConditionByIndex(1);
|
||||
await page.waitForTimeout(500);
|
||||
await pageManager.pipelinesPage.verifyConditionCount(2);
|
||||
|
||||
// Test 8: Verify first condition cannot be deleted (Bug #9075)
|
||||
await pageManager.pipelinesPage.verifyDeleteButtonCount(2);
|
||||
|
||||
// Save and complete pipeline
|
||||
await pageManager.pipelinesPage.saveConditionAndCompletePipeline(pipelineName);
|
||||
await ingestion(page);
|
||||
|
||||
// Test 9: Edit existing condition
|
||||
await pageManager.pipelinesPage.openPipelineMenu();
|
||||
await page.waitForTimeout(1000);
|
||||
await pageManager.pipelinesPage.openPipelineForEdit(pipelineName);
|
||||
await pageManager.pipelinesPage.clickConditionNode();
|
||||
await pageManager.pipelinesPage.verifyConditionDialogOpen();
|
||||
|
||||
// Verify existing value and modify
|
||||
await pageManager.pipelinesPage.verifyConditionValueInputValue("prometheus", 0);
|
||||
await pageManager.pipelinesPage.clearAndFillConditionValue("modified-value", 0);
|
||||
|
||||
await pageManager.pipelinesPage.saveCondition();
|
||||
await page.waitForTimeout(1000);
|
||||
await pageManager.pipelinesPage.savePipeline();
|
||||
|
||||
// Cleanup
|
||||
await pageManager.pipelinesPage.openPipelineMenu();
|
||||
await pageManager.pipelinesPage.deletePipelineByName(pipelineName);
|
||||
});
|
||||
|
||||
test("should handle condition groups, nesting, and reordering", async ({ page }) => {
|
||||
await pageManager.pipelinesPage.createPipelineWithCondition("e2e_conditions_groups");
|
||||
|
||||
// Test 1: Add first condition
|
||||
await pageManager.pipelinesPage.fillCondition("kubernetes_container_name", "=", "test-A", 0);
|
||||
|
||||
// Test 2: Add nested condition group
|
||||
await pageManager.pipelinesPage.addConditionGroup();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Verify nested group exists
|
||||
await pageManager.pipelinesPage.verifyNestedGroupsExist();
|
||||
|
||||
// Test 3: Add conditions in nested group
|
||||
const addConditionButtons = pageManager.pipelinesPage.addConditionButton;
|
||||
const buttonCount = await addConditionButtons.count();
|
||||
expect(buttonCount).toBeGreaterThan(1);
|
||||
|
||||
// Fill the first condition in nested group
|
||||
await pageManager.pipelinesPage.fillCondition("kubernetes_namespace_name", "=", "test-B", 1);
|
||||
|
||||
// Add another condition in nested group
|
||||
await addConditionButtons.nth(1).click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Fill the second condition in nested group
|
||||
await pageManager.pipelinesPage.fillCondition("kubernetes_pod_name", "=", "test-C", 2);
|
||||
|
||||
// Test 4: Verify group-level tabs exist for nested group
|
||||
await pageManager.pipelinesPage.verifyScheduledAlertTabsVisible();
|
||||
|
||||
// Test 5: Test reorder button with actual reordering validation
|
||||
const reorderButtons = pageManager.pipelinesPage.reorderBtn;
|
||||
const reorderButtonCount = await reorderButtons.count();
|
||||
expect(reorderButtonCount).toBeGreaterThan(1);
|
||||
|
||||
// Capture order of condition values before reorder
|
||||
const beforeValues = await pageManager.pipelinesPage.captureConditionValues();
|
||||
testLogger.info('Condition values before reorder:', { beforeValues });
|
||||
|
||||
// Take screenshot before reorder
|
||||
const beforeScreenshot = await pageManager.pipelinesPage.captureConditionScreenshot();
|
||||
|
||||
// Click reorder button at nested level
|
||||
await pageManager.pipelinesPage.clickReorderButton(1);
|
||||
|
||||
// Capture order of condition values after reorder
|
||||
const afterValues = await pageManager.pipelinesPage.captureConditionValues();
|
||||
testLogger.info('Condition values after reorder:', { afterValues });
|
||||
|
||||
// Take screenshot after reorder
|
||||
const afterScreenshot = await pageManager.pipelinesPage.captureConditionScreenshot();
|
||||
|
||||
// Verify screenshots were captured
|
||||
expect(beforeScreenshot.length).toBeGreaterThan(0);
|
||||
expect(afterScreenshot.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify that the order has changed (arrays should not be equal)
|
||||
expect(beforeValues.length).toBe(afterValues.length);
|
||||
expect(beforeValues).not.toEqual(afterValues);
|
||||
|
||||
// Verify reorder button is still visible after click
|
||||
await expect(reorderButtons.nth(1)).toBeVisible();
|
||||
|
||||
// Cancel without saving
|
||||
await pageManager.pipelinesPage.cancelConditionDialog();
|
||||
await pageManager.pipelinesPage.verifyConfirmationDialog();
|
||||
});
|
||||
|
||||
test("should validate fields and show proper error messages", async ({ page }) => {
|
||||
await pageManager.pipelinesPage.createPipelineWithCondition("e2e_conditions_validation");
|
||||
|
||||
// Test 1: Verify guidelines are displayed
|
||||
await pageManager.pipelinesPage.verifyNoteContainer();
|
||||
const noteInfo = pageManager.pipelinesPage.noteInfo;
|
||||
await expect(noteInfo).toContainText('""');
|
||||
await expect(noteInfo).toContainText('null');
|
||||
|
||||
// Test 2: Try to save without any valid conditions
|
||||
await pageManager.pipelinesPage.tryToSaveWithoutValidConditions();
|
||||
|
||||
// Should show error notification
|
||||
await pageManager.pipelinesPage.verifyNotificationVisible();
|
||||
|
||||
// Test 3: Fill partial condition (missing value)
|
||||
await pageManager.pipelinesPage.fillPartialCondition("kubernetes");
|
||||
await pageManager.pipelinesPage.selectOperatorFromMenu("=");
|
||||
|
||||
// Try to save without value
|
||||
await pageManager.pipelinesPage.tryToSaveWithoutValidConditions();
|
||||
|
||||
// Test 4: Test cancel with unsaved changes
|
||||
await pageManager.pipelinesPage.valueInput.first().locator('input').fill("test-value");
|
||||
await pageManager.pipelinesPage.addNewCondition();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await pageManager.pipelinesPage.cancelConditionDialog();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Should show confirmation dialog
|
||||
await pageManager.pipelinesPage.verifyConfirmationDialog();
|
||||
});
|
||||
|
||||
test("should test complex conditions and operator precedence (A OR B AND C)", async ({ page }) => {
|
||||
const pipelineName = `pipeline-complex-${Math.random().toString(36).substring(7)}`;
|
||||
|
||||
await pageManager.pipelinesPage.createPipelineWithCondition("e2e_conditions_precedence");
|
||||
|
||||
// Create: A OR B AND C (tests operator precedence)
|
||||
// Condition A
|
||||
await pageManager.pipelinesPage.fillCondition("kubernetes_container_name", "=", "value-A", 0);
|
||||
|
||||
// Condition B with OR
|
||||
await pageManager.pipelinesPage.addNewCondition();
|
||||
await pageManager.pipelinesPage.toggleConditionOperator(0);
|
||||
await page.waitForTimeout(300);
|
||||
await pageManager.pipelinesPage.fillCondition("kubernetes_host", "=", "value-B", 1);
|
||||
|
||||
// Condition C with AND (default)
|
||||
await pageManager.pipelinesPage.addNewCondition();
|
||||
await pageManager.pipelinesPage.fillCondition("kubernetes_pod_name", "=", "value-C", 2);
|
||||
|
||||
// Verify 3 conditions exist
|
||||
await pageManager.pipelinesPage.verifyConditionCount(3);
|
||||
|
||||
// Verify operators are set correctly
|
||||
const operatorLabels = pageManager.pipelinesPage.operatorLabels;
|
||||
if (await operatorLabels.count() >= 2) {
|
||||
await pageManager.pipelinesPage.verifyOperatorLabel("OR", 0);
|
||||
}
|
||||
|
||||
await pageManager.pipelinesPage.saveConditionAndCompletePipeline(pipelineName);
|
||||
await ingestion(page);
|
||||
|
||||
await pageManager.pipelinesPage.openPipelineMenu();
|
||||
await pageManager.pipelinesPage.deletePipelineByName(pipelineName);
|
||||
});
|
||||
|
||||
test("should test multiple conditions: A, B, C, and D", async ({ page }) => {
|
||||
const pipelineName = `pipeline-multiple-${Math.random().toString(36).substring(7)}`;
|
||||
|
||||
await pageManager.pipelinesPage.createPipelineWithCondition("e2e_conditions_multiple");
|
||||
|
||||
// Wait for condition dialog to be fully loaded
|
||||
await pageManager.pipelinesPage.verifyConditionDialogOpen();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Condition A at root level
|
||||
await pageManager.pipelinesPage.fillCondition("kubernetes_container_name", "=", "value-A", 0);
|
||||
|
||||
// Add condition B
|
||||
await pageManager.pipelinesPage.addNewCondition();
|
||||
await page.waitForTimeout(500);
|
||||
await pageManager.pipelinesPage.fillCondition("kubernetes_host", "=", "value-B", 1);
|
||||
|
||||
// Add condition C
|
||||
await pageManager.pipelinesPage.addNewCondition();
|
||||
await page.waitForTimeout(500);
|
||||
await pageManager.pipelinesPage.fillCondition("kubernetes_pod_name", "=", "value-C", 2);
|
||||
|
||||
// Add condition D
|
||||
await pageManager.pipelinesPage.addNewCondition();
|
||||
await page.waitForTimeout(500);
|
||||
await pageManager.pipelinesPage.fillCondition("kubernetes_namespace_name", "=", "value-D", 3);
|
||||
|
||||
// Verify all 4 conditions exist
|
||||
await pageManager.pipelinesPage.verifyConditionCount(4);
|
||||
|
||||
await pageManager.pipelinesPage.saveConditionAndCompletePipeline(pipelineName);
|
||||
await ingestion(page);
|
||||
|
||||
await pageManager.pipelinesPage.openPipelineMenu();
|
||||
await pageManager.pipelinesPage.deletePipelineByName(pipelineName);
|
||||
});
|
||||
|
||||
test("should delete condition node from pipeline", async ({ page }) => {
|
||||
const pipelineName = `pipeline-delete-${Math.random().toString(36).substring(7)}`;
|
||||
|
||||
// Create pipeline with condition
|
||||
await pageManager.pipelinesPage.createPipelineWithCondition("e2e_conditions_delete");
|
||||
await pageManager.pipelinesPage.fillCondition("kubernetes_container_name", "=", "test", 0);
|
||||
await pageManager.pipelinesPage.saveConditionAndCompletePipeline(pipelineName);
|
||||
|
||||
// Open pipeline in edit mode
|
||||
await pageManager.pipelinesPage.openPipelineMenu();
|
||||
await page.waitForTimeout(1000);
|
||||
await pageManager.pipelinesPage.openPipelineForEdit(pipelineName);
|
||||
|
||||
// Click on condition node to open
|
||||
await pageManager.pipelinesPage.clickConditionNode();
|
||||
|
||||
// Verify delete button exists and click it
|
||||
await pageManager.pipelinesPage.clickDeleteConditionNode();
|
||||
|
||||
// Verify dialog closed
|
||||
await pageManager.pipelinesPage.verifyConditionDialogClosed();
|
||||
|
||||
// After deleting condition node, reconnect source to destination
|
||||
await page.waitForTimeout(1000);
|
||||
await pageManager.pipelinesPage.reconnectSourceToDestination();
|
||||
|
||||
// Save the updated pipeline
|
||||
await pageManager.pipelinesPage.savePipeline();
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Cleanup
|
||||
await pageManager.pipelinesPage.openPipelineMenu();
|
||||
await pageManager.pipelinesPage.deletePipelineByName(pipelineName);
|
||||
});
|
||||
|
||||
test("should test different comparison operators (>=, !=, Contains, NotContains)", async ({ page }) => {
|
||||
const pipelineName = `pipeline-operators-${Math.random().toString(36).substring(7)}`;
|
||||
|
||||
await pageManager.pipelinesPage.createPipelineWithCondition("e2e_conditions_operators");
|
||||
|
||||
// Test >= operator
|
||||
await pageManager.pipelinesPage.fillCondition("kubernetes_container_name", ">=", "100", 0);
|
||||
|
||||
// Add != operator condition
|
||||
await pageManager.pipelinesPage.addNewCondition();
|
||||
await pageManager.pipelinesPage.fillCondition("kubernetes_host", "!=", "excluded", 1);
|
||||
|
||||
// Add NotContains operator condition
|
||||
await pageManager.pipelinesPage.addNewCondition();
|
||||
await pageManager.pipelinesPage.fillCondition("kubernetes_pod_name", "NotContains", "ignore", 2);
|
||||
|
||||
// Verify all 3 conditions exist
|
||||
await pageManager.pipelinesPage.verifyConditionCount(3);
|
||||
|
||||
await pageManager.pipelinesPage.saveConditionAndCompletePipeline(pipelineName);
|
||||
await ingestion(page);
|
||||
|
||||
await pageManager.pipelinesPage.openPipelineMenu();
|
||||
await pageManager.pipelinesPage.deletePipelineByName(pipelineName);
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,7 @@ import store from "@/test/unit/helpers/store";
|
||||
import { installQuasar } from "@/test/unit/helpers";
|
||||
import router from "@/test/unit/helpers/router";
|
||||
import { generateWhereClause } from "@/utils/alerts/alertQueryBuilder";
|
||||
import { detectConditionsVersion } from "@/utils/alerts/alertDataTransforms";
|
||||
|
||||
import PreviewAlert from "@/components/alerts/PreviewAlert.vue";
|
||||
|
||||
@@ -1159,6 +1160,134 @@ describe("AddAlert Component", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('V2 Conditions Version Field Placement', () => {
|
||||
let w: any;
|
||||
beforeEach(() => {
|
||||
w = mount(AddAlert, { global: { provide: { store }, plugins: [i18n, router] } });
|
||||
w.vm.formData.name = "test_v2_alert";
|
||||
w.vm.formData.stream_name = "_rundata";
|
||||
w.vm.formData.stream_type = "logs";
|
||||
w.vm.formData.is_real_time = false;
|
||||
w.vm.scheduledAlertRef = { tab: 'custom' };
|
||||
w.vm.isAggregationEnabled = false;
|
||||
});
|
||||
|
||||
it('should wrap V2 conditions with version field INSIDE conditions object when saving', () => {
|
||||
// Set up V2 format conditions
|
||||
const v2Conditions = {
|
||||
filterType: 'group',
|
||||
logicalOperator: 'AND',
|
||||
groupId: 'test-group',
|
||||
conditions: [
|
||||
{
|
||||
id: 'test-id',
|
||||
filterType: 'condition',
|
||||
column: 'status',
|
||||
operator: '=',
|
||||
value: '200',
|
||||
logicalOperator: 'AND',
|
||||
}
|
||||
]
|
||||
};
|
||||
w.vm.formData.query_condition.conditions = v2Conditions;
|
||||
w.vm.formData.query_condition.aggregation = null;
|
||||
w.vm.formData.query_condition.type = 'custom';
|
||||
|
||||
const payload = w.vm.getAlertPayload();
|
||||
|
||||
// Simulate the version wrapping logic from onSubmit()
|
||||
const version = detectConditionsVersion(w.vm.formData.query_condition.conditions);
|
||||
if (version === 2) {
|
||||
payload.query_condition.conditions = {
|
||||
version: 2,
|
||||
conditions: w.vm.formData.query_condition.conditions,
|
||||
};
|
||||
}
|
||||
|
||||
// Backend expects: conditions: { version: 2, conditions: {...} }
|
||||
expect(payload.query_condition.conditions).toHaveProperty('version');
|
||||
expect(payload.query_condition.conditions.version).toBe(2);
|
||||
expect(payload.query_condition.conditions).toHaveProperty('conditions');
|
||||
expect(payload.query_condition.conditions.conditions.filterType).toBe('group');
|
||||
expect(payload.query_condition.conditions.conditions.logicalOperator).toBe('AND');
|
||||
});
|
||||
|
||||
it('should NOT have version field at query_condition level for V2', () => {
|
||||
// Set up V2 format conditions
|
||||
w.vm.formData.query_condition.conditions = {
|
||||
filterType: 'group',
|
||||
logicalOperator: 'OR',
|
||||
groupId: 'test-group',
|
||||
conditions: []
|
||||
};
|
||||
|
||||
const payload = w.vm.getAlertPayload();
|
||||
|
||||
// Simulate the version wrapping logic from onSubmit()
|
||||
const version = detectConditionsVersion(w.vm.formData.query_condition.conditions);
|
||||
if (version === 2) {
|
||||
payload.query_condition.conditions = {
|
||||
version: 2,
|
||||
conditions: w.vm.formData.query_condition.conditions,
|
||||
};
|
||||
}
|
||||
|
||||
// Version should NOT be at this level
|
||||
expect(payload.query_condition).not.toHaveProperty('version');
|
||||
// But should be inside conditions
|
||||
expect(payload.query_condition.conditions).toHaveProperty('version');
|
||||
});
|
||||
|
||||
it('should handle V1 conditions without version field', () => {
|
||||
// Set up V1 format conditions (tree-based)
|
||||
w.vm.formData.query_condition.conditions = {
|
||||
or: [
|
||||
{ column: 'status', operator: '=', value: '200', ignore_case: true }
|
||||
]
|
||||
};
|
||||
|
||||
const payload = w.vm.getAlertPayload();
|
||||
|
||||
// V1 format should not have version field at any level
|
||||
expect(payload.query_condition).not.toHaveProperty('version');
|
||||
expect(payload.query_condition.conditions).not.toHaveProperty('version');
|
||||
// Should be transformed to V1 backend format
|
||||
expect(payload.query_condition.conditions).toHaveProperty('or');
|
||||
});
|
||||
|
||||
it('should correctly extract V2 conditions when loading from backend', async () => {
|
||||
// Simulate backend response with V2 format
|
||||
w.vm.formData.query_condition.conditions = {
|
||||
version: 2,
|
||||
conditions: {
|
||||
filterType: 'group',
|
||||
logicalOperator: 'AND',
|
||||
groupId: 'backend-group',
|
||||
conditions: [
|
||||
{
|
||||
id: 'backend-id',
|
||||
filterType: 'condition',
|
||||
column: 'level',
|
||||
operator: '=',
|
||||
value: 'error',
|
||||
logicalOperator: 'AND',
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// Call the data loading logic (simulating mounted hook behavior)
|
||||
await w.vm.$nextTick();
|
||||
|
||||
// After loading, conditions should be extracted from nested structure
|
||||
// The actual extraction happens in the component's data loading logic
|
||||
// This test verifies the structure matches backend expectations
|
||||
expect(w.vm.formData.query_condition.conditions.version).toBe(2);
|
||||
expect(w.vm.formData.query_condition.conditions.conditions).toBeDefined();
|
||||
expect(w.vm.formData.query_condition.conditions.conditions.filterType).toBe('group');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateFormAndNavigateToErrorField and navigateToErrorField', () => {
|
||||
it('navigates on invalid and returns false', async () => {
|
||||
const w = mount(AddAlert, { global: { provide: { store }, plugins: [i18n, router] } });
|
||||
|
||||
@@ -525,6 +525,11 @@ import {
|
||||
removeConditionGroup as removeConditionGroupUtil,
|
||||
transformFEToBE as transformFEToBEUtil,
|
||||
retransformBEToFE as retransformBEToFEUtil,
|
||||
detectConditionsVersion,
|
||||
convertV0ToV2,
|
||||
convertV1ToV2,
|
||||
convertV1BEToV2,
|
||||
ensureIds,
|
||||
type TransformContext,
|
||||
} from "@/utils/alerts/alertDataTransforms";
|
||||
|
||||
@@ -537,12 +542,18 @@ const defaultValue: any = () => {
|
||||
query_condition: {
|
||||
conditions:
|
||||
{
|
||||
"or": [
|
||||
filterType: "group",
|
||||
logicalOperator: "AND",
|
||||
groupId: "",
|
||||
conditions: [
|
||||
{
|
||||
"column": "",
|
||||
"operator": ">",
|
||||
"value": "",
|
||||
"ignore_case": false
|
||||
filterType: "condition",
|
||||
column: "",
|
||||
operator: ">",
|
||||
value: "",
|
||||
values: [],
|
||||
logicalOperator: "AND",
|
||||
id: ""
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1205,20 +1216,26 @@ export default defineComponent({
|
||||
const conditions: any[] = [];
|
||||
query.fields.filter.forEach((filter: any) => {
|
||||
if (filter.type === 'list' && filter.values && filter.values.length > 0) {
|
||||
// V2: Create condition with filterType and logicalOperator
|
||||
conditions.push({
|
||||
filterType: 'condition',
|
||||
column: filter.column,
|
||||
operator: "=",
|
||||
value: filter.values[0],
|
||||
ignore_case: false
|
||||
values: [],
|
||||
logicalOperator: 'AND',
|
||||
id: getUUID()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (conditions.length > 0) {
|
||||
// V2: Create group with filterType and conditions array
|
||||
formData.value.query_condition.conditions = {
|
||||
filterType: 'group',
|
||||
logicalOperator: 'AND',
|
||||
groupId: getUUID(),
|
||||
label: 'and',
|
||||
items: conditions
|
||||
conditions: conditions
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1503,18 +1520,54 @@ export default defineComponent({
|
||||
id: getUUID(),
|
||||
};
|
||||
});
|
||||
//this is done because
|
||||
//if we are getting the conditions as null or undefined then we need to create a new group
|
||||
//if we are getting the conditions as an object then we need to transform it to the frontend format
|
||||
if(this.formData.query_condition.conditions && ( !Array.isArray(this.formData.query_condition.conditions) && Object.keys(this.formData.query_condition.conditions).length != 0)){
|
||||
this.formData.query_condition.conditions = this.retransformBEToFE(this.formData.query_condition.conditions);
|
||||
// VERSION DETECTION AND CONVERSION
|
||||
// Supports three versions:
|
||||
// - V0: Flat array of conditions with implicit AND between all (no groups)
|
||||
// - V1: Tree-based structure with {and: [...]} or {or: [...]} or {label, items, groupId}
|
||||
// - V2: Linear structure with filterType, logicalOperator per condition
|
||||
|
||||
// Check for V2 format from backend
|
||||
// Backend sends: query_condition: { conditions: { version: 2, conditions: {...} } }
|
||||
if (this.formData.query_condition.conditions?.version === "2" || this.formData.query_condition.conditions?.version === 2) {
|
||||
// V2: Extract the nested conditions object
|
||||
// Backend sends { version: 2, conditions: {...} }, we need just the inner conditions
|
||||
this.formData.query_condition.conditions = ensureIds(this.formData.query_condition.conditions.conditions);
|
||||
} else if(this.formData.query_condition.conditions && ( !Array.isArray(this.formData.query_condition.conditions) && Object.keys(this.formData.query_condition.conditions).length != 0)){
|
||||
// No version field - could be V1 or old V2
|
||||
// Detect version by structure
|
||||
const version = detectConditionsVersion(this.formData.query_condition.conditions);
|
||||
|
||||
if (version === 0) {
|
||||
// V0: Flat array format - convert to V2
|
||||
// V0 had implicit AND between all conditions (no groups)
|
||||
this.formData.query_condition.conditions = ensureIds(convertV0ToV2(this.formData.query_condition.conditions));
|
||||
} else if (version === 1) {
|
||||
// V1 format - need to convert to V2
|
||||
// First check if it's BE format ({and: [...]}) or FE format ({label, items, groupId})
|
||||
if (this.formData.query_condition.conditions.and || this.formData.query_condition.conditions.or) {
|
||||
// V1 Backend format - convert to V2
|
||||
this.formData.query_condition.conditions = ensureIds(convertV1BEToV2(this.formData.query_condition.conditions));
|
||||
} else if (this.formData.query_condition.conditions.label && this.formData.query_condition.conditions.items) {
|
||||
// V1 Frontend format - convert to V2
|
||||
// this wont be executed atleast once but we are keeping it incase any FE logics got saved already
|
||||
this.formData.query_condition.conditions = ensureIds(convertV1ToV2(this.formData.query_condition.conditions));
|
||||
}
|
||||
} else {
|
||||
// V2 format - ensure all IDs exist
|
||||
this.formData.query_condition.conditions = ensureIds(this.formData.query_condition.conditions);
|
||||
}
|
||||
} else if (Array.isArray(this.formData.query_condition.conditions) && this.formData.query_condition.conditions.length > 0) {
|
||||
// V0: Flat array of conditions - convert to V2
|
||||
// This handles the case where conditions is directly an array at the top level
|
||||
this.formData.query_condition.conditions = ensureIds(convertV0ToV2(this.formData.query_condition.conditions));
|
||||
}
|
||||
else if (this.formData.query_condition.conditions == null || this.formData.query_condition.conditions == undefined || this.formData.query_condition.conditions.length == 0 || Object.keys(this.formData.query_condition.conditions).length == 0){
|
||||
|
||||
// No conditions - create empty V2 group
|
||||
this.formData.query_condition.conditions = {
|
||||
filterType: 'group',
|
||||
logicalOperator: 'AND',
|
||||
conditions: [],
|
||||
groupId: getUUID(),
|
||||
label: 'and',
|
||||
items: []
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1654,8 +1707,14 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
// Transform the form data to the backend format
|
||||
payload.query_condition.conditions = this.transformFEToBE(this.formData.query_condition.conditions);
|
||||
// VERSION HANDLING
|
||||
// All conditions are converted to V2 format during data loading,
|
||||
// so we always wrap with version field for backend
|
||||
// Backend expects: query_condition: { conditions: { version: 2, conditions: {...} } }
|
||||
payload.query_condition.conditions = {
|
||||
version: 2,
|
||||
conditions: this.formData.query_condition.conditions,
|
||||
};
|
||||
|
||||
if (this.beingUpdated) {
|
||||
payload.folder_id = this.router.currentRoute.value.query.folder || "default";
|
||||
|
||||
@@ -1325,66 +1325,27 @@ export default defineComponent({
|
||||
//localAllAlerts is the alerts that we use to store
|
||||
localAllAlerts = localAllAlerts.map((data: any) => {
|
||||
let conditions = "--";
|
||||
//this is deprecated because we are using the new condition format
|
||||
//the new format looks like this
|
||||
// {
|
||||
// "or": [
|
||||
// {
|
||||
// "column": "_timestamp",
|
||||
// "operator": "<=",
|
||||
// "value": "100",
|
||||
// "ignore_case": false
|
||||
// },
|
||||
// {
|
||||
// "column": "job",
|
||||
// "operator": "not_contains",
|
||||
// "value": "12",
|
||||
// "ignore_case": true
|
||||
// },
|
||||
// {
|
||||
// "or": [
|
||||
// {
|
||||
// "column": "job",
|
||||
// "operator": "contains",
|
||||
// "value": "1222",
|
||||
// "ignore_case": true
|
||||
// },
|
||||
// {
|
||||
// "column": "level",
|
||||
// "operator": "not_contains",
|
||||
// "value": "dsff",
|
||||
// "ignore_case": true
|
||||
// },
|
||||
// {
|
||||
// "or": [
|
||||
// {
|
||||
// "column": "job",
|
||||
// "operator": "=",
|
||||
// "value": "111",
|
||||
// "ignore_case": true
|
||||
// },
|
||||
// {
|
||||
// "column": "level",
|
||||
// "operator": "contains",
|
||||
// "value": "1222",
|
||||
// "ignore_case": true
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// "column": "log",
|
||||
// "operator": "!=",
|
||||
// "value": "33",
|
||||
// "ignore_case": true
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
//converted into
|
||||
// (_timestamp <= '100' OR job not_contains '12' OR (job contains '1222' OR level not_contains 'dsff' OR (job = '111' OR level contains '1222') OR log != '33'))
|
||||
|
||||
// Handle different condition formats based on structure
|
||||
if (Object.keys(data.condition).length && data.condition.type == 'custom') {
|
||||
conditions = transformToExpression(data.condition.conditions)
|
||||
const conditionData = data.condition.conditions;
|
||||
|
||||
// Detect format by structure, not by version field (more reliable)
|
||||
if (conditionData?.filterType === 'group') {
|
||||
// V2 format: {filterType: "group", logicalOperator: "AND", conditions: [...]}
|
||||
// This works whether or not there's a version field
|
||||
conditions = transformV2ToExpression(conditionData);
|
||||
} else if (conditionData?.version === 2 && conditionData?.conditions) {
|
||||
// V2 format with version wrapper: {version: 2, conditions: {filterType: "group", ...}}
|
||||
conditions = transformV2ToExpression(conditionData.conditions);
|
||||
} else if (conditionData?.or || conditionData?.and) {
|
||||
// V1 format: {or: [...]} or {and: [...]}
|
||||
conditions = transformToExpression(conditionData);
|
||||
} else {
|
||||
// V0 format (legacy query_condition) or other formats
|
||||
// Try to display as-is if it's a string, otherwise show placeholder
|
||||
conditions = typeof conditionData === 'string' ? conditionData : '--';
|
||||
}
|
||||
} else if (data.condition.sql) {
|
||||
conditions = data.condition.sql;
|
||||
} else if (data.condition.promql) {
|
||||
@@ -2383,6 +2344,7 @@ export default defineComponent({
|
||||
folderIdToBeCloned.value = folderId.value;
|
||||
};
|
||||
|
||||
// V1 format: {or: [...]} or {and: [...]}
|
||||
function transformToExpression(data: any, wrap = true): any {
|
||||
if (!data) return null;
|
||||
|
||||
@@ -2406,6 +2368,46 @@ export default defineComponent({
|
||||
const joined = parts.join(` ${label} `);
|
||||
return wrap ? `(${joined})` : joined;
|
||||
}
|
||||
|
||||
// V2 format: {filterType: "group", logicalOperator: "AND", conditions: [...]}
|
||||
function transformV2ToExpression(group: any, isRoot = true): string {
|
||||
if (!group || !group.conditions || group.conditions.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
group.conditions.forEach((item: any, index: number) => {
|
||||
if (item.filterType === 'group') {
|
||||
// Nested group - recursively build expression
|
||||
const nestedExpr = transformV2ToExpression(item, false);
|
||||
if (nestedExpr) {
|
||||
// Use parent group's logicalOperator for the operator before nested groups
|
||||
if (index > 0) {
|
||||
parts.push(`${group.logicalOperator} (${nestedExpr})`);
|
||||
} else {
|
||||
parts.push(`(${nestedExpr})`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Condition
|
||||
const column = item.column || 'field';
|
||||
const operator = item.operator || '=';
|
||||
const value = typeof item.value === 'string' ? `'${item.value}'` : item.value;
|
||||
const conditionStr = `${column} ${operator} ${value}`;
|
||||
|
||||
// Add operator before condition (except for first item)
|
||||
if (index > 0 && item.logicalOperator) {
|
||||
parts.push(`${item.logicalOperator} ${conditionStr}`);
|
||||
} else {
|
||||
parts.push(conditionStr);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const joined = parts.join(' ');
|
||||
return isRoot ? `(${joined})` : joined;
|
||||
}
|
||||
//this function is used to filter the alerts by the local search not the global search
|
||||
//this will be used when the user is searching for the alerts in the same folder
|
||||
const filterAlertsByQuery = (query: string) => {
|
||||
|
||||
@@ -32,6 +32,7 @@ describe('FilterCondition.vue Branch Coverage', () => {
|
||||
column: '',
|
||||
operator: '',
|
||||
value: '',
|
||||
logicalOperator: 'AND',
|
||||
},
|
||||
streamFields: [
|
||||
{ label: 'Field 1', value: 'field1' },
|
||||
@@ -41,6 +42,7 @@ describe('FilterCondition.vue Branch Coverage', () => {
|
||||
index: 0,
|
||||
label: 'AND',
|
||||
depth: 0,
|
||||
isFirstInGroup: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -68,12 +70,17 @@ describe('FilterCondition.vue Branch Coverage', () => {
|
||||
expect(labelElement.text().trim()).toBe('if');
|
||||
});
|
||||
|
||||
it('should show computed label when index is not 0', async () => {
|
||||
it('should show lowercase operator when not first in group', async () => {
|
||||
const wrapper = mount(FilterCondition, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
condition: {
|
||||
...defaultProps.condition,
|
||||
logicalOperator: 'OR',
|
||||
},
|
||||
index: 1, // Branch condition: index != 0
|
||||
depth: 0,
|
||||
isFirstInGroup: false,
|
||||
label: 'OR',
|
||||
},
|
||||
global: {
|
||||
@@ -84,17 +91,20 @@ describe('FilterCondition.vue Branch Coverage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
// Branch: computedLabel (when index != 0)
|
||||
const labelElement = wrapper.find('.tw-text-sm');
|
||||
expect(labelElement.text().trim()).toBe('OR');
|
||||
// Branch: computedLabel (when not first in group) - CSS lowercase applied
|
||||
const labelElement = wrapper.find('.tw-lowercase');
|
||||
// Text content is still 'OR' but CSS applies text-transform: lowercase
|
||||
expect(labelElement.exists()).toBe(true);
|
||||
expect(labelElement.text().trim()).toBe('OR'); // Content is uppercase, CSS transforms to lowercase visually
|
||||
});
|
||||
|
||||
it('should show "if" when index is 0 regardless of depth', async () => {
|
||||
it('should show "if" only for first condition in root group (index 0, depth 0)', async () => {
|
||||
const wrapper = mount(FilterCondition, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
index: 0,
|
||||
depth: 1, // Even with depth != 0, index 0 should show "if"
|
||||
depth: 0, // V2: "if" only shown for root group first condition
|
||||
isFirstInGroup: true,
|
||||
label: 'AND',
|
||||
},
|
||||
global: {
|
||||
@@ -105,10 +115,34 @@ describe('FilterCondition.vue Branch Coverage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
// Branch: index == 0 shows "if" regardless of depth (updated logic)
|
||||
// Branch: index == 0 && depth == 0 shows "if"
|
||||
const labelElement = wrapper.find('.tw-text-sm');
|
||||
expect(labelElement.text().trim()).toBe('if');
|
||||
});
|
||||
|
||||
it('should show empty space for first condition in nested groups', async () => {
|
||||
const wrapper = mount(FilterCondition, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
index: 0,
|
||||
depth: 1, // Nested group
|
||||
isFirstInGroup: true,
|
||||
label: 'AND',
|
||||
},
|
||||
global: {
|
||||
plugins: [Quasar, mockI18n],
|
||||
provide: {
|
||||
store: mockStore,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Branch: isFirstInGroup && depth > 0 shows empty space (no label)
|
||||
const labelContainer = wrapper.find('.tw-min-w-\\[60px\\]');
|
||||
expect(labelContainer.exists()).toBe(true);
|
||||
// Should not contain operator label or "if"
|
||||
expect(labelContainer.text().trim()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -371,11 +405,16 @@ describe('FilterCondition.vue Branch Coverage', () => {
|
||||
});
|
||||
|
||||
describe('Computed Label Branch Coverage', () => {
|
||||
it('should return the correct computed label', async () => {
|
||||
it('should return the correct computed label when not first in group', async () => {
|
||||
const wrapper = mount(FilterCondition, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
label: 'CUSTOM_LABEL',
|
||||
isFirstInGroup: false, // V2: Only show label when not first
|
||||
condition: {
|
||||
...defaultProps.condition,
|
||||
logicalOperator: 'AND',
|
||||
},
|
||||
},
|
||||
global: {
|
||||
plugins: [Quasar, mockI18n],
|
||||
@@ -385,8 +424,26 @@ describe('FilterCondition.vue Branch Coverage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
// Test computedLabel computed property
|
||||
expect((wrapper.vm as any).computedLabel).toBe('CUSTOM_LABEL');
|
||||
// Test computedLabel computed property - should return condition's logicalOperator
|
||||
expect((wrapper.vm as any).computedLabel).toBe('AND');
|
||||
});
|
||||
|
||||
it('should return empty string for first condition in group', async () => {
|
||||
const wrapper = mount(FilterCondition, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
isFirstInGroup: true, // First condition in group
|
||||
},
|
||||
global: {
|
||||
plugins: [Quasar, mockI18n],
|
||||
provide: {
|
||||
store: mockStore,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Test computedLabel computed property - should return empty string
|
||||
expect((wrapper.vm as any).computedLabel).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,39 @@
|
||||
<template>
|
||||
<div class=" tw-flex tw-items-start tw-gap-2 tw-flex-no-wrap ">
|
||||
<div class="tw-text-sm tw-w-[20px] tw-mr-2 tw-mt-2">
|
||||
{{
|
||||
index == 0 ? 'if' : computedLabel
|
||||
}}
|
||||
<div class=" tw-flex tw-items-start tw-gap-1 tw-flex-no-wrap ">
|
||||
<!-- V2: Fixed-width left column for alignment -->
|
||||
<!-- All conditions have the same width for the operator/label section -->
|
||||
<div class="tw-flex tw-items-center tw-justify-center tw-mt-2 tw-min-w-[60px]">
|
||||
<!-- First condition in root group: show "if" centered -->
|
||||
<template v-if="index === 0 && depth === 0">
|
||||
<span class="tw-text-sm">if</span>
|
||||
</template>
|
||||
|
||||
<!-- First condition in nested groups: empty space for alignment -->
|
||||
<template v-else-if="isFirstInGroup">
|
||||
<!-- Empty space to maintain alignment -->
|
||||
</template>
|
||||
|
||||
<!-- Other conditions: show operator + toggle button -->
|
||||
<template v-else>
|
||||
<span class="tw-text-sm tw-font-medium tw-min-w-[30px] tw-lowercase">
|
||||
{{ computedLabel }}
|
||||
</span>
|
||||
<!-- Toggle AND/OR button after label -->
|
||||
<q-btn
|
||||
data-test="alert-conditions-toggle-operator-btn"
|
||||
flat
|
||||
dense
|
||||
round
|
||||
size="sm"
|
||||
icon="restart_alt"
|
||||
class="tw-h-[26px] tw-flex-shrink-0 operator-toggle-btn"
|
||||
@click="toggleOperator"
|
||||
>
|
||||
<q-tooltip>
|
||||
Toggle between and/or
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
data-test="alert-conditions-select-column"
|
||||
@@ -112,6 +142,11 @@
|
||||
default: '',
|
||||
required: false,
|
||||
},
|
||||
isFirstInGroup: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
import { ref, computed } from "vue";
|
||||
@@ -151,9 +186,26 @@ const addGroupApiHeader = (groupId: string) => {
|
||||
};
|
||||
|
||||
const computedLabel = computed(() => {
|
||||
// V2: First condition in any group should not show AND/OR operator
|
||||
// Only subsequent conditions show the operator
|
||||
if (props.isFirstInGroup) {
|
||||
return ''; // No operator for first condition in group
|
||||
}
|
||||
// V2: Use condition's logicalOperator if available
|
||||
if (props.condition.logicalOperator) {
|
||||
return props.condition.logicalOperator;
|
||||
}
|
||||
return props.label;
|
||||
});
|
||||
|
||||
// Toggle operator between AND/OR for this condition
|
||||
const toggleOperator = () => {
|
||||
if (props.condition.logicalOperator) {
|
||||
props.condition.logicalOperator = props.condition.logicalOperator === 'AND' ? 'OR' : 'AND';
|
||||
emits('input:update', 'conditions', props.condition);
|
||||
}
|
||||
};
|
||||
|
||||
const computedInputWidth = computed(() => {
|
||||
// If custom width is provided, use it; otherwise use default responsive width
|
||||
return props.inputWidth || (store.state.isAiChatEnabled ? '' : 'xl:tw-min-w-[200px] lg:tw-min-w-[90px] lg:tw-w-fit');
|
||||
@@ -180,6 +232,13 @@ const filterColumns = (val: string, update: Function) => {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style >
|
||||
<style scoped>
|
||||
.operator-toggle-btn {
|
||||
color: var(--o2-primary-btn-bg) !important;
|
||||
}
|
||||
|
||||
.operator-toggle-btn:hover {
|
||||
background-color: rgba(var(--o2-primary-btn-bg-rgb), 0.1) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -30,25 +30,30 @@
|
||||
backgroundColor: computedStyleMap
|
||||
}"
|
||||
>
|
||||
<div class="tw-w-fit condition-tabs el-border">
|
||||
<AppTabs
|
||||
data-test="scheduled-alert-tabs"
|
||||
:tabs="tabOptions"
|
||||
class="tw-h-[20px] custom-tabs-selection-container"
|
||||
v-model:active-tab="label"
|
||||
@update:active-tab="toggleLabel"
|
||||
/>
|
||||
<!-- V2: Group-level toggle only for nested groups (depth > 0) -->
|
||||
<!-- Root group (depth 0) doesn't need toggle - its logicalOperator is dummy -->
|
||||
<div v-if="depth > 0" class="tw-w-fit condition-tabs el-border">
|
||||
<AppTabs
|
||||
data-test="scheduled-alert-tabs"
|
||||
:tabs="tabOptions"
|
||||
class="tw-h-[20px] custom-tabs-selection-container"
|
||||
v-model:active-tab="label"
|
||||
@update:active-tab="toggleLabel"
|
||||
/>
|
||||
</div>
|
||||
<!-- Spacer for root group to maintain consistent spacing -->
|
||||
<div v-else class="tw-h-[14px]"></div>
|
||||
|
||||
<!-- Group content -->
|
||||
|
||||
<div v-if="isOpen" class="tw-overflow-x-auto group-container" :class="store.state.theme === 'dark' ? 'dark-mode-group' : 'light-mode-group'">
|
||||
<!-- Items in group -->
|
||||
<div class="tw-ml-2 tw-whitespace-nowrap " v-for="(item, index) in props.group.items" :key="index">
|
||||
<!-- Items in group (V2 uses 'conditions' array) -->
|
||||
<div class="tw-ml-2 tw-whitespace-nowrap " v-for="(item, index) in props.group.conditions" :key="index">
|
||||
<FilterGroup
|
||||
v-if="isGroup(item)"
|
||||
:group="item"
|
||||
:depth="depth + 1"
|
||||
:is-first-group="index === 0 && depth === 0"
|
||||
@add-condition="emit('add-condition', $event)"
|
||||
@add-group="emit('add-group', $event)"
|
||||
@remove-group="emit('remove-group', $event)"
|
||||
@@ -67,11 +72,12 @@
|
||||
:stream-fields="props.streamFields"
|
||||
@input:update="(name, field) => inputUpdate(name, field)"
|
||||
:index="index"
|
||||
:label="group.label"
|
||||
:label="group.logicalOperator?.toLowerCase() || 'and'"
|
||||
:depth="depth"
|
||||
:input-width="props.conditionInputWidth"
|
||||
:is-first-in-group="index === 0"
|
||||
/>
|
||||
<div class="tw-mb-1" v-if="!(props.disableFirstCondition && index === 0 && depth === 0)">
|
||||
<div class="tw-mb-3" v-if="!(props.disableFirstCondition && index === 0 && depth === 0)">
|
||||
<q-btn data-test="alert-conditions-delete-condition-btn" icon="close" size="10px" flat border-less @click="removeCondition(item.id)" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -182,6 +188,11 @@
|
||||
default: true,
|
||||
required: false,
|
||||
},
|
||||
isFirstGroup: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -198,7 +209,8 @@
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const label = ref(props.group.label);
|
||||
// V2: Use logicalOperator (AND/OR) instead of label (and/or)
|
||||
const label = ref(props.group.logicalOperator?.toLowerCase() || 'and');
|
||||
|
||||
const confirmDialog = ref({
|
||||
show: false,
|
||||
@@ -211,72 +223,87 @@
|
||||
// Watch for prop changes to keep groups in sync with parent
|
||||
watch(() => props.group, (newGroup) => {
|
||||
groups.value = newGroup;
|
||||
label.value = newGroup.label;
|
||||
// V2: Use logicalOperator instead of label
|
||||
label.value = newGroup.logicalOperator?.toLowerCase() || 'and';
|
||||
}, { deep: true });
|
||||
|
||||
const tabOptions = computed(() => [
|
||||
{
|
||||
label: "OR",
|
||||
value: "or",
|
||||
disabled: hasOnlyOneCondition.value,
|
||||
tooltipLabel: hasOnlyOneCondition.value ? "Add at least one more condition to enable AND/OR toggle" : undefined,
|
||||
},
|
||||
{
|
||||
label: "AND",
|
||||
value: "and",
|
||||
disabled: hasOnlyOneCondition.value,
|
||||
tooltipLabel: hasOnlyOneCondition.value ? "Add at least one more condition to enable AND/OR toggle" : undefined,
|
||||
},
|
||||
]);
|
||||
|
||||
function isGroup(item: any) {
|
||||
return item && item.items && Array.isArray(item.items);
|
||||
// V2: Check for filterType === "group" with conditions array
|
||||
if (item && item.filterType === "group" && item.conditions && Array.isArray(item.conditions)) {
|
||||
return true;
|
||||
}
|
||||
// V1 compatibility: Check for items array (legacy)
|
||||
if (item && item.items && Array.isArray(item.items) && item.groupId) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const addCondition = (groupId: string) => {
|
||||
groups.value.items.push({
|
||||
// V2: Create condition with filterType and logicalOperator
|
||||
const newCondition = {
|
||||
filterType: 'condition',
|
||||
column: '',
|
||||
operator: '=',
|
||||
value: '',
|
||||
ignore_case: true,
|
||||
values: [],
|
||||
logicalOperator: groups.value.logicalOperator || 'AND',
|
||||
id: getUUID(),
|
||||
});
|
||||
};
|
||||
groups.value.conditions.push(newCondition);
|
||||
emit('add-condition', groups.value);
|
||||
};
|
||||
|
||||
const addGroup = (groupId: string) => {
|
||||
|
||||
groups.value.items.push({
|
||||
// V2: Create group with filterType, logicalOperator, and conditions array
|
||||
const newGroup = {
|
||||
filterType: 'group',
|
||||
logicalOperator: 'OR',
|
||||
groupId: getUUID(),
|
||||
label: 'or',
|
||||
items: [
|
||||
conditions: [
|
||||
{
|
||||
filterType: 'condition',
|
||||
column: '',
|
||||
operator: '=',
|
||||
value: '',
|
||||
ignore_case: true,
|
||||
values: [],
|
||||
logicalOperator: 'OR',
|
||||
id: getUUID(),
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
groups.value.conditions.push(newGroup);
|
||||
emit('add-group', groups.value);
|
||||
};
|
||||
|
||||
// Toggle AND/OR
|
||||
const toggleLabel = (newLabel?: string) => {
|
||||
// V2: Use logicalOperator instead of label
|
||||
// If newLabel is provided, use it; otherwise toggle
|
||||
if (newLabel) {
|
||||
groups.value.label = newLabel;
|
||||
groups.value.logicalOperator = newLabel.toUpperCase();
|
||||
} else {
|
||||
groups.value.label = groups.value.label === 'and' ? 'or' : 'and';
|
||||
groups.value.logicalOperator = groups.value.logicalOperator === 'AND' ? 'OR' : 'AND';
|
||||
}
|
||||
emit('add-group', groups.value); // optional, sync with parent
|
||||
emit('input:update', 'conditions', groups.value);
|
||||
};
|
||||
|
||||
const removeCondition = (id: string) => {
|
||||
// V2: Use conditions array instead of items
|
||||
// First, check what will happen after removing this condition
|
||||
const itemsAfterRemoval = groups.value.items.filter((item: any) => item.id !== id);
|
||||
const itemsAfterRemoval = groups.value.conditions.filter((item: any) => item.id !== id);
|
||||
const hasConditionsAfterRemoval = itemsAfterRemoval.some((item: any) => !isGroup(item));
|
||||
|
||||
// Count sub-groups that will be deleted
|
||||
@@ -303,14 +330,15 @@
|
||||
};
|
||||
|
||||
const performRemoveCondition = (id: string) => {
|
||||
groups.value.items = groups.value.items.filter((item: any) => item.id !== id);
|
||||
// V2: Use conditions array instead of items
|
||||
groups.value.conditions = groups.value.conditions.filter((item: any) => item.id !== id);
|
||||
|
||||
// Check if there are any conditions left (not sub-groups)
|
||||
const hasConditions = groups.value.items.some((item: any) => !isGroup(item));
|
||||
const hasConditions = groups.value.conditions.some((item: any) => !isGroup(item));
|
||||
|
||||
if (!hasConditions) {
|
||||
// No conditions left, clear all items (including sub-groups) and remove this entire group
|
||||
groups.value.items = [];
|
||||
groups.value.conditions = [];
|
||||
emit('remove-group', props.group.groupId);
|
||||
} else {
|
||||
emit('add-group', groups.value); // update as usual
|
||||
@@ -322,12 +350,13 @@
|
||||
};
|
||||
|
||||
const reorderItems = () => {
|
||||
// V2: Use conditions array instead of items
|
||||
// Separate conditions and groups
|
||||
const conditions = groups.value.items.filter((item: any) => !isGroup(item));
|
||||
const subGroups = groups.value.items.filter((item: any) => isGroup(item));
|
||||
const conditions = groups.value.conditions.filter((item: any) => !isGroup(item));
|
||||
const subGroups = groups.value.conditions.filter((item: any) => isGroup(item));
|
||||
|
||||
// Reorder: conditions first, then groups
|
||||
groups.value.items = [...conditions, ...subGroups];
|
||||
groups.value.conditions = [...conditions, ...subGroups];
|
||||
|
||||
// Emit update
|
||||
emit('add-group', groups.value);
|
||||
@@ -392,18 +421,21 @@ const computedOpacity = computed(() => {
|
||||
|
||||
// Build preview string recursively
|
||||
const buildPreviewString = (group: any): string => {
|
||||
if (!group || !group.items || group.items.length === 0) {
|
||||
// V2: Use conditions array instead of items
|
||||
if (!group || !group.conditions || group.conditions.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
group.items.forEach((item: any) => {
|
||||
group.conditions.forEach((item: any, index: number) => {
|
||||
let conditionStr = '';
|
||||
|
||||
if (isGroup(item)) {
|
||||
// Nested group - recursively build its preview
|
||||
const nestedPreview = buildPreviewString(item);
|
||||
if (nestedPreview) {
|
||||
parts.push(`(${nestedPreview})`);
|
||||
conditionStr = `(${nestedPreview})`;
|
||||
}
|
||||
} else {
|
||||
// Condition - show full condition: column operator value
|
||||
@@ -412,13 +444,19 @@ const buildPreviewString = (group: any): string => {
|
||||
const value = item.value !== undefined && item.value !== null && item.value !== ''
|
||||
? `'${item.value}'`
|
||||
: "''";
|
||||
parts.push(`${column} ${operator} ${value}`);
|
||||
conditionStr = `${column} ${operator} ${value}`;
|
||||
}
|
||||
|
||||
// Add logical operator before condition (except for first condition)
|
||||
if (index > 0 && item.logicalOperator) {
|
||||
parts.push(`${item.logicalOperator.toLowerCase()} ${conditionStr}`);
|
||||
} else {
|
||||
parts.push(conditionStr);
|
||||
}
|
||||
});
|
||||
|
||||
// Join with the group's label (AND/OR)
|
||||
const operator = (group.label || 'or').toUpperCase();
|
||||
return parts.join(` ${operator} `);
|
||||
// Join all parts with spaces (operators are already included)
|
||||
return parts.join(' ');
|
||||
};
|
||||
|
||||
// Computed preview string
|
||||
@@ -428,15 +466,6 @@ const previewString = computed(() => {
|
||||
return preview ? `(${preview})` : '';
|
||||
});
|
||||
|
||||
// Check if group has only one condition and no sub-groups (to disable AND/OR toggle)
|
||||
const hasOnlyOneCondition = computed(() => {
|
||||
const conditions = groups?.value?.items?.filter((item: any) => !isGroup(item));
|
||||
const subGroups = groups?.value?.items?.filter((item: any) => isGroup(item));
|
||||
|
||||
// Disable only if there's exactly 1 condition AND no sub-groups
|
||||
return conditions?.length === 1 && subGroups?.length === 0;
|
||||
});
|
||||
|
||||
// Expose functions for testing
|
||||
defineExpose({
|
||||
isGroup,
|
||||
@@ -451,7 +480,6 @@ defineExpose({
|
||||
hslToCSS,
|
||||
computedStyleMap,
|
||||
computedOpacity,
|
||||
hasOnlyOneCondition,
|
||||
groups,
|
||||
label,
|
||||
isOpen,
|
||||
|
||||
@@ -414,6 +414,12 @@ import alertsService from "../../services/alerts";
|
||||
import useStreams from "@/composables/useStreams";
|
||||
import BaseImport from "../common/BaseImport.vue";
|
||||
import SelectFolderDropDown from "../common/sidebar/SelectFolderDropDown.vue";
|
||||
import {
|
||||
detectConditionsVersion,
|
||||
convertV0ToV2,
|
||||
convertV1ToV2,
|
||||
convertV1BEToV2,
|
||||
} from "@/utils/alerts/alertDataTransforms";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ImportAlert",
|
||||
@@ -732,8 +738,44 @@ export default defineComponent({
|
||||
|
||||
// 6. Validate 'query_condition' field
|
||||
if (input.query_condition && input.query_condition.conditions) {
|
||||
const validateCondition = (condition: any) => {
|
||||
// Check if it's a simple condition
|
||||
const validateV2Condition = (item: any): boolean => {
|
||||
if (item.filterType === 'group') {
|
||||
// V2 group - validate it has conditions array
|
||||
if (!Array.isArray(item.conditions)) {
|
||||
alertErrors.push(
|
||||
`Alert - ${index}: V2 group must have a conditions array.`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// Recursively validate nested conditions
|
||||
return item.conditions.every((nestedItem: any) => validateV2Condition(nestedItem));
|
||||
} else if (item.filterType === 'condition') {
|
||||
// V2 condition - validate required fields
|
||||
if (!item.column || !item.operator || item.value === undefined) {
|
||||
alertErrors.push(
|
||||
`Alert - ${index}: V2 condition must have column, operator, and value.`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// Validate operator for custom type
|
||||
if (
|
||||
input.query_condition.type === "custom" &&
|
||||
!["=", ">", "<", ">=", "<=", "Contains", "NotContains","contains","not_contains"].includes(
|
||||
item.operator,
|
||||
)
|
||||
) {
|
||||
alertErrors.push(
|
||||
`Alert - ${index}: Invalid operator '${item.operator}'. Allowed: '=', '>', '<', '>=', '<=', 'Contains', 'NotContains'.`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateV1Condition = (condition: any) => {
|
||||
// Check if it's a simple condition (V0/V1 format)
|
||||
if (condition.column && condition.operator && condition.value !== undefined) {
|
||||
if (
|
||||
input.query_condition.type === "custom" &&
|
||||
@@ -748,7 +790,7 @@ export default defineComponent({
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's a nested condition with 'and' or 'or'
|
||||
// Check if it's a nested condition with 'and' or 'or' (V1 format)
|
||||
if (condition.and || condition.or) {
|
||||
const conditions = condition.and || condition.or;
|
||||
if (!Array.isArray(conditions)) {
|
||||
@@ -757,7 +799,7 @@ export default defineComponent({
|
||||
);
|
||||
return;
|
||||
}
|
||||
conditions.forEach(validateCondition);
|
||||
conditions.forEach(validateV1Condition);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -767,19 +809,35 @@ export default defineComponent({
|
||||
);
|
||||
};
|
||||
|
||||
// Handle both array format and nested format
|
||||
if (Array.isArray(input.query_condition.conditions)) {
|
||||
// Old format - array of conditions
|
||||
input.query_condition.conditions.forEach((condition:any) => {
|
||||
if (!condition.column || !condition.operator || !condition.value) {
|
||||
let conditionsToValidate = input.query_condition.conditions;
|
||||
|
||||
// Check if conditions is wrapped with version field (new format from backend)
|
||||
if (conditionsToValidate.version !== undefined) {
|
||||
// Wrapped format: { version: 2, conditions: {...} }
|
||||
conditionsToValidate = conditionsToValidate.conditions;
|
||||
}
|
||||
|
||||
// Determine format and validate accordingly
|
||||
if (Array.isArray(conditionsToValidate)) {
|
||||
// V0 format - flat array of conditions
|
||||
conditionsToValidate.forEach((condition:any) => {
|
||||
if (!condition.column || !condition.operator || condition.value === undefined) {
|
||||
alertErrors.push(
|
||||
`Alert - ${index}: Each query condition must have 'column', 'operator', and 'value'.`,
|
||||
);
|
||||
}
|
||||
});
|
||||
} else if (conditionsToValidate.filterType === 'group') {
|
||||
// V2 format - new structure with filterType
|
||||
validateV2Condition(conditionsToValidate);
|
||||
} else if (conditionsToValidate.and || conditionsToValidate.or) {
|
||||
// V1 format - nested conditions with and/or
|
||||
validateV1Condition(conditionsToValidate);
|
||||
} else {
|
||||
// New format - nested conditions with and/or
|
||||
validateCondition(input.query_condition.conditions);
|
||||
// Unknown format
|
||||
alertErrors.push(
|
||||
`Alert - ${index}: Unrecognized query condition format.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
// 7. Validate 'sql' and 'promql'
|
||||
@@ -992,6 +1050,41 @@ export default defineComponent({
|
||||
input.owner = store.state.userInfo.email;
|
||||
input.last_edited_by = store.state.userInfo.email;
|
||||
|
||||
// VERSION DETECTION AND CONVERSION
|
||||
// Convert V0 and V1 conditions to V2 format before creating alert
|
||||
if (input.query_condition && input.query_condition.conditions) {
|
||||
let convertedConditions = input.query_condition.conditions;
|
||||
|
||||
// Check if it's already wrapped with version
|
||||
if (convertedConditions.version === 2 || convertedConditions.version === "2") {
|
||||
// Already wrapped, extract the inner conditions for detection
|
||||
convertedConditions = convertedConditions.conditions;
|
||||
}
|
||||
|
||||
const version = detectConditionsVersion(convertedConditions);
|
||||
|
||||
if (version === 0) {
|
||||
// V0: Flat array format - convert to V2
|
||||
convertedConditions = convertV0ToV2(convertedConditions);
|
||||
} else if (version === 1) {
|
||||
// V1: Tree-based format - convert to V2
|
||||
if (convertedConditions.and || convertedConditions.or) {
|
||||
// V1 Backend format
|
||||
convertedConditions = convertV1BEToV2(convertedConditions);
|
||||
} else if (convertedConditions.label && convertedConditions.items) {
|
||||
// V1 Frontend format
|
||||
convertedConditions = convertV1ToV2(convertedConditions);
|
||||
}
|
||||
}
|
||||
// For version === 2, convertedConditions is already in correct format
|
||||
|
||||
// Backend expects: query_condition: { conditions: { version: 2, conditions: {...} } }
|
||||
input.query_condition.conditions = {
|
||||
version: 2,
|
||||
conditions: convertedConditions,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await alertsService.create_by_alert_id(
|
||||
store.state.selectedOrganization.identifier,
|
||||
|
||||
@@ -442,6 +442,12 @@ import destinationService from "@/services/alert_destination";
|
||||
import jstransform from "@/services/jstransform";
|
||||
import usePipelines from "@/composables/usePipelines";
|
||||
import BaseImport from "../common/BaseImport.vue";
|
||||
import {
|
||||
detectConditionsVersion,
|
||||
convertV0ToV2,
|
||||
convertV1ToV2,
|
||||
convertV1BEToV2,
|
||||
} from "@/utils/alerts/alertDataTransforms";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ImportPipeline",
|
||||
@@ -1122,23 +1128,106 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
const validateConditionNode = (input: any) => {
|
||||
const isValid = !input.nodes.some((node: any) => {
|
||||
return (
|
||||
node.io_type == "default" &&
|
||||
node.data.node_type == "condition" &&
|
||||
!node.data.conditions
|
||||
);
|
||||
let hasErrors = false;
|
||||
|
||||
input.nodes.forEach((node: any, nodeIndex: number) => {
|
||||
if (node.io_type === "default" && node.data.node_type === "condition") {
|
||||
// Check if conditions exist
|
||||
if (!node.data.conditions) {
|
||||
pipelineErrors.push({
|
||||
message: `Pipeline - ${index}, Node ${nodeIndex}: Condition is required`,
|
||||
field: "empty_condition",
|
||||
});
|
||||
hasErrors = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate the condition format (V0, V1, or V2)
|
||||
const validateV2Condition = (item: any): boolean => {
|
||||
if (item.filterType === 'group') {
|
||||
if (!Array.isArray(item.conditions)) {
|
||||
pipelineErrors.push({
|
||||
message: `Pipeline - ${index}, Node ${nodeIndex}: V2 group must have a conditions array.`,
|
||||
field: "condition_format",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return item.conditions.every((nestedItem: any) => validateV2Condition(nestedItem));
|
||||
} else if (item.filterType === 'condition') {
|
||||
if (!item.column || !item.operator || item.value === undefined) {
|
||||
pipelineErrors.push({
|
||||
message: `Pipeline - ${index}, Node ${nodeIndex}: V2 condition must have column, operator, and value.`,
|
||||
field: "condition_format",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateV1Condition = (condition: any): boolean => {
|
||||
if (condition.column && condition.operator && condition.value !== undefined) {
|
||||
return true;
|
||||
}
|
||||
if (condition.and || condition.or) {
|
||||
const conditions = condition.and || condition.or;
|
||||
if (!Array.isArray(conditions)) {
|
||||
pipelineErrors.push({
|
||||
message: `Pipeline - ${index}, Node ${nodeIndex}: V1 'and'/'or' conditions must be an array.`,
|
||||
field: "condition_format",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return conditions.every((cond: any) => validateV1Condition(cond));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
let conditionsToValidate = node.data.conditions;
|
||||
|
||||
// Determine format and validate
|
||||
if (Array.isArray(conditionsToValidate)) {
|
||||
// V0 format - flat array
|
||||
const valid = conditionsToValidate.every((condition: any) => {
|
||||
return condition.column && condition.operator && condition.value !== undefined;
|
||||
});
|
||||
if (!valid) {
|
||||
pipelineErrors.push({
|
||||
message: `Pipeline - ${index}, Node ${nodeIndex}: V0 format - each condition must have column, operator, and value.`,
|
||||
field: "condition_format",
|
||||
});
|
||||
hasErrors = true;
|
||||
}
|
||||
} else if (conditionsToValidate.filterType === 'group') {
|
||||
// V2 format
|
||||
if (!validateV2Condition(conditionsToValidate)) {
|
||||
hasErrors = true;
|
||||
}
|
||||
} else if (conditionsToValidate.and || conditionsToValidate.or) {
|
||||
// V1 format
|
||||
if (!validateV1Condition(conditionsToValidate)) {
|
||||
pipelineErrors.push({
|
||||
message: `Pipeline - ${index}, Node ${nodeIndex}: Invalid V1 condition format.`,
|
||||
field: "condition_format",
|
||||
});
|
||||
hasErrors = true;
|
||||
}
|
||||
} else {
|
||||
pipelineErrors.push({
|
||||
message: `Pipeline - ${index}, Node ${nodeIndex}: Unrecognized condition format.`,
|
||||
field: "condition_format",
|
||||
});
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
return isValid;
|
||||
|
||||
return !hasErrors;
|
||||
};
|
||||
validateFunctionNode(input, index);
|
||||
//validate condition node
|
||||
if (!validateConditionNode(input)) {
|
||||
pipelineErrors.push({
|
||||
message: `Pipeline - ${index}: Condition is required`,
|
||||
field: "empty_condition",
|
||||
});
|
||||
}
|
||||
//validate condition node - errors are added inside the function
|
||||
validateConditionNode(input);
|
||||
const isValidRemoteDestination = validateRemoteDestination(input);
|
||||
if (!isValidRemoteDestination) {
|
||||
pipelineErrors.push({
|
||||
@@ -1165,6 +1254,43 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
const createPipeline = async (input: any, index: any) => {
|
||||
// VERSION DETECTION AND CONVERSION
|
||||
// Convert V0 and V1 conditions to V2 format in condition nodes before creating pipeline
|
||||
if (input.nodes && Array.isArray(input.nodes)) {
|
||||
input.nodes.forEach((node: any) => {
|
||||
if (node.data?.node_type === "condition" && node.data?.conditions) {
|
||||
let convertedConditions = node.data.conditions;
|
||||
|
||||
// Check if version field is already present (V2)
|
||||
if (node.data.version !== 2 && node.data.version !== "2") {
|
||||
const version = detectConditionsVersion(convertedConditions);
|
||||
|
||||
if (version === 0) {
|
||||
// V0: Flat array format - convert to V2
|
||||
convertedConditions = convertV0ToV2(convertedConditions);
|
||||
} else if (version === 1) {
|
||||
// V1: Tree-based format - convert to V2
|
||||
if (convertedConditions.and || convertedConditions.or) {
|
||||
// V1 Backend format
|
||||
convertedConditions = convertV1BEToV2(convertedConditions);
|
||||
} else if (convertedConditions.label && convertedConditions.items) {
|
||||
// V1 Frontend format
|
||||
convertedConditions = convertV1ToV2(convertedConditions);
|
||||
}
|
||||
}
|
||||
// For version === 2, convertedConditions is already in correct format
|
||||
|
||||
// Update node data with converted conditions
|
||||
node.data.conditions = convertedConditions;
|
||||
}
|
||||
|
||||
// Ensure version is set as integer (matching Condition.vue structure)
|
||||
// Backend expects: node.data = { node_type: "condition", version: 2, conditions: {...} }
|
||||
node.data.version = 2;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await pipelinesService.createPipeline({
|
||||
data: input,
|
||||
|
||||
@@ -168,9 +168,9 @@ describe("Condition Component", () => {
|
||||
const conditionGroup = wrapper.vm.conditionGroup;
|
||||
expect(conditionGroup).toBeDefined();
|
||||
expect(conditionGroup.groupId).toBeDefined();
|
||||
expect(conditionGroup.label).toBe("and");
|
||||
expect(conditionGroup.items).toBeDefined();
|
||||
expect(Array.isArray(conditionGroup.items)).toBe(true);
|
||||
expect(conditionGroup.logicalOperator).toBe('AND');
|
||||
expect(conditionGroup.conditions).toBeDefined();
|
||||
expect(Array.isArray(conditionGroup.conditions)).toBe(true);
|
||||
});
|
||||
|
||||
it.skip("loads fields from input stream node", async () => {
|
||||
@@ -191,9 +191,10 @@ describe("Condition Component", () => {
|
||||
it("updates condition group when conditions are modified", () => {
|
||||
const initialGroupId = wrapper.vm.conditionGroup.groupId;
|
||||
const updatedGroup = {
|
||||
groupId: initialGroupId,
|
||||
label: "and",
|
||||
items: [
|
||||
filterType: 'group',
|
||||
groupId: initialGroupId,
|
||||
logicalOperator: 'AND',
|
||||
conditions: [
|
||||
{
|
||||
column: "level",
|
||||
operator: "=",
|
||||
@@ -205,24 +206,29 @@ describe("Condition Component", () => {
|
||||
};
|
||||
|
||||
wrapper.vm.updateGroup(updatedGroup);
|
||||
expect(wrapper.vm.conditionGroup.items).toHaveLength(1);
|
||||
expect(wrapper.vm.conditionGroup.items[0].column).toBe("level");
|
||||
expect(wrapper.vm.conditionGroup.conditions).toHaveLength(1);
|
||||
expect(wrapper.vm.conditionGroup.conditions[0].column).toBe("level");
|
||||
});
|
||||
|
||||
it("removes condition group by id", () => {
|
||||
// Create nested structure
|
||||
const parentGroup = wrapper.vm.conditionGroup;
|
||||
const childGroupId = "child-group-id";
|
||||
parentGroup.items = [
|
||||
parentGroup.conditions = [
|
||||
{
|
||||
|
||||
filterType: 'group',
|
||||
|
||||
groupId: childGroupId,
|
||||
label: "or",
|
||||
items: []
|
||||
|
||||
logicalOperator: 'OR',
|
||||
|
||||
conditions: []
|
||||
}
|
||||
];
|
||||
|
||||
wrapper.vm.removeConditionGroup(childGroupId);
|
||||
expect(wrapper.vm.conditionGroup.items).toHaveLength(0);
|
||||
expect(wrapper.vm.conditionGroup.conditions).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -230,9 +236,10 @@ describe("Condition Component", () => {
|
||||
it("prevents saving when no conditions are present", async () => {
|
||||
// Empty group with no items
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: "test-group",
|
||||
label: "and",
|
||||
items: []
|
||||
filterType: 'group',
|
||||
groupId: "test-group",
|
||||
logicalOperator: 'AND',
|
||||
conditions: []
|
||||
};
|
||||
|
||||
await wrapper.vm.saveCondition();
|
||||
@@ -248,9 +255,10 @@ describe("Condition Component", () => {
|
||||
|
||||
it("saves condition with valid data", async () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: "test-group",
|
||||
label: "and",
|
||||
items: [
|
||||
filterType: 'group',
|
||||
groupId: "test-group",
|
||||
logicalOperator: 'AND',
|
||||
conditions: [
|
||||
{
|
||||
column: "level",
|
||||
operator: "=",
|
||||
@@ -266,16 +274,19 @@ describe("Condition Component", () => {
|
||||
expect(mockAddNode).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
node_type: "condition",
|
||||
conditions: {
|
||||
and: [
|
||||
{
|
||||
version: 2,
|
||||
conditions: expect.objectContaining({
|
||||
filterType: 'group',
|
||||
logicalOperator: 'AND',
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
column: "level",
|
||||
operator: "=",
|
||||
value: "error",
|
||||
ignore_case: false
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
])
|
||||
})
|
||||
})
|
||||
);
|
||||
expect(wrapper.emitted()["cancel:hideform"]).toBeTruthy();
|
||||
@@ -285,7 +296,7 @@ describe("Condition Component", () => {
|
||||
describe("Dialog Handling", () => {
|
||||
it("opens cancel dialog when form has changes", async () => {
|
||||
// Modify the condition group
|
||||
wrapper.vm.conditionGroup.items = [
|
||||
wrapper.vm.conditionGroup.conditions = [
|
||||
{
|
||||
column: "level",
|
||||
operator: "=",
|
||||
@@ -355,9 +366,9 @@ describe("Condition Component", () => {
|
||||
|
||||
it("loads existing condition data in edit mode", () => {
|
||||
expect(wrapper.vm.conditionGroup).toBeDefined();
|
||||
expect(wrapper.vm.conditionGroup.label).toBe("and");
|
||||
expect(wrapper.vm.conditionGroup.items).toHaveLength(1);
|
||||
expect(wrapper.vm.conditionGroup.items[0]).toEqual(
|
||||
expect(wrapper.vm.conditionGroup.logicalOperator).toBe('AND');
|
||||
expect(wrapper.vm.conditionGroup.conditions).toHaveLength(1);
|
||||
expect(wrapper.vm.conditionGroup.conditions[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
column: "level",
|
||||
operator: "=",
|
||||
@@ -418,9 +429,10 @@ describe("Condition Component", () => {
|
||||
describe("Condition Validation", () => {
|
||||
it("validates empty value condition", async () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: "test-group",
|
||||
label: "and",
|
||||
items: [
|
||||
filterType: 'group',
|
||||
groupId: "test-group",
|
||||
logicalOperator: 'AND',
|
||||
conditions: [
|
||||
{
|
||||
column: "message",
|
||||
operator: "!=",
|
||||
@@ -434,8 +446,12 @@ describe("Condition Component", () => {
|
||||
await wrapper.vm.saveCondition();
|
||||
expect(mockAddNode).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
node_type: "condition",
|
||||
version: 2,
|
||||
conditions: expect.objectContaining({
|
||||
and: expect.arrayContaining([
|
||||
filterType: 'group',
|
||||
logicalOperator: 'AND',
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({ value: '""' })
|
||||
])
|
||||
})
|
||||
@@ -445,9 +461,10 @@ describe("Condition Component", () => {
|
||||
|
||||
it("validates null value condition", async () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: "test-group",
|
||||
label: "and",
|
||||
items: [
|
||||
filterType: 'group',
|
||||
groupId: "test-group",
|
||||
logicalOperator: 'AND',
|
||||
conditions: [
|
||||
{
|
||||
column: "message",
|
||||
operator: "!=",
|
||||
@@ -461,8 +478,12 @@ describe("Condition Component", () => {
|
||||
await wrapper.vm.saveCondition();
|
||||
expect(mockAddNode).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
node_type: "condition",
|
||||
version: 2,
|
||||
conditions: expect.objectContaining({
|
||||
and: expect.arrayContaining([
|
||||
filterType: 'group',
|
||||
logicalOperator: 'AND',
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({ value: "null" })
|
||||
])
|
||||
})
|
||||
@@ -474,9 +495,10 @@ describe("Condition Component", () => {
|
||||
describe("Multiple Conditions", () => {
|
||||
it("handles multiple conditions correctly", async () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: "test-group",
|
||||
label: "and",
|
||||
items: [
|
||||
filterType: 'group',
|
||||
groupId: "test-group",
|
||||
logicalOperator: 'AND',
|
||||
conditions: [
|
||||
{
|
||||
column: "level",
|
||||
operator: "=",
|
||||
@@ -498,21 +520,26 @@ describe("Condition Component", () => {
|
||||
|
||||
expect(mockAddNode).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
conditions: {
|
||||
and: expect.arrayContaining([
|
||||
node_type: "condition",
|
||||
version: 2,
|
||||
conditions: expect.objectContaining({
|
||||
filterType: 'group',
|
||||
logicalOperator: 'AND',
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({ column: "level", value: "error" }),
|
||||
expect.objectContaining({ column: "message", value: "failed" })
|
||||
])
|
||||
}
|
||||
})
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("handles OR conditions with nested groups", async () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: "test-group",
|
||||
label: "or",
|
||||
items: [
|
||||
filterType: 'group',
|
||||
groupId: "test-group",
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{
|
||||
column: "level",
|
||||
operator: "=",
|
||||
@@ -534,12 +561,16 @@ describe("Condition Component", () => {
|
||||
|
||||
expect(mockAddNode).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
conditions: {
|
||||
or: expect.arrayContaining([
|
||||
node_type: "condition",
|
||||
version: 2,
|
||||
conditions: expect.objectContaining({
|
||||
filterType: 'group',
|
||||
logicalOperator: 'OR',
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({ column: "level", value: "error" }),
|
||||
expect.objectContaining({ column: "level", value: "critical" })
|
||||
])
|
||||
}
|
||||
})
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -548,9 +579,10 @@ describe("Condition Component", () => {
|
||||
describe("Edge Cases", () => {
|
||||
it("prevents saving condition with empty column", async () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: "test-group",
|
||||
label: "and",
|
||||
items: [
|
||||
filterType: 'group',
|
||||
groupId: "test-group",
|
||||
logicalOperator: 'AND',
|
||||
conditions: [
|
||||
{
|
||||
column: "",
|
||||
operator: "=",
|
||||
@@ -574,9 +606,10 @@ describe("Condition Component", () => {
|
||||
|
||||
it("prevents saving condition with empty operator", async () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: "test-group",
|
||||
label: "and",
|
||||
items: [
|
||||
filterType: 'group',
|
||||
groupId: "test-group",
|
||||
logicalOperator: 'AND',
|
||||
conditions: [
|
||||
{
|
||||
column: "level",
|
||||
operator: "",
|
||||
@@ -600,9 +633,10 @@ describe("Condition Component", () => {
|
||||
|
||||
it("preserves nested group structure", () => {
|
||||
const nestedGroup = {
|
||||
groupId: "nested-group",
|
||||
label: "or",
|
||||
items: [
|
||||
filterType: 'group',
|
||||
groupId: "nested-group",
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{
|
||||
column: "status",
|
||||
operator: "=",
|
||||
@@ -613,19 +647,20 @@ describe("Condition Component", () => {
|
||||
]
|
||||
};
|
||||
|
||||
wrapper.vm.conditionGroup.items = [nestedGroup];
|
||||
wrapper.vm.conditionGroup.conditions = [nestedGroup];
|
||||
|
||||
expect(wrapper.vm.conditionGroup.items[0].groupId).toBe("nested-group");
|
||||
expect(wrapper.vm.conditionGroup.items[0].label).toBe("or");
|
||||
expect(wrapper.vm.conditionGroup.conditions[0].groupId).toBe("nested-group");
|
||||
expect(wrapper.vm.conditionGroup.conditions[0].logicalOperator).toBe('OR');
|
||||
});
|
||||
});
|
||||
|
||||
describe("Complex Nested Conditions with OR/AND", () => {
|
||||
it("handles complex nested AND within OR", async () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: "root-group",
|
||||
label: "or",
|
||||
items: [
|
||||
filterType: 'group',
|
||||
groupId: "root-group",
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{
|
||||
column: "level",
|
||||
operator: "=",
|
||||
@@ -634,9 +669,14 @@ describe("Condition Component", () => {
|
||||
id: "test-id-1"
|
||||
},
|
||||
{
|
||||
|
||||
filterType: 'group',
|
||||
|
||||
groupId: "nested-and-group",
|
||||
label: "and",
|
||||
items: [
|
||||
|
||||
logicalOperator: 'AND',
|
||||
|
||||
conditions: [
|
||||
{
|
||||
column: "status",
|
||||
operator: "=",
|
||||
@@ -660,26 +700,26 @@ describe("Condition Component", () => {
|
||||
|
||||
expect(mockAddNode).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
conditions: {
|
||||
or: [
|
||||
expect.objectContaining({ column: "level", value: "error" }),
|
||||
{
|
||||
and: [
|
||||
expect.objectContaining({ column: "status", value: "failed" }),
|
||||
expect.objectContaining({ column: "retry_count", value: "3" })
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
node_type: "condition",
|
||||
version: 2,
|
||||
conditions: expect.objectContaining({
|
||||
filterType: 'group',
|
||||
logicalOperator: 'OR',
|
||||
groupId: expect.any(String),
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({ column: "level", value: "error" })
|
||||
])
|
||||
})
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("handles complex nested OR within AND", async () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: "root-group",
|
||||
label: "and",
|
||||
items: [
|
||||
filterType: 'group',
|
||||
groupId: "root-group",
|
||||
logicalOperator: 'AND',
|
||||
conditions: [
|
||||
{
|
||||
column: "app_name",
|
||||
operator: "=",
|
||||
@@ -688,9 +728,14 @@ describe("Condition Component", () => {
|
||||
id: "test-id-1"
|
||||
},
|
||||
{
|
||||
|
||||
filterType: 'group',
|
||||
|
||||
groupId: "nested-or-group",
|
||||
label: "or",
|
||||
items: [
|
||||
|
||||
logicalOperator: 'OR',
|
||||
|
||||
conditions: [
|
||||
{
|
||||
column: "level",
|
||||
operator: "=",
|
||||
@@ -714,26 +759,26 @@ describe("Condition Component", () => {
|
||||
|
||||
expect(mockAddNode).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
conditions: {
|
||||
and: [
|
||||
expect.objectContaining({ column: "app_name", value: "myapp" }),
|
||||
{
|
||||
or: [
|
||||
expect.objectContaining({ column: "level", value: "error" }),
|
||||
expect.objectContaining({ column: "level", value: "critical" })
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
node_type: "condition",
|
||||
version: 2,
|
||||
conditions: expect.objectContaining({
|
||||
filterType: 'group',
|
||||
logicalOperator: 'AND',
|
||||
groupId: expect.any(String),
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({ column: "app_name", value: "myapp" })
|
||||
])
|
||||
})
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("handles triple-nested conditions", async () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: "root-group",
|
||||
label: "and",
|
||||
items: [
|
||||
filterType: 'group',
|
||||
groupId: "root-group",
|
||||
logicalOperator: 'AND',
|
||||
conditions: [
|
||||
{
|
||||
column: "env",
|
||||
operator: "=",
|
||||
@@ -742,9 +787,14 @@ describe("Condition Component", () => {
|
||||
id: "test-id-1"
|
||||
},
|
||||
{
|
||||
|
||||
filterType: 'group',
|
||||
|
||||
groupId: "level-1-group",
|
||||
label: "or",
|
||||
items: [
|
||||
|
||||
logicalOperator: 'OR',
|
||||
|
||||
conditions: [
|
||||
{
|
||||
column: "severity",
|
||||
operator: "=",
|
||||
@@ -753,9 +803,14 @@ describe("Condition Component", () => {
|
||||
id: "test-id-2"
|
||||
},
|
||||
{
|
||||
|
||||
filterType: 'group',
|
||||
|
||||
groupId: "level-2-group",
|
||||
label: "and",
|
||||
items: [
|
||||
|
||||
logicalOperator: 'AND',
|
||||
|
||||
conditions: [
|
||||
{
|
||||
column: "user_type",
|
||||
operator: "=",
|
||||
@@ -781,31 +836,26 @@ describe("Condition Component", () => {
|
||||
|
||||
expect(mockAddNode).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
conditions: {
|
||||
and: [
|
||||
expect.objectContaining({ column: "env", value: "production" }),
|
||||
{
|
||||
or: [
|
||||
expect.objectContaining({ column: "severity", value: "high" }),
|
||||
{
|
||||
and: [
|
||||
expect.objectContaining({ column: "user_type", value: "premium" }),
|
||||
expect.objectContaining({ column: "error_count", value: "10" })
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
node_type: "condition",
|
||||
version: 2,
|
||||
conditions: expect.objectContaining({
|
||||
filterType: 'group',
|
||||
logicalOperator: 'AND',
|
||||
groupId: expect.any(String),
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({ column: "env", value: "production" })
|
||||
])
|
||||
})
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("correctly updates nested groups", () => {
|
||||
const rootGroup = {
|
||||
groupId: "root-group",
|
||||
label: "and",
|
||||
items: [
|
||||
filterType: 'group',
|
||||
groupId: "root-group",
|
||||
logicalOperator: 'AND',
|
||||
conditions: [
|
||||
{
|
||||
column: "field1",
|
||||
operator: "=",
|
||||
@@ -814,9 +864,14 @@ describe("Condition Component", () => {
|
||||
id: "test-id-1"
|
||||
},
|
||||
{
|
||||
|
||||
filterType: 'group',
|
||||
|
||||
groupId: "nested-group",
|
||||
label: "or",
|
||||
items: [
|
||||
|
||||
logicalOperator: 'OR',
|
||||
|
||||
conditions: [
|
||||
{
|
||||
column: "field2",
|
||||
operator: "=",
|
||||
@@ -833,9 +888,10 @@ describe("Condition Component", () => {
|
||||
|
||||
// Update nested group
|
||||
const updatedNestedGroup = {
|
||||
groupId: "nested-group",
|
||||
label: "or",
|
||||
items: [
|
||||
filterType: 'group',
|
||||
groupId: "nested-group",
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{
|
||||
column: "field2",
|
||||
operator: "=",
|
||||
@@ -855,15 +911,16 @@ describe("Condition Component", () => {
|
||||
|
||||
wrapper.vm.updateGroup(updatedNestedGroup);
|
||||
|
||||
expect(wrapper.vm.conditionGroup.items[1].items).toHaveLength(2);
|
||||
expect(wrapper.vm.conditionGroup.items[1].items[1].column).toBe("field3");
|
||||
expect(wrapper.vm.conditionGroup.conditions[1].conditions).toHaveLength(2);
|
||||
expect(wrapper.vm.conditionGroup.conditions[1].conditions[1].column).toBe("field3");
|
||||
});
|
||||
|
||||
it("handles case sensitivity correctly in OR conditions", async () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: "test-group",
|
||||
label: "or",
|
||||
items: [
|
||||
filterType: 'group',
|
||||
groupId: "test-group",
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{
|
||||
column: "message",
|
||||
operator: "Contains",
|
||||
@@ -885,8 +942,13 @@ describe("Condition Component", () => {
|
||||
|
||||
expect(mockAddNode).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
conditions: {
|
||||
or: [
|
||||
node_type: "condition",
|
||||
version: 2,
|
||||
conditions: expect.objectContaining({
|
||||
filterType: 'group',
|
||||
logicalOperator: 'OR',
|
||||
groupId: expect.any(String),
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
column: "message",
|
||||
value: "Error",
|
||||
@@ -897,8 +959,8 @@ describe("Condition Component", () => {
|
||||
value: "CRITICAL",
|
||||
ignore_case: false
|
||||
})
|
||||
]
|
||||
}
|
||||
])
|
||||
})
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -907,9 +969,10 @@ describe("Condition Component", () => {
|
||||
describe("OR Operator Edge Cases", () => {
|
||||
it("handles single condition in OR group", async () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: "test-group",
|
||||
label: "or",
|
||||
items: [
|
||||
filterType: 'group',
|
||||
groupId: "test-group",
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{
|
||||
column: "level",
|
||||
operator: "=",
|
||||
@@ -924,20 +987,26 @@ describe("Condition Component", () => {
|
||||
|
||||
expect(mockAddNode).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
conditions: {
|
||||
or: [
|
||||
node_type: "condition",
|
||||
version: 2,
|
||||
conditions: expect.objectContaining({
|
||||
filterType: 'group',
|
||||
logicalOperator: 'OR',
|
||||
groupId: expect.any(String),
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({ column: "level", value: "error" })
|
||||
]
|
||||
}
|
||||
])
|
||||
})
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("validates OR group with nested empty group", async () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: "test-group",
|
||||
label: "or",
|
||||
items: [
|
||||
filterType: 'group',
|
||||
groupId: "test-group",
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{
|
||||
column: "level",
|
||||
operator: "=",
|
||||
@@ -946,9 +1015,14 @@ describe("Condition Component", () => {
|
||||
id: "test-id-1"
|
||||
},
|
||||
{
|
||||
|
||||
filterType: 'group',
|
||||
|
||||
groupId: "empty-nested-group",
|
||||
label: "and",
|
||||
items: []
|
||||
|
||||
logicalOperator: 'AND',
|
||||
|
||||
conditions: []
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -961,17 +1035,28 @@ describe("Condition Component", () => {
|
||||
|
||||
it("removes deeply nested group correctly", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: "root-group",
|
||||
label: "and",
|
||||
items: [
|
||||
filterType: 'group',
|
||||
groupId: "root-group",
|
||||
logicalOperator: 'AND',
|
||||
conditions: [
|
||||
{
|
||||
|
||||
filterType: 'group',
|
||||
|
||||
groupId: "level-1-group",
|
||||
label: "or",
|
||||
items: [
|
||||
|
||||
logicalOperator: 'OR',
|
||||
|
||||
conditions: [
|
||||
{
|
||||
|
||||
filterType: 'group',
|
||||
|
||||
groupId: "level-2-group",
|
||||
label: "and",
|
||||
items: [
|
||||
|
||||
logicalOperator: 'AND',
|
||||
|
||||
conditions: [
|
||||
{
|
||||
column: "test",
|
||||
operator: "=",
|
||||
@@ -989,7 +1074,7 @@ describe("Condition Component", () => {
|
||||
wrapper.vm.removeConditionGroup("level-2-group");
|
||||
|
||||
// After removing level-2-group, level-1-group should also be removed (empty)
|
||||
expect(wrapper.vm.conditionGroup.items).toHaveLength(0);
|
||||
expect(wrapper.vm.conditionGroup.conditions).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1027,9 +1112,9 @@ describe("Condition Component", () => {
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.vm.conditionGroup.label).toBe("and");
|
||||
expect(wrapper.vm.conditionGroup.items).toHaveLength(1);
|
||||
expect(wrapper.vm.conditionGroup.items[0].column).toBe("status");
|
||||
expect(wrapper.vm.conditionGroup.logicalOperator).toBe('AND');
|
||||
expect(wrapper.vm.conditionGroup.conditions).toHaveLength(1);
|
||||
expect(wrapper.vm.conditionGroup.conditions[0].column).toBe("status");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1047,9 +1132,10 @@ describe("Condition Component", () => {
|
||||
vi.setSystemTime(now);
|
||||
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: "test-group",
|
||||
label: "and",
|
||||
items: [
|
||||
filterType: 'group',
|
||||
groupId: "test-group",
|
||||
logicalOperator: 'AND',
|
||||
conditions: [
|
||||
{
|
||||
column: "level",
|
||||
operator: "=",
|
||||
@@ -1064,16 +1150,20 @@ describe("Condition Component", () => {
|
||||
|
||||
expect(mockAddNode).toHaveBeenCalledWith({
|
||||
node_type: "condition",
|
||||
conditions: {
|
||||
and: [
|
||||
{
|
||||
version: 2,
|
||||
conditions: expect.objectContaining({
|
||||
filterType: 'group',
|
||||
logicalOperator: 'AND',
|
||||
groupId: expect.any(String),
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
column: "level",
|
||||
operator: "=",
|
||||
value: "error",
|
||||
ignore_case: false
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
])
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1121,8 +1211,8 @@ describe("Condition Component", () => {
|
||||
await flushPromises();
|
||||
|
||||
// Verify the condition group was loaded correctly
|
||||
expect(wrapper.vm.conditionGroup.label).toBe("or");
|
||||
expect(wrapper.vm.conditionGroup.items).toHaveLength(2);
|
||||
expect(wrapper.vm.conditionGroup.logicalOperator).toBe('OR');
|
||||
expect(wrapper.vm.conditionGroup.conditions).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,12 +40,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<!-- Wrapper for FilterGroup with pipeline-specific styling -->
|
||||
<div class="pipeline-filter-group-wrapper" @submit.stop.prevent>
|
||||
<FilterGroup
|
||||
v-if="conditionGroup && conditionGroup.items"
|
||||
v-if="conditionGroup && (conditionGroup.conditions || conditionGroup.items)"
|
||||
:key="filterGroupKey"
|
||||
:stream-fields="filteredColumns"
|
||||
:group="conditionGroup"
|
||||
:depth="0"
|
||||
condition-input-width="tw-w-[150px]"
|
||||
condition-input-width="tw-w-[130px]"
|
||||
@add-condition="(updatedGroup) => updateGroup(updatedGroup)"
|
||||
@add-group="(updatedGroup) => updateGroup(updatedGroup)"
|
||||
@remove-group="(groupId) => removeConditionGroup(groupId)"
|
||||
@@ -163,8 +163,14 @@ import searchService from "@/services/search";
|
||||
import { convertDateToTimestamp } from "@/utils/date";
|
||||
import useDragAndDrop from "@/plugins/pipelines/useDnD";
|
||||
import {
|
||||
transformFEToBE,
|
||||
retransformBEToFE,
|
||||
detectConditionsVersion,
|
||||
convertV0ToV2,
|
||||
convertV1ToV2,
|
||||
convertV1BEToV2,
|
||||
updateGroup as updateGroupUtil,
|
||||
removeConditionGroup as removeConditionGroupUtil,
|
||||
ensureIds,
|
||||
type V2Group,
|
||||
} from "@/utils/alerts/alertDataTransforms";
|
||||
|
||||
|
||||
@@ -172,6 +178,7 @@ const VariablesInput = defineAsyncComponent(
|
||||
() => import("@/components/alerts/VariablesInput.vue"),
|
||||
);
|
||||
|
||||
// V1 interfaces (legacy support)
|
||||
interface FilterCondition {
|
||||
column: string;
|
||||
operator: string;
|
||||
@@ -181,9 +188,14 @@ interface FilterCondition {
|
||||
}
|
||||
|
||||
interface ConditionGroup {
|
||||
groupId: string;
|
||||
label: 'and' | 'or';
|
||||
items: (FilterCondition | ConditionGroup)[];
|
||||
// V2 properties
|
||||
filterType?: 'group';
|
||||
logicalOperator?: 'AND' | 'OR';
|
||||
conditions?: (FilterCondition | ConditionGroup)[];
|
||||
// V1 properties (legacy)
|
||||
groupId?: string;
|
||||
label?: 'and' | 'or';
|
||||
items?: (FilterCondition | ConditionGroup)[];
|
||||
}
|
||||
|
||||
interface StreamRoute {
|
||||
@@ -285,31 +297,56 @@ const getDefaultStreamRoute: any = () => {
|
||||
};
|
||||
};
|
||||
|
||||
// Initialize condition group - convert from backend format if editing
|
||||
// Initialize condition group - V2: Auto-convert V0/V1 to V2 format
|
||||
// Supports three versions:
|
||||
// - V0: Flat array of conditions with implicit AND between all (no groups)
|
||||
// - V1: Tree-based structure with {and: [...]} or {or: [...]} or {label, items, groupId}
|
||||
// - V2: Linear structure with filterType, logicalOperator per condition
|
||||
const getDefaultConditionGroup = (): ConditionGroup => {
|
||||
if (pipelineObj.isEditNode && pipelineObj.currentSelectedNodeData?.data?.conditions) {
|
||||
try {
|
||||
// Convert backend ConditionList format to FilterGroup format
|
||||
const converted = retransformBEToFE(pipelineObj.currentSelectedNodeData.data.conditions);
|
||||
if (converted) {
|
||||
return converted;
|
||||
const conditions = pipelineObj.currentSelectedNodeData.data.conditions;
|
||||
const version = detectConditionsVersion(conditions);
|
||||
|
||||
if (version === 0) {
|
||||
// V0: Flat array format - convert to V2
|
||||
// V0 had implicit AND between all conditions (no groups)
|
||||
const converted = convertV0ToV2(conditions);
|
||||
return ensureIds(converted) as any;
|
||||
} else if (version === 1) {
|
||||
// V1: Convert to V2
|
||||
let converted;
|
||||
if (conditions.and || conditions.or) {
|
||||
// V1 Backend format
|
||||
converted = convertV1BEToV2(conditions);
|
||||
} else if (conditions.label && conditions.items) {
|
||||
// V1 Frontend format
|
||||
converted = convertV1ToV2(conditions);
|
||||
}
|
||||
return ensureIds(converted) as any;
|
||||
} else {
|
||||
// V2: Use as-is, but ensure all groupIds and ids exist recursively
|
||||
return ensureIds(conditions) as any;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error converting condition to group format:", error);
|
||||
}
|
||||
}
|
||||
// Default empty group
|
||||
// Default empty V2 group
|
||||
return {
|
||||
filterType: 'group',
|
||||
logicalOperator: 'AND',
|
||||
groupId: getUUID(),
|
||||
label: 'and',
|
||||
items: [{
|
||||
conditions: [{
|
||||
filterType: 'condition',
|
||||
column: '',
|
||||
operator: '=',
|
||||
value: '',
|
||||
ignore_case: true,
|
||||
values: [],
|
||||
logicalOperator: 'AND',
|
||||
id: getUUID(),
|
||||
}]
|
||||
};
|
||||
} as any;
|
||||
};
|
||||
|
||||
onBeforeMount(async () => {
|
||||
@@ -449,57 +486,43 @@ const getFields = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Group management functions
|
||||
// Group management functions - Using shared utilities from alertDataTransforms
|
||||
// These functions are called when FilterGroup emits add-condition, add-group, or remove-group events
|
||||
|
||||
const updateGroup = (updatedGroup: any) => {
|
||||
// If the updated group has the same groupId as the root, replace entirely
|
||||
if (updatedGroup.groupId === conditionGroup.value.groupId) {
|
||||
conditionGroup.value = updatedGroup;
|
||||
} else {
|
||||
// Otherwise, it's a nested group - find and update it recursively
|
||||
const updateNestedGroup = (group: any, updated: any): boolean => {
|
||||
if (!group.items || !Array.isArray(group.items)) return false;
|
||||
|
||||
for (let i = 0; i < group.items.length; i++) {
|
||||
const item = group.items[i];
|
||||
if (item.groupId && item.groupId === updated.groupId) {
|
||||
group.items[i] = updated;
|
||||
return true;
|
||||
}
|
||||
if (item.groupId && item.items) {
|
||||
if (updateNestedGroup(item, updated)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Create a context object that matches the alert utility's expected structure
|
||||
// The utility expects: context.formData.query_condition.conditions
|
||||
// We need to create a temporary wrapper and then extract the updated value
|
||||
const tempContext = {
|
||||
formData: {
|
||||
query_condition: {
|
||||
conditions: conditionGroup.value
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const updatedRoot = JSON.parse(JSON.stringify(conditionGroup.value));
|
||||
if (updateNestedGroup(updatedRoot, updatedGroup)) {
|
||||
conditionGroup.value = updatedRoot;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Call the shared utility
|
||||
updateGroupUtil(updatedGroup, tempContext as any);
|
||||
|
||||
// Extract the updated value back
|
||||
conditionGroup.value = tempContext.formData.query_condition.conditions;
|
||||
};
|
||||
|
||||
const removeConditionGroup = (targetGroupId: string) => {
|
||||
if (!conditionGroup.value?.items || !Array.isArray(conditionGroup.value.items)) return;
|
||||
|
||||
const filterEmptyGroups = (items: any[]): any[] => {
|
||||
return items.filter((item: any) => {
|
||||
if (item.groupId === targetGroupId) {
|
||||
return false;
|
||||
// Create a context object that matches the alert utility's expected structure
|
||||
const tempContext = {
|
||||
formData: {
|
||||
query_condition: {
|
||||
conditions: conditionGroup.value
|
||||
}
|
||||
|
||||
if (item.items && Array.isArray(item.items)) {
|
||||
item.items = filterEmptyGroups(item.items);
|
||||
return item.items.length > 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
conditionGroup.value.items = filterEmptyGroups(conditionGroup.value.items);
|
||||
// Call the shared utility
|
||||
removeConditionGroupUtil(targetGroupId, conditionGroup.value, tempContext as any);
|
||||
|
||||
// Extract the updated value back
|
||||
conditionGroup.value = tempContext.formData.query_condition.conditions;
|
||||
};
|
||||
|
||||
const onInputUpdate = (name: string, field: any) => {
|
||||
@@ -537,9 +560,12 @@ const openCancelDialog = () => {
|
||||
|
||||
const saveCondition = async () => {
|
||||
try {
|
||||
// Check if there are any valid conditions
|
||||
const hasValidConditions = conditionGroup.value.items.some((item: any) => {
|
||||
if (item.groupId && item.items) return true;
|
||||
// V2: Check if there are any valid conditions (use 'conditions' array)
|
||||
const conditionsArray = (conditionGroup.value as any).conditions || [];
|
||||
const hasValidConditions = conditionsArray.some((item: any) => {
|
||||
// Check for nested groups (V2 format)
|
||||
if (item.filterType === 'group' && item.conditions) return true;
|
||||
// Check for valid conditions
|
||||
return item.column && item.operator;
|
||||
});
|
||||
|
||||
@@ -552,12 +578,12 @@ const saveCondition = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Transform FilterGroup format to backend ConditionList format
|
||||
const backendCondition = transformFEToBE(conditionGroup.value);
|
||||
|
||||
// V2: Send directly to backend (no transformation needed)
|
||||
// The conditionGroup is already in V2 format which matches backend
|
||||
let conditionData = {
|
||||
node_type: "condition",
|
||||
conditions: backendCondition,
|
||||
version: 2, // Numeric version for consistency with alerts
|
||||
conditions: conditionGroup.value,
|
||||
};
|
||||
|
||||
// Ensure currentSelectedNodeData has proper structure
|
||||
|
||||
@@ -154,158 +154,178 @@ describe("Condition Component - OR Operator Tests", () => {
|
||||
it("should initialize conditions with nested OR group structure", () => {
|
||||
const conditionGroup = wrapper.vm.conditionGroup;
|
||||
expect(conditionGroup).toBeDefined();
|
||||
expect(conditionGroup.items).toBeDefined();
|
||||
expect(Array.isArray(conditionGroup.items)).toBe(true);
|
||||
expect(conditionGroup.filterType).toBe('group');
|
||||
expect(conditionGroup.conditions).toBeDefined();
|
||||
expect(Array.isArray(conditionGroup.conditions)).toBe(true);
|
||||
});
|
||||
|
||||
it("should support OR condition with multiple fields", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
filterType: 'group',
|
||||
groupId: 'root-group',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '1', column: 'status_code', operator: '=', value: '500', ignore_case: false },
|
||||
{ id: '2', column: 'status_code', operator: '=', value: '503', ignore_case: false }
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'status_code', operator: '=', value: '500', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '2', column: 'status_code', operator: '=', value: '503', ignore_case: false, logicalOperator: 'OR' }
|
||||
]
|
||||
};
|
||||
|
||||
expect(wrapper.vm.conditionGroup.label).toBe('or');
|
||||
expect(wrapper.vm.conditionGroup.items).toHaveLength(2);
|
||||
expect(wrapper.vm.conditionGroup.logicalOperator).toBe('OR');
|
||||
expect(wrapper.vm.conditionGroup.conditions).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("should support AND condition within condition node", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'root-group',
|
||||
label: 'and',
|
||||
items: [
|
||||
{ id: '1', column: 'level', operator: '=', value: 'error', ignore_case: true },
|
||||
{ id: '2', column: 'status_code', operator: '>=', value: '500', ignore_case: false }
|
||||
filterType: 'group',
|
||||
groupId: 'root-group',
|
||||
logicalOperator: 'AND',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'level', operator: '=', value: 'error', ignore_case: true, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '2', column: 'status_code', operator: '>=', value: '500', ignore_case: false, logicalOperator: 'OR' }
|
||||
]
|
||||
};
|
||||
|
||||
expect(wrapper.vm.conditionGroup.label).toBe('and');
|
||||
expect(wrapper.vm.conditionGroup.items).toHaveLength(2);
|
||||
expect(wrapper.vm.conditionGroup.logicalOperator).toBe('AND');
|
||||
expect(wrapper.vm.conditionGroup.conditions).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("should support nested OR within AND condition", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'root-and',
|
||||
label: 'and',
|
||||
items: [
|
||||
{ id: '1', column: 'region', operator: '=', value: 'us-east-1', ignore_case: false },
|
||||
filterType: 'group',
|
||||
groupId: 'root-and',
|
||||
logicalOperator: 'AND',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'region', operator: '=', value: 'us-east-1', ignore_case: false, logicalOperator: 'OR' },
|
||||
{
|
||||
|
||||
filterType: 'group',
|
||||
|
||||
groupId: 'nested-or',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '2', column: 'level', operator: '=', value: 'error', ignore_case: true },
|
||||
{ id: '3', column: 'level', operator: '=', value: 'critical', ignore_case: true }
|
||||
|
||||
logicalOperator: 'OR',
|
||||
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '2', column: 'level', operator: '=', value: 'error', ignore_case: true, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '3', column: 'level', operator: '=', value: 'critical', ignore_case: true, logicalOperator: 'OR' }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
expect(wrapper.vm.conditionGroup.items).toHaveLength(2);
|
||||
expect(wrapper.vm.conditionGroup.items[1].label).toBe('or');
|
||||
expect(wrapper.vm.conditionGroup.items[1].items).toHaveLength(2);
|
||||
expect(wrapper.vm.conditionGroup.conditions).toHaveLength(2);
|
||||
expect(wrapper.vm.conditionGroup.conditions[1].logicalOperator).toBe('OR');
|
||||
expect(wrapper.vm.conditionGroup.conditions[1].conditions).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("should support nested AND within OR condition", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'root-or',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '1', column: 'status_code', operator: '=', value: '404', ignore_case: false },
|
||||
filterType: 'group',
|
||||
groupId: 'root-or',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'status_code', operator: '=', value: '404', ignore_case: false, logicalOperator: 'OR' },
|
||||
{
|
||||
|
||||
filterType: 'group',
|
||||
|
||||
groupId: 'nested-and',
|
||||
label: 'and',
|
||||
items: [
|
||||
{ id: '2', column: 'status_code', operator: '>=', value: '500', ignore_case: false },
|
||||
{ id: '3', column: 'message', operator: 'Contains', value: 'timeout', ignore_case: true }
|
||||
|
||||
logicalOperator: 'AND',
|
||||
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '2', column: 'status_code', operator: '>=', value: '500', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '3', column: 'message', operator: 'Contains', value: 'timeout', ignore_case: true, logicalOperator: 'OR' }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
expect(wrapper.vm.conditionGroup.items).toHaveLength(2);
|
||||
expect(wrapper.vm.conditionGroup.items[1].label).toBe('and');
|
||||
expect(wrapper.vm.conditionGroup.items[1].items).toHaveLength(2);
|
||||
expect(wrapper.vm.conditionGroup.conditions).toHaveLength(2);
|
||||
expect(wrapper.vm.conditionGroup.conditions[1].logicalOperator).toBe('AND');
|
||||
expect(wrapper.vm.conditionGroup.conditions[1].conditions).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("OR Operator - Multiple Conditions Scenarios", () => {
|
||||
it("should handle OR with three conditions", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'group-1',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '1', column: 'status_code', operator: '=', value: '500', ignore_case: false },
|
||||
{ id: '2', column: 'status_code', operator: '=', value: '502', ignore_case: false },
|
||||
{ id: '3', column: 'status_code', operator: '=', value: '503', ignore_case: false }
|
||||
filterType: 'group',
|
||||
groupId: 'group-1',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'status_code', operator: '=', value: '500', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '2', column: 'status_code', operator: '=', value: '502', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '3', column: 'status_code', operator: '=', value: '503', ignore_case: false, logicalOperator: 'OR' }
|
||||
]
|
||||
};
|
||||
|
||||
expect(wrapper.vm.conditionGroup.items).toHaveLength(3);
|
||||
wrapper.vm.conditionGroup.items.forEach(item => {
|
||||
expect(wrapper.vm.conditionGroup.conditions).toHaveLength(3);
|
||||
wrapper.vm.conditionGroup.conditions.forEach(item => {
|
||||
expect(item.column).toBe('status_code');
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle OR with different operators", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'group-1',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '1', column: 'status_code', operator: '>=', value: '500', ignore_case: false },
|
||||
{ id: '2', column: 'message', operator: 'Contains', value: 'error', ignore_case: true },
|
||||
{ id: '3', column: 'level', operator: '=', value: 'critical', ignore_case: true }
|
||||
filterType: 'group',
|
||||
groupId: 'group-1',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'status_code', operator: '>=', value: '500', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '2', column: 'message', operator: 'Contains', value: 'error', ignore_case: true, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '3', column: 'level', operator: '=', value: 'critical', ignore_case: true, logicalOperator: 'OR' }
|
||||
]
|
||||
};
|
||||
|
||||
expect(wrapper.vm.conditionGroup.items[0].operator).toBe('>=');
|
||||
expect(wrapper.vm.conditionGroup.items[1].operator).toBe('Contains');
|
||||
expect(wrapper.vm.conditionGroup.items[2].operator).toBe('=');
|
||||
expect(wrapper.vm.conditionGroup.conditions[0].operator).toBe('>=');
|
||||
expect(wrapper.vm.conditionGroup.conditions[1].operator).toBe('Contains');
|
||||
expect(wrapper.vm.conditionGroup.conditions[2].operator).toBe('=');
|
||||
});
|
||||
|
||||
it("should handle OR with mixed case sensitivity", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'group-1',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '1', column: 'message', operator: 'Contains', value: 'ERROR', ignore_case: true },
|
||||
{ id: '2', column: 'message', operator: 'Contains', value: 'Error', ignore_case: false },
|
||||
{ id: '3', column: 'message', operator: '=', value: 'error', ignore_case: true }
|
||||
filterType: 'group',
|
||||
groupId: 'group-1',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'message', operator: 'Contains', value: 'ERROR', ignore_case: true, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '2', column: 'message', operator: 'Contains', value: 'Error', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '3', column: 'message', operator: '=', value: 'error', ignore_case: true, logicalOperator: 'OR' }
|
||||
]
|
||||
};
|
||||
|
||||
expect(wrapper.vm.conditionGroup.items[0].ignore_case).toBe(true);
|
||||
expect(wrapper.vm.conditionGroup.items[1].ignore_case).toBe(false);
|
||||
expect(wrapper.vm.conditionGroup.items[2].ignore_case).toBe(true);
|
||||
expect(wrapper.vm.conditionGroup.conditions[0].ignore_case).toBe(true);
|
||||
expect(wrapper.vm.conditionGroup.conditions[1].ignore_case).toBe(false);
|
||||
expect(wrapper.vm.conditionGroup.conditions[2].ignore_case).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle OR with NotContains operator", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'group-1',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '1', column: 'message', operator: 'NotContains', value: 'debug', ignore_case: true },
|
||||
{ id: '2', column: 'message', operator: 'NotContains', value: 'trace', ignore_case: true }
|
||||
filterType: 'group',
|
||||
groupId: 'group-1',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'message', operator: 'NotContains', value: 'debug', ignore_case: true, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '2', column: 'message', operator: 'NotContains', value: 'trace', ignore_case: true, logicalOperator: 'OR' }
|
||||
]
|
||||
};
|
||||
|
||||
expect(wrapper.vm.conditionGroup.items[0].operator).toBe('NotContains');
|
||||
expect(wrapper.vm.conditionGroup.items[1].operator).toBe('NotContains');
|
||||
expect(wrapper.vm.conditionGroup.conditions[0].operator).toBe('NotContains');
|
||||
expect(wrapper.vm.conditionGroup.conditions[1].operator).toBe('NotContains');
|
||||
});
|
||||
|
||||
it("should handle OR with != (not equals) operator", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'group-1',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '1', column: 'status', operator: '!=', value: 'success', ignore_case: false },
|
||||
{ id: '2', column: 'status', operator: '!=', value: 'pending', ignore_case: false }
|
||||
filterType: 'group',
|
||||
groupId: 'group-1',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'status', operator: '!=', value: 'success', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '2', column: 'status', operator: '!=', value: 'pending', ignore_case: false, logicalOperator: 'OR' }
|
||||
]
|
||||
};
|
||||
|
||||
wrapper.vm.conditionGroup.items.forEach(item => {
|
||||
wrapper.vm.conditionGroup.conditions.forEach(item => {
|
||||
expect(item.operator).toBe('!=');
|
||||
});
|
||||
});
|
||||
@@ -314,21 +334,32 @@ describe("Condition Component - OR Operator Tests", () => {
|
||||
describe("OR Operator - Complex Nested Structures", () => {
|
||||
it("should handle deeply nested OR-AND-OR structure", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'root',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '1', column: 'level', operator: '=', value: 'critical', ignore_case: true },
|
||||
filterType: 'group',
|
||||
groupId: 'root',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'level', operator: '=', value: 'critical', ignore_case: true, logicalOperator: 'OR' },
|
||||
{
|
||||
|
||||
filterType: 'group',
|
||||
|
||||
groupId: 'mid-and',
|
||||
label: 'and',
|
||||
items: [
|
||||
{ id: '2', column: 'region', operator: '=', value: 'us-west-1', ignore_case: false },
|
||||
|
||||
logicalOperator: 'AND',
|
||||
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '2', column: 'region', operator: '=', value: 'us-west-1', ignore_case: false, logicalOperator: 'OR' },
|
||||
{
|
||||
|
||||
filterType: 'group',
|
||||
|
||||
groupId: 'deep-or',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '3', column: 'status_code', operator: '=', value: '500', ignore_case: false },
|
||||
{ id: '4', column: 'status_code', operator: '=', value: '503', ignore_case: false }
|
||||
|
||||
logicalOperator: 'OR',
|
||||
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '3', column: 'status_code', operator: '=', value: '500', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '4', column: 'status_code', operator: '=', value: '503', ignore_case: false, logicalOperator: 'OR' }
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -336,69 +367,82 @@ describe("Condition Component - OR Operator Tests", () => {
|
||||
]
|
||||
};
|
||||
|
||||
const rootItems = wrapper.vm.conditionGroup.items;
|
||||
const rootItems = wrapper.vm.conditionGroup.conditions;
|
||||
expect(rootItems).toHaveLength(2);
|
||||
expect(rootItems[1].items[1].label).toBe('or');
|
||||
expect(rootItems[1].items[1].items).toHaveLength(2);
|
||||
expect(rootItems[1].conditions[1].logicalOperator).toBe('OR');
|
||||
expect(rootItems[1].conditions[1].conditions).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("should handle multiple OR groups at same level", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'root-and',
|
||||
label: 'and',
|
||||
items: [
|
||||
filterType: 'group',
|
||||
groupId: 'root-and',
|
||||
logicalOperator: 'AND',
|
||||
conditions: [
|
||||
{
|
||||
|
||||
filterType: 'group',
|
||||
|
||||
groupId: 'or-1',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '1', column: 'region', operator: '=', value: 'us-east-1', ignore_case: false },
|
||||
{ id: '2', column: 'region', operator: '=', value: 'us-west-1', ignore_case: false }
|
||||
|
||||
logicalOperator: 'OR',
|
||||
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'region', operator: '=', value: 'us-east-1', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '2', column: 'region', operator: '=', value: 'us-west-1', ignore_case: false, logicalOperator: 'OR' }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
filterType: 'group',
|
||||
|
||||
groupId: 'or-2',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '3', column: 'tier', operator: '=', value: 'gold', ignore_case: false },
|
||||
{ id: '4', column: 'tier', operator: '=', value: 'platinum', ignore_case: false }
|
||||
|
||||
logicalOperator: 'OR',
|
||||
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '3', column: 'tier', operator: '=', value: 'gold', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '4', column: 'tier', operator: '=', value: 'platinum', ignore_case: false, logicalOperator: 'OR' }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const items = wrapper.vm.conditionGroup.items;
|
||||
const items = wrapper.vm.conditionGroup.conditions;
|
||||
expect(items).toHaveLength(2);
|
||||
expect(items[0].label).toBe('or');
|
||||
expect(items[1].label).toBe('or');
|
||||
expect(items[0].logicalOperator).toBe('OR');
|
||||
expect(items[1].logicalOperator).toBe('OR');
|
||||
});
|
||||
|
||||
it("should handle OR with empty value (checking for empty strings)", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'group-1',
|
||||
label: 'or',
|
||||
items: [
|
||||
filterType: 'group',
|
||||
groupId: 'group-1',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ id: '1', column: 'app_name', operator: '=', value: '""', ignore_case: false },
|
||||
{ id: '2', column: 'app_name', operator: '=', value: 'null', ignore_case: false }
|
||||
{ filterType: 'condition', id: '2', column: 'app_name', operator: '=', value: 'null', ignore_case: false, logicalOperator: 'OR' }
|
||||
]
|
||||
};
|
||||
|
||||
expect(wrapper.vm.conditionGroup.items[0].value).toBe('""');
|
||||
expect(wrapper.vm.conditionGroup.items[1].value).toBe('null');
|
||||
expect(wrapper.vm.conditionGroup.conditions[0].value).toBe('""');
|
||||
expect(wrapper.vm.conditionGroup.conditions[1].value).toBe('null');
|
||||
});
|
||||
|
||||
it("should handle OR with numeric comparison operators", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'group-1',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '1', column: 'response_time', operator: '>', value: '1000', ignore_case: false },
|
||||
{ id: '2', column: 'response_time', operator: '<', value: '10', ignore_case: false },
|
||||
{ id: '3', column: 'response_time', operator: '>=', value: '5000', ignore_case: false },
|
||||
{ id: '4', column: 'response_time', operator: '<=', value: '1', ignore_case: false }
|
||||
filterType: 'group',
|
||||
groupId: 'group-1',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'response_time', operator: '>', value: '1000', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '2', column: 'response_time', operator: '<', value: '10', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '3', column: 'response_time', operator: '>=', value: '5000', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '4', column: 'response_time', operator: '<=', value: '1', ignore_case: false, logicalOperator: 'OR' }
|
||||
]
|
||||
};
|
||||
|
||||
const operators = wrapper.vm.conditionGroup.items.map(item => item.operator);
|
||||
const operators = wrapper.vm.conditionGroup.conditions.map(item => item.operator);
|
||||
expect(operators).toEqual(['>', '<', '>=', '<=']);
|
||||
});
|
||||
});
|
||||
@@ -406,229 +450,257 @@ describe("Condition Component - OR Operator Tests", () => {
|
||||
describe("OR Operator - Edge Cases", () => {
|
||||
it("should handle single OR condition", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'group-1',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '1', column: 'status', operator: '=', value: 'active', ignore_case: true }
|
||||
filterType: 'group',
|
||||
groupId: 'group-1',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'status', operator: '=', value: 'active', ignore_case: true, logicalOperator: 'OR' }
|
||||
]
|
||||
};
|
||||
|
||||
expect(wrapper.vm.conditionGroup.items).toHaveLength(1);
|
||||
expect(wrapper.vm.conditionGroup.label).toBe('or');
|
||||
expect(wrapper.vm.conditionGroup.conditions).toHaveLength(1);
|
||||
expect(wrapper.vm.conditionGroup.logicalOperator).toBe('OR');
|
||||
});
|
||||
|
||||
it("should handle OR with special characters in values", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'group-1',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '1', column: 'message', operator: 'Contains', value: 'error: connection failed!', ignore_case: true },
|
||||
{ id: '2', column: 'message', operator: 'Contains', value: 'special@#$%^&*()', ignore_case: false }
|
||||
filterType: 'group',
|
||||
groupId: 'group-1',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'message', operator: 'Contains', value: 'error: connection failed!', ignore_case: true, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '2', column: 'message', operator: 'Contains', value: 'special@#$%^&*()', ignore_case: false, logicalOperator: 'OR' }
|
||||
]
|
||||
};
|
||||
|
||||
expect(wrapper.vm.conditionGroup.items[0].value).toContain('!');
|
||||
expect(wrapper.vm.conditionGroup.items[1].value).toContain('@#$%^&*()');
|
||||
expect(wrapper.vm.conditionGroup.conditions[0].value).toContain('!');
|
||||
expect(wrapper.vm.conditionGroup.conditions[1].value).toContain('@#$%^&*()');
|
||||
});
|
||||
|
||||
it("should handle OR with unicode characters", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'group-1',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '1', column: 'message', operator: 'Contains', value: '错误', ignore_case: true },
|
||||
{ id: '2', column: 'message', operator: 'Contains', value: 'エラー', ignore_case: true },
|
||||
{ id: '3', column: 'message', operator: 'Contains', value: 'خطأ', ignore_case: true }
|
||||
filterType: 'group',
|
||||
groupId: 'group-1',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'message', operator: 'Contains', value: '错误', ignore_case: true, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '2', column: 'message', operator: 'Contains', value: 'エラー', ignore_case: true, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '3', column: 'message', operator: 'Contains', value: 'خطأ', ignore_case: true, logicalOperator: 'OR' }
|
||||
]
|
||||
};
|
||||
|
||||
expect(wrapper.vm.conditionGroup.items).toHaveLength(3);
|
||||
expect(wrapper.vm.conditionGroup.items[0].value).toBe('错误');
|
||||
expect(wrapper.vm.conditionGroup.items[1].value).toBe('エラー');
|
||||
expect(wrapper.vm.conditionGroup.items[2].value).toBe('خطأ');
|
||||
expect(wrapper.vm.conditionGroup.conditions).toHaveLength(3);
|
||||
expect(wrapper.vm.conditionGroup.conditions[0].value).toBe('错误');
|
||||
expect(wrapper.vm.conditionGroup.conditions[1].value).toBe('エラー');
|
||||
expect(wrapper.vm.conditionGroup.conditions[2].value).toBe('خطأ');
|
||||
});
|
||||
|
||||
it("should handle OR with whitespace in values", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'group-1',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '1', column: 'message', operator: '=', value: ' leading spaces', ignore_case: false },
|
||||
{ id: '2', column: 'message', operator: '=', value: 'trailing spaces ', ignore_case: false },
|
||||
{ id: '3', column: 'message', operator: '=', value: ' both sides ', ignore_case: false }
|
||||
filterType: 'group',
|
||||
groupId: 'group-1',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'message', operator: '=', value: ' leading spaces', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '2', column: 'message', operator: '=', value: 'trailing spaces ', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '3', column: 'message', operator: '=', value: ' both sides ', ignore_case: false, logicalOperator: 'OR' }
|
||||
]
|
||||
};
|
||||
|
||||
expect(wrapper.vm.conditionGroup.items[0].value).toBe(' leading spaces');
|
||||
expect(wrapper.vm.conditionGroup.items[1].value).toBe('trailing spaces ');
|
||||
expect(wrapper.vm.conditionGroup.items[2].value).toBe(' both sides ');
|
||||
expect(wrapper.vm.conditionGroup.conditions[0].value).toBe(' leading spaces');
|
||||
expect(wrapper.vm.conditionGroup.conditions[1].value).toBe('trailing spaces ');
|
||||
expect(wrapper.vm.conditionGroup.conditions[2].value).toBe(' both sides ');
|
||||
});
|
||||
|
||||
it("should handle OR with numeric string values", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'group-1',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '1', column: 'count', operator: '=', value: '100', ignore_case: false },
|
||||
{ id: '2', column: 'count', operator: '=', value: '1000.50', ignore_case: false },
|
||||
{ id: '3', column: 'count', operator: '=', value: '-50', ignore_case: false }
|
||||
filterType: 'group',
|
||||
groupId: 'group-1',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'count', operator: '=', value: '100', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '2', column: 'count', operator: '=', value: '1000.50', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '3', column: 'count', operator: '=', value: '-50', ignore_case: false, logicalOperator: 'OR' }
|
||||
]
|
||||
};
|
||||
|
||||
expect(wrapper.vm.conditionGroup.items[0].value).toBe('100');
|
||||
expect(wrapper.vm.conditionGroup.items[1].value).toBe('1000.50');
|
||||
expect(wrapper.vm.conditionGroup.items[2].value).toBe('-50');
|
||||
expect(wrapper.vm.conditionGroup.conditions[0].value).toBe('100');
|
||||
expect(wrapper.vm.conditionGroup.conditions[1].value).toBe('1000.50');
|
||||
expect(wrapper.vm.conditionGroup.conditions[2].value).toBe('-50');
|
||||
});
|
||||
|
||||
it("should handle OR with boolean-like string values", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'group-1',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '1', column: 'is_valid', operator: '=', value: 'true', ignore_case: false },
|
||||
{ id: '2', column: 'is_valid', operator: '=', value: 'false', ignore_case: false }
|
||||
filterType: 'group',
|
||||
groupId: 'group-1',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'is_valid', operator: '=', value: 'true', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '2', column: 'is_valid', operator: '=', value: 'false', ignore_case: false, logicalOperator: 'OR' }
|
||||
]
|
||||
};
|
||||
|
||||
expect(wrapper.vm.conditionGroup.items[0].value).toBe('true');
|
||||
expect(wrapper.vm.conditionGroup.items[1].value).toBe('false');
|
||||
expect(wrapper.vm.conditionGroup.conditions[0].value).toBe('true');
|
||||
expect(wrapper.vm.conditionGroup.conditions[1].value).toBe('false');
|
||||
});
|
||||
});
|
||||
|
||||
describe("OR Operator - Real-World Scenarios", () => {
|
||||
it("should handle HTTP error codes OR condition", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'http-errors',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '1', column: 'status_code', operator: '=', value: '400', ignore_case: false },
|
||||
{ id: '2', column: 'status_code', operator: '=', value: '401', ignore_case: false },
|
||||
{ id: '3', column: 'status_code', operator: '=', value: '403', ignore_case: false },
|
||||
{ id: '4', column: 'status_code', operator: '=', value: '404', ignore_case: false },
|
||||
{ id: '5', column: 'status_code', operator: '=', value: '500', ignore_case: false },
|
||||
{ id: '6', column: 'status_code', operator: '=', value: '502', ignore_case: false },
|
||||
{ id: '7', column: 'status_code', operator: '=', value: '503', ignore_case: false }
|
||||
filterType: 'group',
|
||||
groupId: 'http-errors',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'status_code', operator: '=', value: '400', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '2', column: 'status_code', operator: '=', value: '401', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '3', column: 'status_code', operator: '=', value: '403', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '4', column: 'status_code', operator: '=', value: '404', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '5', column: 'status_code', operator: '=', value: '500', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '6', column: 'status_code', operator: '=', value: '502', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '7', column: 'status_code', operator: '=', value: '503', ignore_case: false, logicalOperator: 'OR' }
|
||||
]
|
||||
};
|
||||
|
||||
expect(wrapper.vm.conditionGroup.items).toHaveLength(7);
|
||||
expect(wrapper.vm.conditionGroup.label).toBe('or');
|
||||
expect(wrapper.vm.conditionGroup.conditions).toHaveLength(7);
|
||||
expect(wrapper.vm.conditionGroup.logicalOperator).toBe('OR');
|
||||
});
|
||||
|
||||
it("should handle log level filtering with OR", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'log-levels',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '1', column: 'level', operator: '=', value: 'error', ignore_case: true },
|
||||
{ id: '2', column: 'level', operator: '=', value: 'critical', ignore_case: true },
|
||||
{ id: '3', column: 'level', operator: '=', value: 'fatal', ignore_case: true },
|
||||
{ id: '4', column: 'level', operator: '=', value: 'emergency', ignore_case: true }
|
||||
filterType: 'group',
|
||||
groupId: 'log-levels',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'level', operator: '=', value: 'error', ignore_case: true, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '2', column: 'level', operator: '=', value: 'critical', ignore_case: true, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '3', column: 'level', operator: '=', value: 'fatal', ignore_case: true, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '4', column: 'level', operator: '=', value: 'emergency', ignore_case: true, logicalOperator: 'OR' }
|
||||
]
|
||||
};
|
||||
|
||||
expect(wrapper.vm.conditionGroup.items).toHaveLength(4);
|
||||
wrapper.vm.conditionGroup.items.forEach(item => {
|
||||
expect(wrapper.vm.conditionGroup.conditions).toHaveLength(4);
|
||||
wrapper.vm.conditionGroup.conditions.forEach(item => {
|
||||
expect(item.ignore_case).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle multi-region filtering", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'regions',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '1', column: 'region', operator: '=', value: 'us-east-1', ignore_case: false },
|
||||
{ id: '2', column: 'region', operator: '=', value: 'us-west-1', ignore_case: false },
|
||||
{ id: '3', column: 'region', operator: '=', value: 'eu-west-1', ignore_case: false },
|
||||
{ id: '4', column: 'region', operator: '=', value: 'ap-southeast-1', ignore_case: false }
|
||||
filterType: 'group',
|
||||
groupId: 'regions',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'region', operator: '=', value: 'us-east-1', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '2', column: 'region', operator: '=', value: 'us-west-1', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '3', column: 'region', operator: '=', value: 'eu-west-1', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '4', column: 'region', operator: '=', value: 'ap-southeast-1', ignore_case: false, logicalOperator: 'OR' }
|
||||
]
|
||||
};
|
||||
|
||||
expect(wrapper.vm.conditionGroup.items).toHaveLength(4);
|
||||
expect(wrapper.vm.conditionGroup.items.every(item => item.column === 'region')).toBe(true);
|
||||
expect(wrapper.vm.conditionGroup.conditions).toHaveLength(4);
|
||||
expect(wrapper.vm.conditionGroup.conditions.every(item => item.column === 'region')).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle complex alert condition: critical errors OR high response time AND specific region", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'root-or',
|
||||
label: 'or',
|
||||
items: [
|
||||
filterType: 'group',
|
||||
groupId: 'root-or',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{
|
||||
|
||||
filterType: 'group',
|
||||
|
||||
groupId: 'critical-errors',
|
||||
label: 'and',
|
||||
items: [
|
||||
{ id: '1', column: 'level', operator: '=', value: 'critical', ignore_case: true },
|
||||
{ id: '2', column: 'region', operator: '=', value: 'us-east-1', ignore_case: false }
|
||||
|
||||
logicalOperator: 'AND',
|
||||
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'level', operator: '=', value: 'critical', ignore_case: true, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '2', column: 'region', operator: '=', value: 'us-east-1', ignore_case: false, logicalOperator: 'OR' }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
filterType: 'group',
|
||||
|
||||
groupId: 'high-response',
|
||||
label: 'and',
|
||||
items: [
|
||||
{ id: '3', column: 'response_time', operator: '>', value: '5000', ignore_case: false },
|
||||
{ id: '4', column: 'region', operator: '=', value: 'us-east-1', ignore_case: false }
|
||||
|
||||
logicalOperator: 'AND',
|
||||
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '3', column: 'response_time', operator: '>', value: '5000', ignore_case: false, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '4', column: 'region', operator: '=', value: 'us-east-1', ignore_case: false, logicalOperator: 'OR' }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
expect(wrapper.vm.conditionGroup.items).toHaveLength(2);
|
||||
expect(wrapper.vm.conditionGroup.items[0].label).toBe('and');
|
||||
expect(wrapper.vm.conditionGroup.items[1].label).toBe('and');
|
||||
expect(wrapper.vm.conditionGroup.conditions).toHaveLength(2);
|
||||
expect(wrapper.vm.conditionGroup.conditions[0].logicalOperator).toBe('AND');
|
||||
expect(wrapper.vm.conditionGroup.conditions[1].logicalOperator).toBe('AND');
|
||||
});
|
||||
|
||||
it("should handle error pattern matching with OR", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'error-patterns',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '1', column: 'message', operator: 'Contains', value: 'timeout', ignore_case: true },
|
||||
{ id: '2', column: 'message', operator: 'Contains', value: 'connection refused', ignore_case: true },
|
||||
{ id: '3', column: 'message', operator: 'Contains', value: 'out of memory', ignore_case: true },
|
||||
{ id: '4', column: 'message', operator: 'Contains', value: 'null pointer', ignore_case: true }
|
||||
filterType: 'group',
|
||||
groupId: 'error-patterns',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'message', operator: 'Contains', value: 'timeout', ignore_case: true, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '2', column: 'message', operator: 'Contains', value: 'connection refused', ignore_case: true, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '3', column: 'message', operator: 'Contains', value: 'out of memory', ignore_case: true, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '4', column: 'message', operator: 'Contains', value: 'null pointer', ignore_case: true, logicalOperator: 'OR' }
|
||||
]
|
||||
};
|
||||
|
||||
expect(wrapper.vm.conditionGroup.items).toHaveLength(4);
|
||||
expect(wrapper.vm.conditionGroup.items.every(item => item.operator === 'Contains')).toBe(true);
|
||||
expect(wrapper.vm.conditionGroup.conditions).toHaveLength(4);
|
||||
expect(wrapper.vm.conditionGroup.conditions.every(item => item.operator === 'Contains')).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle service status monitoring", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'root-and',
|
||||
label: 'and',
|
||||
items: [
|
||||
{ id: '1', column: 'service', operator: '=', value: 'api-gateway', ignore_case: false },
|
||||
filterType: 'group',
|
||||
groupId: 'root-and',
|
||||
logicalOperator: 'AND',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'service', operator: '=', value: 'api-gateway', ignore_case: false, logicalOperator: 'OR' },
|
||||
{
|
||||
|
||||
filterType: 'group',
|
||||
|
||||
groupId: 'status-or',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '2', column: 'status', operator: '=', value: 'degraded', ignore_case: true },
|
||||
{ id: '3', column: 'status', operator: '=', value: 'down', ignore_case: true },
|
||||
{ id: '4', column: 'status', operator: '=', value: 'maintenance', ignore_case: true }
|
||||
|
||||
logicalOperator: 'OR',
|
||||
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '2', column: 'status', operator: '=', value: 'degraded', ignore_case: true, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '3', column: 'status', operator: '=', value: 'down', ignore_case: true, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '4', column: 'status', operator: '=', value: 'maintenance', ignore_case: true, logicalOperator: 'OR' }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
expect(wrapper.vm.conditionGroup.items).toHaveLength(2);
|
||||
expect(wrapper.vm.conditionGroup.items[1].items).toHaveLength(3);
|
||||
expect(wrapper.vm.conditionGroup.conditions).toHaveLength(2);
|
||||
expect(wrapper.vm.conditionGroup.conditions[1].conditions).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("OR Operator - Data Validation", () => {
|
||||
it("should validate OR conditions with valid data", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'valid-or',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '1', column: 'status', operator: '=', value: 'active', ignore_case: true },
|
||||
{ id: '2', column: 'status', operator: '=', value: 'pending', ignore_case: true }
|
||||
filterType: 'group',
|
||||
groupId: 'valid-or',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'status', operator: '=', value: 'active', ignore_case: true, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '2', column: 'status', operator: '=', value: 'pending', ignore_case: true, logicalOperator: 'OR' }
|
||||
]
|
||||
};
|
||||
|
||||
const isValid = wrapper.vm.conditionGroup.items.every(item =>
|
||||
const isValid = wrapper.vm.conditionGroup.conditions.every(item =>
|
||||
item.column && item.operator && item.value !== undefined
|
||||
);
|
||||
|
||||
@@ -637,44 +709,47 @@ describe("Condition Component - OR Operator Tests", () => {
|
||||
|
||||
it("should handle OR conditions with missing column", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'invalid-or',
|
||||
label: 'or',
|
||||
items: [
|
||||
filterType: 'group',
|
||||
groupId: 'invalid-or',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ id: '1', column: '', operator: '=', value: 'test', ignore_case: false },
|
||||
{ id: '2', column: 'status', operator: '=', value: 'active', ignore_case: false }
|
||||
{ filterType: 'condition', id: '2', column: 'status', operator: '=', value: 'active', ignore_case: false, logicalOperator: 'OR' }
|
||||
]
|
||||
};
|
||||
|
||||
const hasInvalid = wrapper.vm.conditionGroup.items.some(item => !item.column);
|
||||
const hasInvalid = wrapper.vm.conditionGroup.conditions.some(item => !item.column);
|
||||
expect(hasInvalid).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle OR conditions with missing operator", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'invalid-or',
|
||||
label: 'or',
|
||||
items: [
|
||||
filterType: 'group',
|
||||
groupId: 'invalid-or',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ id: '1', column: 'status', operator: '', value: 'test', ignore_case: false },
|
||||
{ id: '2', column: 'level', operator: '=', value: 'error', ignore_case: false }
|
||||
{ filterType: 'condition', id: '2', column: 'level', operator: '=', value: 'error', ignore_case: false, logicalOperator: 'OR' }
|
||||
]
|
||||
};
|
||||
|
||||
const hasInvalid = wrapper.vm.conditionGroup.items.some(item => !item.operator);
|
||||
const hasInvalid = wrapper.vm.conditionGroup.conditions.some(item => !item.operator);
|
||||
expect(hasInvalid).toBe(true);
|
||||
});
|
||||
|
||||
it("should allow OR conditions with empty string value when checking for empty", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'empty-check',
|
||||
label: 'or',
|
||||
items: [
|
||||
filterType: 'group',
|
||||
groupId: 'empty-check',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ id: '1', column: 'field1', operator: '=', value: '""', ignore_case: false },
|
||||
{ id: '2', column: 'field2', operator: '!=', value: '""', ignore_case: false }
|
||||
]
|
||||
};
|
||||
|
||||
expect(wrapper.vm.conditionGroup.items[0].value).toBe('""');
|
||||
expect(wrapper.vm.conditionGroup.items[1].value).toBe('""');
|
||||
expect(wrapper.vm.conditionGroup.conditions[0].value).toBe('""');
|
||||
expect(wrapper.vm.conditionGroup.conditions[1].value).toBe('""');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -689,32 +764,44 @@ describe("Condition Component - OR Operator Tests", () => {
|
||||
}));
|
||||
|
||||
wrapper.vm.conditionGroup = {
|
||||
filterType: 'group',
|
||||
groupId: 'many-conditions',
|
||||
label: 'or',
|
||||
items
|
||||
logicalOperator: 'OR',
|
||||
conditions: items
|
||||
};
|
||||
|
||||
expect(wrapper.vm.conditionGroup.items).toHaveLength(15);
|
||||
expect(wrapper.vm.conditionGroup.conditions).toHaveLength(15);
|
||||
});
|
||||
|
||||
it("should handle deeply nested structure (3 levels)", () => {
|
||||
wrapper.vm.conditionGroup = {
|
||||
groupId: 'level-1-or',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '1', column: 'priority', operator: '=', value: 'critical', ignore_case: false },
|
||||
filterType: 'group',
|
||||
groupId: 'level-1-or',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'priority', operator: '=', value: 'critical', ignore_case: false, logicalOperator: 'OR' },
|
||||
{
|
||||
|
||||
filterType: 'group',
|
||||
|
||||
groupId: 'level-2-and',
|
||||
label: 'and',
|
||||
items: [
|
||||
{ id: '2', column: 'service', operator: '=', value: 'auth', ignore_case: false },
|
||||
|
||||
logicalOperator: 'AND',
|
||||
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '2', column: 'service', operator: '=', value: 'auth', ignore_case: false, logicalOperator: 'OR' },
|
||||
{
|
||||
|
||||
filterType: 'group',
|
||||
|
||||
groupId: 'level-3-or',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '3', column: 'error_type', operator: '=', value: 'timeout', ignore_case: true },
|
||||
{ id: '4', column: 'error_type', operator: '=', value: 'connection', ignore_case: true },
|
||||
{ id: '5', column: 'error_type', operator: '=', value: 'auth_failed', ignore_case: true }
|
||||
|
||||
logicalOperator: 'OR',
|
||||
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '3', column: 'error_type', operator: '=', value: 'timeout', ignore_case: true, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '4', column: 'error_type', operator: '=', value: 'connection', ignore_case: true, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '5', column: 'error_type', operator: '=', value: 'auth_failed', ignore_case: true, logicalOperator: 'OR' }
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -723,31 +810,32 @@ describe("Condition Component - OR Operator Tests", () => {
|
||||
};
|
||||
|
||||
const level1 = wrapper.vm.conditionGroup;
|
||||
const level2 = level1.items[1];
|
||||
const level3 = level2.items[1];
|
||||
const level2 = level1.conditions[1];
|
||||
const level3 = level2.conditions[1];
|
||||
|
||||
expect(level1.label).toBe('or');
|
||||
expect(level2.label).toBe('and');
|
||||
expect(level3.label).toBe('or');
|
||||
expect(level3.items).toHaveLength(3);
|
||||
expect(level1.logicalOperator).toBe('OR');
|
||||
expect(level2.logicalOperator).toBe('AND');
|
||||
expect(level3.logicalOperator).toBe('OR');
|
||||
expect(level3.conditions).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("OR Operator - Integration with Pipeline", () => {
|
||||
it("should preserve OR structure when saving condition node", () => {
|
||||
const orCondition = {
|
||||
groupId: 'save-test',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '1', column: 'status', operator: '=', value: 'error', ignore_case: true },
|
||||
{ id: '2', column: 'status', operator: '=', value: 'failed', ignore_case: true }
|
||||
filterType: 'group',
|
||||
groupId: 'save-test',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'status', operator: '=', value: 'error', ignore_case: true, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '2', column: 'status', operator: '=', value: 'failed', ignore_case: true, logicalOperator: 'OR' }
|
||||
]
|
||||
};
|
||||
|
||||
wrapper.vm.conditionGroup = orCondition;
|
||||
|
||||
expect(wrapper.vm.conditionGroup).toEqual(orCondition);
|
||||
expect(wrapper.vm.conditionGroup.label).toBe('or');
|
||||
expect(wrapper.vm.conditionGroup.logicalOperator).toBe('OR');
|
||||
});
|
||||
|
||||
it("should handle updating existing condition node with OR", () => {
|
||||
@@ -756,10 +844,11 @@ describe("Condition Component - OR Operator Tests", () => {
|
||||
data: {
|
||||
query_condition: {
|
||||
conditions: {
|
||||
groupId: 'existing',
|
||||
label: 'and',
|
||||
items: [
|
||||
{ id: '1', column: 'old_field', operator: '=', value: 'old_value', ignore_case: false }
|
||||
filterType: 'group',
|
||||
groupId: 'existing',
|
||||
logicalOperator: 'AND',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'old_field', operator: '=', value: 'old_value', ignore_case: false, logicalOperator: 'OR' }
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -768,18 +857,19 @@ describe("Condition Component - OR Operator Tests", () => {
|
||||
};
|
||||
|
||||
const newOrCondition = {
|
||||
groupId: 'updated',
|
||||
label: 'or',
|
||||
items: [
|
||||
{ id: '1', column: 'new_field', operator: '=', value: 'new_value', ignore_case: true },
|
||||
{ id: '2', column: 'another_field', operator: '!=', value: 'test', ignore_case: false }
|
||||
filterType: 'group',
|
||||
groupId: 'updated',
|
||||
logicalOperator: 'OR',
|
||||
conditions: [
|
||||
{ filterType: 'condition', id: '1', column: 'new_field', operator: '=', value: 'new_value', ignore_case: true, logicalOperator: 'OR' },
|
||||
{ filterType: 'condition', id: '2', column: 'another_field', operator: '!=', value: 'test', ignore_case: false, logicalOperator: 'OR' }
|
||||
]
|
||||
};
|
||||
|
||||
wrapper.vm.conditionGroup = newOrCondition;
|
||||
|
||||
expect(wrapper.vm.conditionGroup.label).toBe('or');
|
||||
expect(wrapper.vm.conditionGroup.items).toHaveLength(2);
|
||||
expect(wrapper.vm.conditionGroup.logicalOperator).toBe('OR');
|
||||
expect(wrapper.vm.conditionGroup.conditions).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -305,45 +305,106 @@ const getTruncatedConditions = (conditionData) => {
|
||||
// Handle null/undefined
|
||||
if (!conditionData) return '';
|
||||
|
||||
// Helper to recursively extract all conditions from ConditionList
|
||||
const extractConditions = (node) => {
|
||||
if (!node) return [];
|
||||
// Build preview string recursively
|
||||
const buildPreviewString = (node) => {
|
||||
if (!node) return '';
|
||||
|
||||
// Handle OR node
|
||||
// V2 Format: Group
|
||||
if (node.filterType === 'group' && node.conditions && Array.isArray(node.conditions)) {
|
||||
if (node.conditions.length === 0) return '';
|
||||
|
||||
const parts = [];
|
||||
node.conditions.forEach((item, index) => {
|
||||
let conditionStr = '';
|
||||
|
||||
if (item.filterType === 'group') {
|
||||
// Nested group
|
||||
const nestedPreview = buildPreviewString(item);
|
||||
if (nestedPreview) {
|
||||
conditionStr = `(${nestedPreview})`;
|
||||
}
|
||||
} else if (item.filterType === 'condition') {
|
||||
// Condition
|
||||
const column = item.column || 'field';
|
||||
const operator = item.operator || '=';
|
||||
const value = item.value !== undefined && item.value !== null && item.value !== ''
|
||||
? `'${item.value}'`
|
||||
: "''";
|
||||
conditionStr = `${column} ${operator} ${value}`;
|
||||
}
|
||||
|
||||
// Add logical operator before condition (except for first)
|
||||
if (index > 0 && item.logicalOperator) {
|
||||
parts.push(`${item.logicalOperator.toLowerCase()} ${conditionStr}`);
|
||||
} else {
|
||||
parts.push(conditionStr);
|
||||
}
|
||||
});
|
||||
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
// V1 Backend Format: OR node
|
||||
if (node.or && Array.isArray(node.or)) {
|
||||
return node.or.flatMap(item => extractConditions(item));
|
||||
const parts = node.or.map(item => {
|
||||
const nested = buildPreviewString(item);
|
||||
return nested ? `(${nested})` : '';
|
||||
}).filter(Boolean);
|
||||
return parts.join(' or ');
|
||||
}
|
||||
|
||||
// Handle AND node
|
||||
// V1 Backend Format: AND node
|
||||
if (node.and && Array.isArray(node.and)) {
|
||||
return node.and.flatMap(item => extractConditions(item));
|
||||
const parts = node.and.map(item => {
|
||||
const nested = buildPreviewString(item);
|
||||
return nested ? `(${nested})` : '';
|
||||
}).filter(Boolean);
|
||||
return parts.join(' and ');
|
||||
}
|
||||
|
||||
// Handle NOT node
|
||||
// V1 Backend Format: NOT node
|
||||
if (node.not) {
|
||||
return extractConditions(node.not);
|
||||
const nested = buildPreviewString(node.not);
|
||||
return nested ? `not (${nested})` : '';
|
||||
}
|
||||
|
||||
// Handle single condition (EndCondition) - has column/operator/value
|
||||
// V1 Frontend Format: items array
|
||||
if (node.items && Array.isArray(node.items)) {
|
||||
const operator = node.label?.toLowerCase() || 'and';
|
||||
const parts = node.items.map(item => buildPreviewString(item)).filter(Boolean);
|
||||
return parts.join(` ${operator} `);
|
||||
}
|
||||
|
||||
// Single condition
|
||||
if (node.column && node.operator) {
|
||||
return [node];
|
||||
const column = node.column || 'field';
|
||||
const operator = node.operator || '=';
|
||||
const value = node.value !== undefined && node.value !== null && node.value !== ''
|
||||
? `'${node.value}'`
|
||||
: "''";
|
||||
return `${column} ${operator} ${value}`;
|
||||
}
|
||||
|
||||
// If it's an array (legacy format), return as-is
|
||||
// V0 Format: Array
|
||||
if (Array.isArray(node)) {
|
||||
return node.filter(c => c.column && c.operator);
|
||||
const parts = node.filter(c => c.column && c.operator).map(c => {
|
||||
const column = c.column || 'field';
|
||||
const operator = c.operator || '=';
|
||||
const value = c.value !== undefined && c.value !== null && c.value !== ''
|
||||
? `'${c.value}'`
|
||||
: "''";
|
||||
return `${column} ${operator} ${value}`;
|
||||
});
|
||||
return parts.join(' and ');
|
||||
}
|
||||
|
||||
return [];
|
||||
return '';
|
||||
};
|
||||
|
||||
const allConditions = extractConditions(conditionData);
|
||||
const previewText = buildPreviewString(conditionData);
|
||||
|
||||
const allConditionsText = allConditions.map(condition =>
|
||||
`${condition.column} ${condition.operator} ${condition.value || ''}`
|
||||
).join(', ');
|
||||
|
||||
return allConditionsText.length > 30 ? allConditionsText.substring(0, 30) + '...' : allConditionsText;
|
||||
// Truncate to 20 characters
|
||||
return previewText.length > 20 ? previewText.substring(0, 20) + '...' : previewText;
|
||||
}
|
||||
|
||||
const confirmDialogMeta = ref({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,10 @@
|
||||
/**
|
||||
* Alert Data Transformation Utilities
|
||||
* Extracted from AddAlert.vue to reduce file complexity
|
||||
*
|
||||
* VERSION SUPPORT:
|
||||
* - Version 1: Old structure with {and: [...]} or {or: [...]} format
|
||||
* - Version 2: New structure with filterType, logicalOperator, and conditions array
|
||||
*/
|
||||
|
||||
import { getUUID } from "@/utils/zincutils";
|
||||
@@ -9,13 +13,122 @@ export interface TransformContext {
|
||||
formData: any;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// VERSION 2 INTERFACES
|
||||
// ============================================================================
|
||||
|
||||
export interface V2Condition {
|
||||
filterType: "condition";
|
||||
column: string;
|
||||
operator: string;
|
||||
value: string | number;
|
||||
values?: any[];
|
||||
logicalOperator: "AND" | "OR";
|
||||
ignore_case?: boolean;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface V2Group {
|
||||
filterType: "group";
|
||||
logicalOperator: "AND" | "OR";
|
||||
conditions: (V2Condition | V2Group)[];
|
||||
groupId?: string;
|
||||
}
|
||||
|
||||
export interface V1Condition {
|
||||
column: string;
|
||||
operator: string;
|
||||
value: string | number;
|
||||
ignore_case?: boolean;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface V1Group {
|
||||
groupId: string;
|
||||
label: "and" | "or";
|
||||
items: (V1Condition | V1Group)[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures all groups have groupId and all conditions have id
|
||||
* This is needed because data from backend might not have these fields
|
||||
*/
|
||||
export const ensureIds = (group: any): any => {
|
||||
if (!group) return group;
|
||||
|
||||
// Ensure this group has a groupId
|
||||
if (group.filterType === "group" && !group.groupId) {
|
||||
group.groupId = getUUID();
|
||||
}
|
||||
|
||||
// Process conditions array
|
||||
if (group.conditions && Array.isArray(group.conditions)) {
|
||||
group.conditions = group.conditions.map((item: any) => {
|
||||
if (item.filterType === "group") {
|
||||
// Recursively ensure IDs for nested groups
|
||||
return ensureIds(item);
|
||||
} else {
|
||||
// Ensure condition has an id
|
||||
if (!item.id) {
|
||||
item.id = getUUID();
|
||||
}
|
||||
return item;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return group;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates a condition group (root or nested) with new data
|
||||
* This is called when FilterGroup emits add-condition or add-group events
|
||||
*
|
||||
* @param updatedGroup - The updated group emitted by FilterGroup
|
||||
* @param context - Context object containing formData
|
||||
*/
|
||||
export const updateGroup = (updatedGroup: any, context: TransformContext): void => {
|
||||
const { formData } = context;
|
||||
formData.query_condition.conditions.items.forEach((element: any) => {
|
||||
if (element.groupId === updatedGroup.groupId) {
|
||||
element.items = updatedGroup.items;
|
||||
const rootGroup = formData.query_condition.conditions;
|
||||
|
||||
// If the updated group is the root group, replace it entirely
|
||||
if (rootGroup.groupId === updatedGroup.groupId) {
|
||||
formData.query_condition.conditions = updatedGroup;
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, find and update the nested group recursively
|
||||
const updateNestedGroup = (group: any, updated: any): boolean => {
|
||||
// V2: Use conditions array, V1: use items array
|
||||
const itemsArray = group.conditions || group.items;
|
||||
if (!itemsArray || !Array.isArray(itemsArray)) return false;
|
||||
|
||||
for (let i = 0; i < itemsArray.length; i++) {
|
||||
const item = itemsArray[i];
|
||||
|
||||
// Check if this is the group to update
|
||||
if (item.groupId && item.groupId === updated.groupId) {
|
||||
// Replace the entire group object
|
||||
if (group.conditions) {
|
||||
group.conditions[i] = updated;
|
||||
} else {
|
||||
group.items[i] = updated;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Recursively check nested groups
|
||||
const nestedItems = item.conditions || item.items;
|
||||
if (nestedItems && Array.isArray(nestedItems)) {
|
||||
if (updateNestedGroup(item, updated)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return false;
|
||||
};
|
||||
|
||||
updateNestedGroup(rootGroup, updatedGroup);
|
||||
};
|
||||
|
||||
export const removeConditionGroup = (
|
||||
@@ -25,8 +138,10 @@ export const removeConditionGroup = (
|
||||
): void => {
|
||||
const { formData } = context;
|
||||
const groupToProcess = currentGroup || formData.query_condition.conditions;
|
||||
|
||||
if (!groupToProcess?.items || !Array.isArray(groupToProcess.items)) return;
|
||||
|
||||
// V2: Use conditions array, V1: use items array
|
||||
const itemsArray = groupToProcess?.conditions || groupToProcess?.items;
|
||||
if (!itemsArray || !Array.isArray(itemsArray)) return;
|
||||
|
||||
const filterEmptyGroups = (items: any[]): any[] => {
|
||||
return items.filter((item: any) => {
|
||||
@@ -34,16 +149,29 @@ export const removeConditionGroup = (
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item.items && Array.isArray(item.items)) {
|
||||
item.items = filterEmptyGroups(item.items);
|
||||
return item.items.length > 0;
|
||||
// V2: check conditions, V1: check items
|
||||
const nestedItems = item.conditions || item.items;
|
||||
if (nestedItems && Array.isArray(nestedItems)) {
|
||||
const filtered = filterEmptyGroups(nestedItems);
|
||||
if (item.conditions) {
|
||||
item.conditions = filtered;
|
||||
return item.conditions.length > 0;
|
||||
} else {
|
||||
item.items = filtered;
|
||||
return item.items.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
groupToProcess.items = filterEmptyGroups(groupToProcess.items);
|
||||
const filtered = filterEmptyGroups(itemsArray);
|
||||
if (groupToProcess.conditions) {
|
||||
groupToProcess.conditions = filtered;
|
||||
} else {
|
||||
groupToProcess.items = filtered;
|
||||
}
|
||||
};
|
||||
|
||||
export const transformFEToBE = (node: any): any => {
|
||||
@@ -113,4 +241,262 @@ export const retransformBEToFE = (data: any): any => {
|
||||
label,
|
||||
items: transformedItems,
|
||||
};
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// VERSION 2 UTILITIES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Detects which version of conditions structure we have:
|
||||
*
|
||||
* V0 (Original): Flat array of conditions, no groups, implicit AND between all
|
||||
* Example: [{column: "x", operator: "=", value: "1"}, ...]
|
||||
*
|
||||
* V1 (Tree-based): Nested structure with {and: [...]} or {or: [...]} or {label, items, groupId}
|
||||
* Example: {and: [{column: "x", operator: "=", value: "1"}]} or
|
||||
* {label: "and", items: [...], groupId: "..."}
|
||||
*
|
||||
* V2 (Linear): New structure with filterType, logicalOperator per condition
|
||||
* Example: {filterType: "group", logicalOperator: "AND", conditions: [...]}
|
||||
*
|
||||
* Note: version field should be at parent level (query_condition.version), not inside conditions
|
||||
*/
|
||||
export const detectConditionsVersion = (conditions: any): 0 | 1 | 2 => {
|
||||
if (!conditions) return 0;
|
||||
|
||||
// Check for V2 structure (newest)
|
||||
if (conditions.filterType === "group" && conditions.conditions && Array.isArray(conditions.conditions)) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Check for V1 structure (tree-based with nesting)
|
||||
if (conditions.and || conditions.or || (conditions.label && conditions.items)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Check for V0 structure (flat array of conditions)
|
||||
if (Array.isArray(conditions) && conditions.length > 0) {
|
||||
// It's a flat array - V0
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Empty or unknown - default to V0
|
||||
return 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts V0 flat array to V2 format
|
||||
* V0: [{column: "x", operator: "=", value: "1"}, ...]
|
||||
* V2: {filterType: "group", logicalOperator: "AND", conditions: [...]}
|
||||
* Note: V0 had implicit AND between all conditions (no groups)
|
||||
*/
|
||||
export const convertV0ToV2 = (v0Data: any[]): V2Group => {
|
||||
if (!Array.isArray(v0Data) || v0Data.length === 0) {
|
||||
return {
|
||||
filterType: "group",
|
||||
logicalOperator: "AND",
|
||||
conditions: [],
|
||||
groupId: getUUID(),
|
||||
};
|
||||
}
|
||||
|
||||
// Convert each flat condition to V2 format
|
||||
const conditions: V2Condition[] = v0Data.map((item: any, index: number) => {
|
||||
const condition: V2Condition = {
|
||||
filterType: "condition",
|
||||
column: item.column || "",
|
||||
operator: item.operator || "=",
|
||||
value: item.value || "",
|
||||
values: item.values || [],
|
||||
logicalOperator: "AND", // V0 had implicit AND between all
|
||||
id: item.id || getUUID(),
|
||||
};
|
||||
|
||||
return condition;
|
||||
});
|
||||
|
||||
return {
|
||||
filterType: "group",
|
||||
logicalOperator: "AND",
|
||||
conditions: conditions,
|
||||
groupId: getUUID(),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts V1 frontend format (with groupId, label, items) to V2 format
|
||||
* V1: {groupId, label: "and", items: [...]}
|
||||
* V2: {filterType: "group", logicalOperator: "AND", conditions: [...]}
|
||||
*/
|
||||
export const convertV1ToV2 = (v1Data: any, isFirstGroup: boolean = true): V2Group => {
|
||||
if (!v1Data) {
|
||||
return {
|
||||
filterType: "group",
|
||||
logicalOperator: "AND",
|
||||
conditions: [],
|
||||
};
|
||||
}
|
||||
|
||||
// If it's already V2 format, return as is
|
||||
if (v1Data.filterType === "group") {
|
||||
return v1Data;
|
||||
}
|
||||
// ["c1","c2","g1","c3","g4"]
|
||||
// ["g1"]
|
||||
const items = v1Data.items || [];
|
||||
const label = v1Data.label || "and";
|
||||
const logicalOperator = label.toUpperCase() as "AND" | "OR";
|
||||
|
||||
const conditions: (V2Condition | V2Group)[] = items.map((item: any, index: number) => {
|
||||
// Check if it's a nested group
|
||||
// V1 groups have 'items' array (and optionally 'label' and 'groupId')
|
||||
if (item.items && Array.isArray(item.items)) {
|
||||
// It's a V1 group, convert recursively
|
||||
return convertV1ToV2(item, false);
|
||||
}
|
||||
|
||||
// It's a condition
|
||||
const condition: V2Condition = {
|
||||
filterType: "condition",
|
||||
column: item.column || "",
|
||||
operator: item.operator || "=",
|
||||
value: item.value || "",
|
||||
values: item.values || [],
|
||||
logicalOperator: logicalOperator,
|
||||
};
|
||||
|
||||
// Keep the id if it exists
|
||||
if (item.id) {
|
||||
condition.id = item.id;
|
||||
}
|
||||
|
||||
return condition;
|
||||
});
|
||||
|
||||
const result: V2Group = {
|
||||
filterType: "group",
|
||||
logicalOperator: logicalOperator,
|
||||
conditions: conditions,
|
||||
};
|
||||
|
||||
// Keep groupId if it exists (useful for frontend tracking)
|
||||
if (v1Data.groupId) {
|
||||
result.groupId = v1Data.groupId;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts V1 backend format to V2 format
|
||||
* V1 BE: {and: [{column, operator, value}]} or {or: [...]}
|
||||
* V2: {filterType: "group", logicalOperator: "AND", conditions: [...]}
|
||||
*
|
||||
* In V1: Group has operator that applies between all items in that group
|
||||
* In V2: Each condition has operator that determines how it connects to the NEXT item
|
||||
*/
|
||||
export const convertV1BEToV2 = (v1BEData: any): V2Group => {
|
||||
// we will check if v1bedata is there or not
|
||||
// if not we will return empty v2 format
|
||||
if (!v1BEData) {
|
||||
return {
|
||||
filterType: "group",
|
||||
logicalOperator: "AND",
|
||||
conditions: [],
|
||||
};
|
||||
}
|
||||
|
||||
// If it's already V2 format, return as is
|
||||
// it wont execute because we are checking the version before triggering this function
|
||||
// but incase if we get to this point
|
||||
// we will return the entire data as v2 format
|
||||
if (v1BEData.filterType === "group") {
|
||||
return v1BEData;
|
||||
}
|
||||
|
||||
// Get the operator key (and/or)
|
||||
// here we will get the toplevel operator like or / and
|
||||
// becuase based on that only v1 got built for example
|
||||
// {
|
||||
// "or":[
|
||||
// "cond1",
|
||||
// "cond2",
|
||||
// "group1",
|
||||
// "cond3",
|
||||
// "group2"
|
||||
// ]
|
||||
// }
|
||||
// only group
|
||||
// {
|
||||
// "or":[
|
||||
// "g1"
|
||||
// ]
|
||||
// }
|
||||
//only condition
|
||||
// {
|
||||
// "or":[
|
||||
// "c1"
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// here also if we dont have that key we will return empty list
|
||||
const keys = Object.keys(v1BEData); //or / and
|
||||
if (keys.length === 0) {
|
||||
return {
|
||||
filterType: "group",
|
||||
logicalOperator: "AND",
|
||||
conditions: [],
|
||||
};
|
||||
}
|
||||
|
||||
const operatorKey = keys[0]; // "and" or "or" ["or"]
|
||||
// here we will get operator key and based on that we will extract all the condition and groups
|
||||
// items will be after extracting
|
||||
// ["cond1", "cond2", "group1", "cond3", "group2"]
|
||||
//if only group is there
|
||||
// ["group1"]
|
||||
const items = v1BEData[operatorKey];
|
||||
const logicalOperator = operatorKey.toUpperCase() as "AND" | "OR";
|
||||
//in recursive call
|
||||
//the current logicalOperator will be and
|
||||
|
||||
const conditions: (V2Condition | V2Group)[] = items.map((item: any, index: number) => {
|
||||
// Check if it's a nested group
|
||||
// here we will map all the items one by one and if we find any group
|
||||
// then we will again send that to conversion recursively and whatever operator we had for this particualr group before we send
|
||||
// we will assign it to that nested group logicalOperator
|
||||
if (item.and || item.or) {
|
||||
// For nested groups, recursively convert
|
||||
const nestedGroup = convertV1BEToV2(item);
|
||||
|
||||
// CRITICAL: The nested group's logicalOperator should be set to the PARENT's operator
|
||||
// This is used to determine what operator comes BEFORE this group
|
||||
nestedGroup.logicalOperator = logicalOperator;
|
||||
|
||||
return nestedGroup;
|
||||
}
|
||||
|
||||
// It's a condition
|
||||
// The condition's logicalOperator is the parent group's operator
|
||||
// (All items in a group use the same operator from the parent)
|
||||
const condition: V2Condition = {
|
||||
filterType: "condition",
|
||||
column: item.column || "",
|
||||
operator: item.operator || "=",
|
||||
value: item.value || "",
|
||||
values: item.values || [],
|
||||
logicalOperator: logicalOperator,
|
||||
id: item.id || getUUID(),
|
||||
};
|
||||
|
||||
return condition;
|
||||
});
|
||||
|
||||
return {
|
||||
filterType: "group",
|
||||
logicalOperator: logicalOperator,
|
||||
conditions: conditions,
|
||||
groupId: getUUID(),
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user