Monitor Conditions (#5048)
parent
032ac161f7
commit
36f8be040d
@ -0,0 +1,12 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.alterTable("monitor", function (table) {
|
||||||
|
table.text("conditions").notNullable().defaultTo("[]");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema.alterTable("monitor", function (table) {
|
||||||
|
table.dropColumn("conditions");
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,71 @@
|
|||||||
|
const { ConditionExpressionGroup, ConditionExpression, LOGICAL } = require("./expression");
|
||||||
|
const { operatorMap } = require("./operators");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ConditionExpression} expression Expression to evaluate
|
||||||
|
* @param {object} context Context to evaluate against; These are values for variables in the expression
|
||||||
|
* @returns {boolean} Whether the expression evaluates true or false
|
||||||
|
* @throws {Error}
|
||||||
|
*/
|
||||||
|
function evaluateExpression(expression, context) {
|
||||||
|
/**
|
||||||
|
* @type {import("./operators").ConditionOperator|null}
|
||||||
|
*/
|
||||||
|
const operator = operatorMap.get(expression.operator) || null;
|
||||||
|
if (operator === null) {
|
||||||
|
throw new Error("Unexpected expression operator ID '" + expression.operator + "'. Expected one of [" + operatorMap.keys().join(",") + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(context, expression.variable)) {
|
||||||
|
throw new Error("Variable missing in context: " + expression.variable);
|
||||||
|
}
|
||||||
|
|
||||||
|
return operator.test(context[expression.variable], expression.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ConditionExpressionGroup} group Group of expressions to evaluate
|
||||||
|
* @param {object} context Context to evaluate against; These are values for variables in the expression
|
||||||
|
* @returns {boolean} Whether the group evaluates true or false
|
||||||
|
* @throws {Error}
|
||||||
|
*/
|
||||||
|
function evaluateExpressionGroup(group, context) {
|
||||||
|
if (!group.children.length) {
|
||||||
|
throw new Error("ConditionExpressionGroup must contain at least one child.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = null;
|
||||||
|
|
||||||
|
for (const child of group.children) {
|
||||||
|
let childResult;
|
||||||
|
|
||||||
|
if (child instanceof ConditionExpression) {
|
||||||
|
childResult = evaluateExpression(child, context);
|
||||||
|
} else if (child instanceof ConditionExpressionGroup) {
|
||||||
|
childResult = evaluateExpressionGroup(child, context);
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid child type in ConditionExpressionGroup. Expected ConditionExpression or ConditionExpressionGroup");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result === null) {
|
||||||
|
result = childResult; // Initialize result with the first child's result
|
||||||
|
} else if (child.andOr === LOGICAL.OR) {
|
||||||
|
result = result || childResult;
|
||||||
|
} else if (child.andOr === LOGICAL.AND) {
|
||||||
|
result = result && childResult;
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid logical operator in child of ConditionExpressionGroup. Expected 'and' or 'or'. Got '" + group.andOr + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result === null) {
|
||||||
|
throw new Error("ConditionExpressionGroup did not result in a boolean.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
evaluateExpression,
|
||||||
|
evaluateExpressionGroup,
|
||||||
|
};
|
@ -0,0 +1,111 @@
|
|||||||
|
/**
|
||||||
|
* @readonly
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
const LOGICAL = {
|
||||||
|
AND: "and",
|
||||||
|
OR: "or",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively processes an array of raw condition objects and populates the given parent group with
|
||||||
|
* corresponding ConditionExpression or ConditionExpressionGroup instances.
|
||||||
|
* @param {Array} conditions Array of raw condition objects, where each object represents either a group or an expression.
|
||||||
|
* @param {ConditionExpressionGroup} parentGroup The parent group to which the instantiated ConditionExpression or ConditionExpressionGroup objects will be added.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function processMonitorConditions(conditions, parentGroup) {
|
||||||
|
conditions.forEach(condition => {
|
||||||
|
const andOr = condition.andOr === LOGICAL.OR ? LOGICAL.OR : LOGICAL.AND;
|
||||||
|
|
||||||
|
if (condition.type === "group") {
|
||||||
|
const group = new ConditionExpressionGroup([], andOr);
|
||||||
|
|
||||||
|
// Recursively process the group's children
|
||||||
|
processMonitorConditions(condition.children, group);
|
||||||
|
|
||||||
|
parentGroup.children.push(group);
|
||||||
|
} else if (condition.type === "expression") {
|
||||||
|
const expression = new ConditionExpression(condition.variable, condition.operator, condition.value, andOr);
|
||||||
|
parentGroup.children.push(expression);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConditionExpressionGroup {
|
||||||
|
/**
|
||||||
|
* @type {ConditionExpressionGroup[]|ConditionExpression[]} Groups and/or expressions to test
|
||||||
|
*/
|
||||||
|
children = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {LOGICAL} Connects group result with previous group/expression results
|
||||||
|
*/
|
||||||
|
andOr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ConditionExpressionGroup[]|ConditionExpression[]} children Groups and/or expressions to test
|
||||||
|
* @param {LOGICAL} andOr Connects group result with previous group/expression results
|
||||||
|
*/
|
||||||
|
constructor(children = [], andOr = LOGICAL.AND) {
|
||||||
|
this.children = children;
|
||||||
|
this.andOr = andOr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Monitor} monitor Monitor instance
|
||||||
|
* @returns {ConditionExpressionGroup|null} A ConditionExpressionGroup with the Monitor's conditions
|
||||||
|
*/
|
||||||
|
static fromMonitor(monitor) {
|
||||||
|
const conditions = JSON.parse(monitor.conditions);
|
||||||
|
if (conditions.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = new ConditionExpressionGroup();
|
||||||
|
processMonitorConditions(conditions, root);
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConditionExpression {
|
||||||
|
/**
|
||||||
|
* @type {string} ID of variable
|
||||||
|
*/
|
||||||
|
variable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {string} ID of operator
|
||||||
|
*/
|
||||||
|
operator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {string} Value to test with the operator
|
||||||
|
*/
|
||||||
|
value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {LOGICAL} Connects expression result with previous group/expression results
|
||||||
|
*/
|
||||||
|
andOr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} variable ID of variable to test against
|
||||||
|
* @param {string} operator ID of operator to test the variable with
|
||||||
|
* @param {string} value Value to test with the operator
|
||||||
|
* @param {LOGICAL} andOr Connects expression result with previous group/expression results
|
||||||
|
*/
|
||||||
|
constructor(variable, operator, value, andOr = LOGICAL.AND) {
|
||||||
|
this.variable = variable;
|
||||||
|
this.operator = operator;
|
||||||
|
this.value = value;
|
||||||
|
this.andOr = andOr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
LOGICAL,
|
||||||
|
ConditionExpressionGroup,
|
||||||
|
ConditionExpression,
|
||||||
|
};
|
@ -0,0 +1,318 @@
|
|||||||
|
class ConditionOperator {
|
||||||
|
id = undefined;
|
||||||
|
caption = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {mixed} variable
|
||||||
|
* @type {mixed} value
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
throw new Error("You need to override test()");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const OP_STR_EQUALS = "equals";
|
||||||
|
|
||||||
|
const OP_STR_NOT_EQUALS = "not_equals";
|
||||||
|
|
||||||
|
const OP_CONTAINS = "contains";
|
||||||
|
|
||||||
|
const OP_NOT_CONTAINS = "not_contains";
|
||||||
|
|
||||||
|
const OP_STARTS_WITH = "starts_with";
|
||||||
|
|
||||||
|
const OP_NOT_STARTS_WITH = "not_starts_with";
|
||||||
|
|
||||||
|
const OP_ENDS_WITH = "ends_with";
|
||||||
|
|
||||||
|
const OP_NOT_ENDS_WITH = "not_ends_with";
|
||||||
|
|
||||||
|
const OP_NUM_EQUALS = "num_equals";
|
||||||
|
|
||||||
|
const OP_NUM_NOT_EQUALS = "num_not_equals";
|
||||||
|
|
||||||
|
const OP_LT = "lt";
|
||||||
|
|
||||||
|
const OP_GT = "gt";
|
||||||
|
|
||||||
|
const OP_LTE = "lte";
|
||||||
|
|
||||||
|
const OP_GTE = "gte";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a variable is equal to a value.
|
||||||
|
*/
|
||||||
|
class StringEqualsOperator extends ConditionOperator {
|
||||||
|
id = OP_STR_EQUALS;
|
||||||
|
caption = "equals";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
return variable === value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a variable is not equal to a value.
|
||||||
|
*/
|
||||||
|
class StringNotEqualsOperator extends ConditionOperator {
|
||||||
|
id = OP_STR_NOT_EQUALS;
|
||||||
|
caption = "not equals";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
return variable !== value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a variable contains a value.
|
||||||
|
* Handles both Array and String variable types.
|
||||||
|
*/
|
||||||
|
class ContainsOperator extends ConditionOperator {
|
||||||
|
id = OP_CONTAINS;
|
||||||
|
caption = "contains";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
if (Array.isArray(variable)) {
|
||||||
|
return variable.includes(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return variable.indexOf(value) !== -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a variable does not contain a value.
|
||||||
|
* Handles both Array and String variable types.
|
||||||
|
*/
|
||||||
|
class NotContainsOperator extends ConditionOperator {
|
||||||
|
id = OP_NOT_CONTAINS;
|
||||||
|
caption = "not contains";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
if (Array.isArray(variable)) {
|
||||||
|
return !variable.includes(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return variable.indexOf(value) === -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a variable starts with a value.
|
||||||
|
*/
|
||||||
|
class StartsWithOperator extends ConditionOperator {
|
||||||
|
id = OP_STARTS_WITH;
|
||||||
|
caption = "starts with";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
return variable.startsWith(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a variable does not start with a value.
|
||||||
|
*/
|
||||||
|
class NotStartsWithOperator extends ConditionOperator {
|
||||||
|
id = OP_NOT_STARTS_WITH;
|
||||||
|
caption = "not starts with";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
return !variable.startsWith(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a variable ends with a value.
|
||||||
|
*/
|
||||||
|
class EndsWithOperator extends ConditionOperator {
|
||||||
|
id = OP_ENDS_WITH;
|
||||||
|
caption = "ends with";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
return variable.endsWith(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a variable does not end with a value.
|
||||||
|
*/
|
||||||
|
class NotEndsWithOperator extends ConditionOperator {
|
||||||
|
id = OP_NOT_ENDS_WITH;
|
||||||
|
caption = "not ends with";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
return !variable.endsWith(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a numeric variable is equal to a value.
|
||||||
|
*/
|
||||||
|
class NumberEqualsOperator extends ConditionOperator {
|
||||||
|
id = OP_NUM_EQUALS;
|
||||||
|
caption = "equals";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
return variable === Number(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a numeric variable is not equal to a value.
|
||||||
|
*/
|
||||||
|
class NumberNotEqualsOperator extends ConditionOperator {
|
||||||
|
id = OP_NUM_NOT_EQUALS;
|
||||||
|
caption = "not equals";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
return variable !== Number(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a variable is less than a value.
|
||||||
|
*/
|
||||||
|
class LessThanOperator extends ConditionOperator {
|
||||||
|
id = OP_LT;
|
||||||
|
caption = "less than";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
return variable < Number(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a variable is greater than a value.
|
||||||
|
*/
|
||||||
|
class GreaterThanOperator extends ConditionOperator {
|
||||||
|
id = OP_GT;
|
||||||
|
caption = "greater than";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
return variable > Number(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a variable is less than or equal to a value.
|
||||||
|
*/
|
||||||
|
class LessThanOrEqualToOperator extends ConditionOperator {
|
||||||
|
id = OP_LTE;
|
||||||
|
caption = "less than or equal to";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
return variable <= Number(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts a variable is greater than or equal to a value.
|
||||||
|
*/
|
||||||
|
class GreaterThanOrEqualToOperator extends ConditionOperator {
|
||||||
|
id = OP_GTE;
|
||||||
|
caption = "greater than or equal to";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
test(variable, value) {
|
||||||
|
return variable >= Number(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const operatorMap = new Map([
|
||||||
|
[ OP_STR_EQUALS, new StringEqualsOperator ],
|
||||||
|
[ OP_STR_NOT_EQUALS, new StringNotEqualsOperator ],
|
||||||
|
[ OP_CONTAINS, new ContainsOperator ],
|
||||||
|
[ OP_NOT_CONTAINS, new NotContainsOperator ],
|
||||||
|
[ OP_STARTS_WITH, new StartsWithOperator ],
|
||||||
|
[ OP_NOT_STARTS_WITH, new NotStartsWithOperator ],
|
||||||
|
[ OP_ENDS_WITH, new EndsWithOperator ],
|
||||||
|
[ OP_NOT_ENDS_WITH, new NotEndsWithOperator ],
|
||||||
|
[ OP_NUM_EQUALS, new NumberEqualsOperator ],
|
||||||
|
[ OP_NUM_NOT_EQUALS, new NumberNotEqualsOperator ],
|
||||||
|
[ OP_LT, new LessThanOperator ],
|
||||||
|
[ OP_GT, new GreaterThanOperator ],
|
||||||
|
[ OP_LTE, new LessThanOrEqualToOperator ],
|
||||||
|
[ OP_GTE, new GreaterThanOrEqualToOperator ],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const defaultStringOperators = [
|
||||||
|
operatorMap.get(OP_STR_EQUALS),
|
||||||
|
operatorMap.get(OP_STR_NOT_EQUALS),
|
||||||
|
operatorMap.get(OP_CONTAINS),
|
||||||
|
operatorMap.get(OP_NOT_CONTAINS),
|
||||||
|
operatorMap.get(OP_STARTS_WITH),
|
||||||
|
operatorMap.get(OP_NOT_STARTS_WITH),
|
||||||
|
operatorMap.get(OP_ENDS_WITH),
|
||||||
|
operatorMap.get(OP_NOT_ENDS_WITH)
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultNumberOperators = [
|
||||||
|
operatorMap.get(OP_NUM_EQUALS),
|
||||||
|
operatorMap.get(OP_NUM_NOT_EQUALS),
|
||||||
|
operatorMap.get(OP_LT),
|
||||||
|
operatorMap.get(OP_GT),
|
||||||
|
operatorMap.get(OP_LTE),
|
||||||
|
operatorMap.get(OP_GTE)
|
||||||
|
];
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
OP_STR_EQUALS,
|
||||||
|
OP_STR_NOT_EQUALS,
|
||||||
|
OP_CONTAINS,
|
||||||
|
OP_NOT_CONTAINS,
|
||||||
|
OP_STARTS_WITH,
|
||||||
|
OP_NOT_STARTS_WITH,
|
||||||
|
OP_ENDS_WITH,
|
||||||
|
OP_NOT_ENDS_WITH,
|
||||||
|
OP_NUM_EQUALS,
|
||||||
|
OP_NUM_NOT_EQUALS,
|
||||||
|
OP_LT,
|
||||||
|
OP_GT,
|
||||||
|
OP_LTE,
|
||||||
|
OP_GTE,
|
||||||
|
operatorMap,
|
||||||
|
defaultStringOperators,
|
||||||
|
defaultNumberOperators,
|
||||||
|
ConditionOperator,
|
||||||
|
};
|
@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Represents a variable used in a condition and the set of operators that can be applied to this variable.
|
||||||
|
*
|
||||||
|
* A `ConditionVariable` holds the ID of the variable and a list of operators that define how this variable can be evaluated
|
||||||
|
* in conditions. For example, if the variable is a request body or a specific field in a request, the operators can include
|
||||||
|
* operations such as equality checks, comparisons, or other custom evaluations.
|
||||||
|
*/
|
||||||
|
class ConditionVariable {
|
||||||
|
/**
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import("./operators").ConditionOperator[]}
|
||||||
|
*/
|
||||||
|
operators = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} id ID of variable
|
||||||
|
* @param {import("./operators").ConditionOperator[]} operators Operators the condition supports
|
||||||
|
*/
|
||||||
|
constructor(id, operators = []) {
|
||||||
|
this.id = id;
|
||||||
|
this.operators = operators;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ConditionVariable,
|
||||||
|
};
|
@ -0,0 +1,152 @@
|
|||||||
|
<template>
|
||||||
|
<div class="monitor-condition mb-3" data-testid="condition">
|
||||||
|
<button
|
||||||
|
v-if="!isInGroup || !isFirst || !isLast"
|
||||||
|
class="btn btn-outline-danger remove-button"
|
||||||
|
type="button"
|
||||||
|
:aria-label="$t('conditionDelete')"
|
||||||
|
data-testid="remove-condition"
|
||||||
|
@click="remove"
|
||||||
|
>
|
||||||
|
<font-awesome-icon icon="trash" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<select v-if="!isFirst" v-model="model.andOr" class="form-select and-or-select" data-testid="condition-and-or">
|
||||||
|
<option value="and">{{ $t("and") }}</option>
|
||||||
|
<option value="or">{{ $t("or") }}</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select v-model="model.variable" class="form-select" data-testid="condition-variable">
|
||||||
|
<option
|
||||||
|
v-for="variable in conditionVariables"
|
||||||
|
:key="variable.id"
|
||||||
|
:value="variable.id"
|
||||||
|
>
|
||||||
|
{{ $t(variable.id) }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select v-model="model.operator" class="form-select" data-testid="condition-operator">
|
||||||
|
<option
|
||||||
|
v-for="operator in getVariableOperators(model.variable)"
|
||||||
|
:key="operator.id"
|
||||||
|
:value="operator.id"
|
||||||
|
>
|
||||||
|
{{ $t(operator.caption) }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<input
|
||||||
|
v-model="model.value"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:aria-label="$t('conditionValuePlaceholder')"
|
||||||
|
data-testid="condition-value"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "EditMonitorCondition",
|
||||||
|
|
||||||
|
props: {
|
||||||
|
/**
|
||||||
|
* The monitor condition
|
||||||
|
*/
|
||||||
|
modelValue: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this is the first condition
|
||||||
|
*/
|
||||||
|
isFirst: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this is the last condition
|
||||||
|
*/
|
||||||
|
isLast: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this condition is in a group
|
||||||
|
*/
|
||||||
|
isInGroup: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variable choices
|
||||||
|
*/
|
||||||
|
conditionVariables: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: [ "update:modelValue", "remove" ],
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
model: {
|
||||||
|
get() {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit("update:modelValue", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
remove() {
|
||||||
|
this.$emit("remove", this.model);
|
||||||
|
},
|
||||||
|
|
||||||
|
getVariableOperators(variableId) {
|
||||||
|
return this.conditionVariables.find(v => v.id === variableId)?.operators ?? [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.monitor-condition {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-button {
|
||||||
|
justify-self: flex-end;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (min-width: 500px) {
|
||||||
|
.monitor-condition {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-button {
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-left: 10px;
|
||||||
|
order: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.and-or-select {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,189 @@
|
|||||||
|
<template>
|
||||||
|
<div class="condition-group mb-3" data-testid="condition-group">
|
||||||
|
<div class="d-flex">
|
||||||
|
<select v-if="!isFirst" v-model="model.andOr" class="form-select" style="width: auto;" data-testid="condition-group-and-or">
|
||||||
|
<option value="and">{{ $t("and") }}</option>
|
||||||
|
<option value="or">{{ $t("or") }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="condition-group-inner mt-2 pa-2">
|
||||||
|
<div class="condition-group-conditions">
|
||||||
|
<template v-for="(child, childIndex) in model.children" :key="childIndex">
|
||||||
|
<EditMonitorConditionGroup
|
||||||
|
v-if="child.type === 'group'"
|
||||||
|
v-model="model.children[childIndex]"
|
||||||
|
:is-first="childIndex === 0"
|
||||||
|
:get-new-group="getNewGroup"
|
||||||
|
:get-new-condition="getNewCondition"
|
||||||
|
:condition-variables="conditionVariables"
|
||||||
|
@remove="removeChild"
|
||||||
|
/>
|
||||||
|
<EditMonitorCondition
|
||||||
|
v-else
|
||||||
|
v-model="model.children[childIndex]"
|
||||||
|
:is-first="childIndex === 0"
|
||||||
|
:is-last="childIndex === model.children.length - 1"
|
||||||
|
:is-in-group="true"
|
||||||
|
:condition-variables="conditionVariables"
|
||||||
|
@remove="removeChild"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="condition-group-actions mt-3">
|
||||||
|
<button class="btn btn-outline-secondary me-2" type="button" data-testid="add-condition-button" @click="addCondition">
|
||||||
|
{{ $t("conditionAdd") }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary me-2" type="button" data-testid="add-group-button" @click="addGroup">
|
||||||
|
{{ $t("conditionAddGroup") }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-danger"
|
||||||
|
type="button"
|
||||||
|
:aria-label="$t('conditionDeleteGroup')"
|
||||||
|
data-testid="remove-condition-group"
|
||||||
|
@click="remove"
|
||||||
|
>
|
||||||
|
<font-awesome-icon icon="trash" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import EditMonitorCondition from "./EditMonitorCondition.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "EditMonitorConditionGroup",
|
||||||
|
|
||||||
|
components: {
|
||||||
|
EditMonitorCondition,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
/**
|
||||||
|
* The condition group
|
||||||
|
*/
|
||||||
|
modelValue: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this is the first condition
|
||||||
|
*/
|
||||||
|
isFirst: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to generate a new group model
|
||||||
|
*/
|
||||||
|
getNewGroup: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to generate a new condition model
|
||||||
|
*/
|
||||||
|
getNewCondition: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variable choices
|
||||||
|
*/
|
||||||
|
conditionVariables: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: [ "update:modelValue", "remove" ],
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
model: {
|
||||||
|
get() {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit("update:modelValue", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
addGroup() {
|
||||||
|
const conditions = [ ...this.model.children ];
|
||||||
|
conditions.push(this.getNewGroup());
|
||||||
|
this.model.children = conditions;
|
||||||
|
},
|
||||||
|
|
||||||
|
addCondition() {
|
||||||
|
const conditions = [ ...this.model.children ];
|
||||||
|
conditions.push(this.getNewCondition());
|
||||||
|
this.model.children = conditions;
|
||||||
|
},
|
||||||
|
|
||||||
|
remove() {
|
||||||
|
this.$emit("remove", this.model);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeChild(child) {
|
||||||
|
const idx = this.model.children.indexOf(child);
|
||||||
|
if (idx !== -1) {
|
||||||
|
this.model.children.splice(idx, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.condition-group-inner {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .condition-group-inner {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.condition-group-conditions {
|
||||||
|
container-type: inline-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.condition-group-actions {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete button
|
||||||
|
.condition-group-actions > :last-child {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (min-width: 400px) {
|
||||||
|
.condition-group-actions {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete button
|
||||||
|
.condition-group-actions > :last-child {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete-group {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,149 @@
|
|||||||
|
<template>
|
||||||
|
<div class="monitor-conditions">
|
||||||
|
<label class="form-label">{{ $t("Conditions") }}</label>
|
||||||
|
<div class="monitor-conditions-conditions">
|
||||||
|
<template v-for="(condition, conditionIndex) in model" :key="conditionIndex">
|
||||||
|
<EditMonitorConditionGroup
|
||||||
|
v-if="condition.type === 'group'"
|
||||||
|
v-model="model[conditionIndex]"
|
||||||
|
:is-first="conditionIndex === 0"
|
||||||
|
:get-new-group="getNewGroup"
|
||||||
|
:get-new-condition="getNewCondition"
|
||||||
|
:condition-variables="conditionVariables"
|
||||||
|
@remove="removeCondition"
|
||||||
|
/>
|
||||||
|
<EditMonitorCondition
|
||||||
|
v-else
|
||||||
|
v-model="model[conditionIndex]"
|
||||||
|
:is-first="conditionIndex === 0"
|
||||||
|
:is-last="conditionIndex === model.length - 1"
|
||||||
|
:condition-variables="conditionVariables"
|
||||||
|
@remove="removeCondition"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="monitor-conditions-buttons">
|
||||||
|
<button class="btn btn-outline-secondary me-2" type="button" data-testid="add-condition-button" @click="addCondition">
|
||||||
|
{{ $t("conditionAdd") }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary me-2" type="button" data-testid="add-group-button" @click="addGroup">
|
||||||
|
{{ $t("conditionAddGroup") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import EditMonitorConditionGroup from "./EditMonitorConditionGroup.vue";
|
||||||
|
import EditMonitorCondition from "./EditMonitorCondition.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "EditMonitorConditions",
|
||||||
|
|
||||||
|
components: {
|
||||||
|
EditMonitorConditionGroup,
|
||||||
|
EditMonitorCondition,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
/**
|
||||||
|
* The monitor conditions
|
||||||
|
*/
|
||||||
|
modelValue: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
conditionVariables: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: [ "update:modelValue" ],
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
model: {
|
||||||
|
get() {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit("update:modelValue", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.model.length === 0) {
|
||||||
|
this.addCondition();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getNewGroup() {
|
||||||
|
return {
|
||||||
|
type: "group",
|
||||||
|
children: [ this.getNewCondition() ],
|
||||||
|
andOr: "and",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getNewCondition() {
|
||||||
|
const firstVariable = this.conditionVariables[0]?.id || null;
|
||||||
|
const firstOperator = this.getVariableOperators(firstVariable)[0] || null;
|
||||||
|
return {
|
||||||
|
type: "expression",
|
||||||
|
variable: firstVariable,
|
||||||
|
operator: firstOperator?.id || null,
|
||||||
|
value: "",
|
||||||
|
andOr: "and",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addGroup() {
|
||||||
|
const conditions = [ ...this.model ];
|
||||||
|
conditions.push(this.getNewGroup());
|
||||||
|
this.$emit("update:modelValue", conditions);
|
||||||
|
},
|
||||||
|
|
||||||
|
addCondition() {
|
||||||
|
const conditions = [ ...this.model ];
|
||||||
|
conditions.push(this.getNewCondition());
|
||||||
|
this.$emit("update:modelValue", conditions);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeCondition(condition) {
|
||||||
|
const conditions = [ ...this.model ];
|
||||||
|
const idx = conditions.indexOf(condition);
|
||||||
|
if (idx !== -1) {
|
||||||
|
conditions.splice(idx, 1);
|
||||||
|
this.$emit("update:modelValue", conditions);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getVariableOperators(variableId) {
|
||||||
|
return this.conditionVariables.find(v => v.id === variableId)?.operators ?? [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.monitor-conditions,
|
||||||
|
.monitor-conditions-conditions {
|
||||||
|
container-type: inline-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-conditions-buttons {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (min-width: 400px) {
|
||||||
|
.monitor-conditions-buttons {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,46 @@
|
|||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { ConditionExpressionGroup, ConditionExpression, LOGICAL } = require("../../../server/monitor-conditions/expression.js");
|
||||||
|
const { evaluateExpressionGroup, evaluateExpression } = require("../../../server/monitor-conditions/evaluator.js");
|
||||||
|
|
||||||
|
test("Test evaluateExpression", async (t) => {
|
||||||
|
const expr = new ConditionExpression("record", "contains", "mx1.example.com");
|
||||||
|
assert.strictEqual(true, evaluateExpression(expr, { record: "mx1.example.com" }));
|
||||||
|
assert.strictEqual(false, evaluateExpression(expr, { record: "mx2.example.com" }));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test evaluateExpressionGroup with logical AND", async (t) => {
|
||||||
|
const group = new ConditionExpressionGroup([
|
||||||
|
new ConditionExpression("record", "contains", "mx1."),
|
||||||
|
new ConditionExpression("record", "contains", "example.com", LOGICAL.AND),
|
||||||
|
]);
|
||||||
|
assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.com" }));
|
||||||
|
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1." }));
|
||||||
|
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.com" }));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test evaluateExpressionGroup with logical OR", async (t) => {
|
||||||
|
const group = new ConditionExpressionGroup([
|
||||||
|
new ConditionExpression("record", "contains", "example.com"),
|
||||||
|
new ConditionExpression("record", "contains", "example.org", LOGICAL.OR),
|
||||||
|
]);
|
||||||
|
assert.strictEqual(true, evaluateExpressionGroup(group, { record: "example.com" }));
|
||||||
|
assert.strictEqual(true, evaluateExpressionGroup(group, { record: "example.org" }));
|
||||||
|
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.net" }));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test evaluateExpressionGroup with nested group", async (t) => {
|
||||||
|
const group = new ConditionExpressionGroup([
|
||||||
|
new ConditionExpression("record", "contains", "mx1."),
|
||||||
|
new ConditionExpressionGroup([
|
||||||
|
new ConditionExpression("record", "contains", "example.com"),
|
||||||
|
new ConditionExpression("record", "contains", "example.org", LOGICAL.OR),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1." }));
|
||||||
|
assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.com" }));
|
||||||
|
assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.org" }));
|
||||||
|
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.com" }));
|
||||||
|
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.org" }));
|
||||||
|
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1.example.net" }));
|
||||||
|
});
|
@ -0,0 +1,55 @@
|
|||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { ConditionExpressionGroup, ConditionExpression } = require("../../../server/monitor-conditions/expression.js");
|
||||||
|
|
||||||
|
test("Test ConditionExpressionGroup.fromMonitor", async (t) => {
|
||||||
|
const monitor = {
|
||||||
|
conditions: JSON.stringify([
|
||||||
|
{
|
||||||
|
"type": "expression",
|
||||||
|
"andOr": "and",
|
||||||
|
"operator": "contains",
|
||||||
|
"value": "foo",
|
||||||
|
"variable": "record"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "group",
|
||||||
|
"andOr": "and",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "expression",
|
||||||
|
"andOr": "and",
|
||||||
|
"operator": "contains",
|
||||||
|
"value": "bar",
|
||||||
|
"variable": "record"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "group",
|
||||||
|
"andOr": "and",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "expression",
|
||||||
|
"andOr": "and",
|
||||||
|
"operator": "contains",
|
||||||
|
"value": "car",
|
||||||
|
"variable": "record"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
const root = ConditionExpressionGroup.fromMonitor(monitor);
|
||||||
|
assert.strictEqual(true, root.children.length === 2);
|
||||||
|
assert.strictEqual(true, root.children[0] instanceof ConditionExpression);
|
||||||
|
assert.strictEqual(true, root.children[0].value === "foo");
|
||||||
|
assert.strictEqual(true, root.children[1] instanceof ConditionExpressionGroup);
|
||||||
|
assert.strictEqual(true, root.children[1].children.length === 2);
|
||||||
|
assert.strictEqual(true, root.children[1].children[0] instanceof ConditionExpression);
|
||||||
|
assert.strictEqual(true, root.children[1].children[0].value === "bar");
|
||||||
|
assert.strictEqual(true, root.children[1].children[1] instanceof ConditionExpressionGroup);
|
||||||
|
assert.strictEqual(true, root.children[1].children[1].children.length === 1);
|
||||||
|
assert.strictEqual(true, root.children[1].children[1].children[0] instanceof ConditionExpression);
|
||||||
|
assert.strictEqual(true, root.children[1].children[1].children[0].value === "car");
|
||||||
|
});
|
@ -0,0 +1,108 @@
|
|||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { operatorMap, OP_CONTAINS, OP_NOT_CONTAINS, OP_LT, OP_GT, OP_LTE, OP_GTE, OP_STR_EQUALS, OP_STR_NOT_EQUALS, OP_NUM_EQUALS, OP_NUM_NOT_EQUALS, OP_STARTS_WITH, OP_ENDS_WITH, OP_NOT_STARTS_WITH, OP_NOT_ENDS_WITH } = require("../../../server/monitor-conditions/operators.js");
|
||||||
|
|
||||||
|
test("Test StringEqualsOperator", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_STR_EQUALS);
|
||||||
|
assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.com"));
|
||||||
|
assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.org"));
|
||||||
|
assert.strictEqual(false, op.test("1", 1)); // strict equality
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test StringNotEqualsOperator", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_STR_NOT_EQUALS);
|
||||||
|
assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.org"));
|
||||||
|
assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.com"));
|
||||||
|
assert.strictEqual(true, op.test(1, "1")); // variable is not typecasted (strict equality)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test ContainsOperator with scalar", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_CONTAINS);
|
||||||
|
assert.strictEqual(true, op.test("mx1.example.org", "example.org"));
|
||||||
|
assert.strictEqual(false, op.test("mx1.example.org", "example.com"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test ContainsOperator with array", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_CONTAINS);
|
||||||
|
assert.strictEqual(true, op.test([ "example.org" ], "example.org"));
|
||||||
|
assert.strictEqual(false, op.test([ "example.org" ], "example.com"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test NotContainsOperator with scalar", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_NOT_CONTAINS);
|
||||||
|
assert.strictEqual(true, op.test("example.org", ".com"));
|
||||||
|
assert.strictEqual(false, op.test("example.org", ".org"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test NotContainsOperator with array", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_NOT_CONTAINS);
|
||||||
|
assert.strictEqual(true, op.test([ "example.org" ], "example.com"));
|
||||||
|
assert.strictEqual(false, op.test([ "example.org" ], "example.org"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test StartsWithOperator", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_STARTS_WITH);
|
||||||
|
assert.strictEqual(true, op.test("mx1.example.com", "mx1"));
|
||||||
|
assert.strictEqual(false, op.test("mx1.example.com", "mx2"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test NotStartsWithOperator", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_NOT_STARTS_WITH);
|
||||||
|
assert.strictEqual(true, op.test("mx1.example.com", "mx2"));
|
||||||
|
assert.strictEqual(false, op.test("mx1.example.com", "mx1"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test EndsWithOperator", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_ENDS_WITH);
|
||||||
|
assert.strictEqual(true, op.test("mx1.example.com", "example.com"));
|
||||||
|
assert.strictEqual(false, op.test("mx1.example.com", "example.net"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test NotEndsWithOperator", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_NOT_ENDS_WITH);
|
||||||
|
assert.strictEqual(true, op.test("mx1.example.com", "example.net"));
|
||||||
|
assert.strictEqual(false, op.test("mx1.example.com", "example.com"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test NumberEqualsOperator", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_NUM_EQUALS);
|
||||||
|
assert.strictEqual(true, op.test(1, 1));
|
||||||
|
assert.strictEqual(true, op.test(1, "1"));
|
||||||
|
assert.strictEqual(false, op.test(1, "2"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test NumberNotEqualsOperator", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_NUM_NOT_EQUALS);
|
||||||
|
assert.strictEqual(true, op.test(1, "2"));
|
||||||
|
assert.strictEqual(false, op.test(1, "1"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test LessThanOperator", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_LT);
|
||||||
|
assert.strictEqual(true, op.test(1, 2));
|
||||||
|
assert.strictEqual(true, op.test(1, "2"));
|
||||||
|
assert.strictEqual(false, op.test(1, 1));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test GreaterThanOperator", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_GT);
|
||||||
|
assert.strictEqual(true, op.test(2, 1));
|
||||||
|
assert.strictEqual(true, op.test(2, "1"));
|
||||||
|
assert.strictEqual(false, op.test(1, 1));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test LessThanOrEqualToOperator", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_LTE);
|
||||||
|
assert.strictEqual(true, op.test(1, 1));
|
||||||
|
assert.strictEqual(true, op.test(1, 2));
|
||||||
|
assert.strictEqual(true, op.test(1, "2"));
|
||||||
|
assert.strictEqual(false, op.test(1, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test GreaterThanOrEqualToOperator", async (t) => {
|
||||||
|
const op = operatorMap.get(OP_GTE);
|
||||||
|
assert.strictEqual(true, op.test(1, 1));
|
||||||
|
assert.strictEqual(true, op.test(2, 1));
|
||||||
|
assert.strictEqual(true, op.test(2, "2"));
|
||||||
|
assert.strictEqual(false, op.test(2, 3));
|
||||||
|
});
|
@ -0,0 +1,109 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import { login, restoreSqliteSnapshot, screenshot } from "../util-test";
|
||||||
|
|
||||||
|
test.describe("Monitor Form", () => {
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await restoreSqliteSnapshot(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("condition ui", async ({ page }, testInfo) => {
|
||||||
|
await page.goto("./add");
|
||||||
|
await login(page);
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
|
||||||
|
const monitorTypeSelect = page.getByTestId("monitor-type-select");
|
||||||
|
await expect(monitorTypeSelect).toBeVisible();
|
||||||
|
|
||||||
|
await monitorTypeSelect.selectOption("dns");
|
||||||
|
const selectedValue = await monitorTypeSelect.evaluate(select => select.value);
|
||||||
|
expect(selectedValue).toBe("dns");
|
||||||
|
|
||||||
|
// Add Conditions & verify:
|
||||||
|
await page.getByTestId("add-condition-button").click();
|
||||||
|
expect(await page.getByTestId("condition").count()).toEqual(2); // 1 added by default + 1 explicitly added
|
||||||
|
|
||||||
|
// Add a Condition Group & verify:
|
||||||
|
await page.getByTestId("add-group-button").click();
|
||||||
|
expect(await page.getByTestId("condition-group").count()).toEqual(1);
|
||||||
|
expect(await page.getByTestId("condition").count()).toEqual(3); // 2 solo conditions + 1 condition in group
|
||||||
|
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
|
||||||
|
// Remove a condition & verify:
|
||||||
|
await page.getByTestId("remove-condition").first().click();
|
||||||
|
expect(await page.getByTestId("condition").count()).toEqual(2); // 1 solo condition + 1 condition in group
|
||||||
|
|
||||||
|
// Remove a condition group & verify:
|
||||||
|
await page.getByTestId("remove-condition-group").first().click();
|
||||||
|
expect(await page.getByTestId("condition-group").count()).toEqual(0);
|
||||||
|
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("successful condition", async ({ page }, testInfo) => {
|
||||||
|
await page.goto("./add");
|
||||||
|
await login(page);
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
|
||||||
|
const monitorTypeSelect = page.getByTestId("monitor-type-select");
|
||||||
|
await expect(monitorTypeSelect).toBeVisible();
|
||||||
|
|
||||||
|
await monitorTypeSelect.selectOption("dns");
|
||||||
|
const selectedValue = await monitorTypeSelect.evaluate(select => select.value);
|
||||||
|
expect(selectedValue).toBe("dns");
|
||||||
|
|
||||||
|
const friendlyName = "Example DNS NS";
|
||||||
|
await page.getByTestId("friendly-name-input").fill(friendlyName);
|
||||||
|
await page.getByTestId("hostname-input").fill("example.com");
|
||||||
|
|
||||||
|
// Vue-Multiselect component
|
||||||
|
const resolveTypeSelect = page.getByTestId("resolve-type-select");
|
||||||
|
await resolveTypeSelect.click();
|
||||||
|
await resolveTypeSelect.getByRole("option", { name: "NS" }).click();
|
||||||
|
|
||||||
|
await page.getByTestId("add-condition-button").click();
|
||||||
|
expect(await page.getByTestId("condition").count()).toEqual(2); // 1 added by default + 1 explicitly added
|
||||||
|
await page.getByTestId("condition-value").nth(0).fill("a.iana-servers.net");
|
||||||
|
await page.getByTestId("condition-and-or").nth(0).selectOption("or");
|
||||||
|
await page.getByTestId("condition-value").nth(1).fill("b.iana-servers.net");
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
|
||||||
|
await page.getByTestId("save-button").click();
|
||||||
|
await page.waitForURL("/dashboard/*"); // wait for the monitor to be created
|
||||||
|
await expect(page.getByTestId("monitor-status")).toHaveText("up", { ignoreCase: true });
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("failing condition", async ({ page }, testInfo) => {
|
||||||
|
await page.goto("./add");
|
||||||
|
await login(page);
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
|
||||||
|
const monitorTypeSelect = page.getByTestId("monitor-type-select");
|
||||||
|
await expect(monitorTypeSelect).toBeVisible();
|
||||||
|
|
||||||
|
await monitorTypeSelect.selectOption("dns");
|
||||||
|
const selectedValue = await monitorTypeSelect.evaluate(select => select.value);
|
||||||
|
expect(selectedValue).toBe("dns");
|
||||||
|
|
||||||
|
const friendlyName = "Example DNS NS";
|
||||||
|
await page.getByTestId("friendly-name-input").fill(friendlyName);
|
||||||
|
await page.getByTestId("hostname-input").fill("example.com");
|
||||||
|
|
||||||
|
// Vue-Multiselect component
|
||||||
|
const resolveTypeSelect = page.getByTestId("resolve-type-select");
|
||||||
|
await resolveTypeSelect.click();
|
||||||
|
await resolveTypeSelect.getByRole("option", { name: "NS" }).click();
|
||||||
|
|
||||||
|
expect(await page.getByTestId("condition").count()).toEqual(1); // 1 added by default
|
||||||
|
await page.getByTestId("condition-value").nth(0).fill("definitely-not.net");
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
|
||||||
|
await page.getByTestId("save-button").click();
|
||||||
|
await page.waitForURL("/dashboard/*"); // wait for the monitor to be created
|
||||||
|
await expect(page.getByTestId("monitor-status")).toHaveText("down", { ignoreCase: true });
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
Loading…
Reference in new issue