From 1b87c0bdc709b49ebfe1b316db95ea5ade6d2a16 Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Mon, 17 Apr 2023 15:07:02 -0700 Subject: [PATCH] Implemented support for >, >=, <, and <= query operators for the doc level monitor visual editor. (#508) (#528) * Implemented support for >, >=, <, and <= query operators for the doc level monitor visual editor. Signed-off-by: AWSHurneyt * Fixed a bug with doc level trigger creation. See issue 448 for more details. Signed-off-by: AWSHurneyt * Revert "Fixed a bug with doc level trigger creation. See issue 448 for more details." This fix will be implemented in a separate PR. This reverts commit 3a1e8b1aa51b38bf9a94dfb5213b05c02bc11730. Signed-off-by: AWSHurneyt * Fixed a bug involving the parsing of query operators. Signed-off-by: AWSHurneyt * Implemented cypress tests to validate all support query operators. Signed-off-by: AWSHurneyt * Renamed function based on PR feedback. Signed-off-by: AWSHurneyt * Refactored validation method to be more succinct. Moved related unit tests to a more appropriate location. Signed-off-by: AWSHurneyt * Refactored validation method to check whether input value is NaN. Implemented related unit tests. Signed-off-by: AWSHurneyt --------- Signed-off-by: AWSHurneyt (cherry picked from commit 6089ad913bd74ac4c4734b625d4270acc6cd2657) Co-authored-by: AWSHurneyt --- .../document_level_monitor_spec.js | 195 ++++++++++++++---- .../DocumentLevelQuery.js | 106 +++++++--- .../utils/constants.js | 76 +++++++ .../utils/helpers.js | 43 ++++ .../utils/helpers.test.js | 124 +++++++++++ .../expressions/WhereExpression.test.js | 2 +- .../expressions/utils/constants.js | 98 +++++---- .../expressions/utils/dataTypes.js | 18 +- .../expressions/utils/whereHelpers.js | 16 +- .../expressions/utils/whereHelpers.test.js | 28 +-- .../CreateMonitor/utils/constants.js | 6 +- .../CreateMonitor/utils/formikToMonitor.js | 16 +- .../utils/formikToMonitor.test.js | 39 ++-- .../CreateMonitor/utils/monitorToFormik.js | 91 ++++++-- .../CreateMonitor/utils/whereFilters.js | 33 +-- .../containers/DefineMonitor/DefineMonitor.js | 2 +- .../FindingsDashboard/findingsUtils.js | 22 +- .../FindingsDashboard/findingsUtils.test.js | 137 +----------- public/utils/validate.js | 38 ++-- public/utils/validate.test.js | 35 ++++ 20 files changed, 775 insertions(+), 350 deletions(-) create mode 100644 public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/utils/constants.js create mode 100644 public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/utils/helpers.js create mode 100644 public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/utils/helpers.test.js diff --git a/cypress/integration/document_level_monitor_spec.js b/cypress/integration/document_level_monitor_spec.js index fb50875ed..9e82db039 100644 --- a/cypress/integration/document_level_monitor_spec.js +++ b/cypress/integration/document_level_monitor_spec.js @@ -14,17 +14,75 @@ const SAMPLE_EXTRACTION_QUERY_MONITOR = 'sample_extraction_query_document_level_ const SAMPLE_VISUAL_EDITOR_MONITOR = 'sample_visual_editor_document_level_monitor'; const SAMPLE_DOCUMENT_LEVEL_MONITOR = 'sample_document_level_monitor'; +const sampleDocument = { + message: 'This is an error from IAD region', + date: '2020-06-04T18:57:12', + region: 'us-west-2', + numberField: 100, +}; + +const queryOperators = [ + { + text: 'is', + value: 'is', + }, + { + text: 'is not', + value: 'is_not', + }, + { + text: 'is greater than', + value: 'is_greater', + }, + { + text: 'is greater than equal', + value: 'is_greater_equal', + }, + { + text: 'is less than', + value: 'is_less', + }, + { + text: 'is less than equal', + value: 'is_less_equal', + }, +]; + const addDocumentsToTestIndex = (indexName = '', numOfDocs = 0) => { for (let i = 0; i < numOfDocs; i++) { - const docBody = { - message: 'This is an error from IAD region', - date: '2020-06-04T18:57:12', - region: 'us-west-2', - }; - cy.insertDocumentToIndex(indexName, undefined, docBody); + cy.insertDocumentToIndex(indexName, undefined, sampleDocument); } }; +const addQuery = ({ queryIndex = 0, queryName, queryField, operator, query, tags = [] }) => { + // Add another query + if (queryIndex > 0) cy.get('[data-test-subj="addDocLevelQueryButton"]').click({ force: true }); + + // Enter query name + cy.get(`[data-test-subj="documentLevelQuery_queryName${queryIndex}"]`).type(queryName); + + // Enter query field + cy.get(`[data-test-subj="documentLevelQuery_field${queryIndex}"]`).type( + `${queryField}{downarrow}{enter}` + ); + + // Select query operator + cy.get(`[data-test-subj="documentLevelQuery_operator${queryIndex}"]`).select(operator); + + // Enter query + cy.get(`[data-test-subj="documentLevelQuery_query${queryIndex}"]`).type(query); + + // Enter tags + tags.forEach((tag, tagIndex) => { + cy.get(`[data-test-subj="addDocLevelQueryTagButton_query${queryIndex}"]`).click({ + force: true, + }); + cy.get( + `[data-test-subj="documentLevelQueryTag_text_field_query${queryIndex}_tag${tagIndex}"]` + ).type(tag); + }); +}; + describe('DocumentLevelMonitor', () => { before(() => { // Load sample data @@ -53,15 +111,15 @@ describe('DocumentLevelMonitor', () => { cy.contains('There are no existing monitors'); // Go to create monitor page - cy.contains('Create monitor').click(); + cy.contains('Create monitor').click({ force: true }); // Select the Document-Level Monitor type - cy.get('[data-test-subj="docLevelMonitorRadioCard"]').click(); + cy.get('[data-test-subj="docLevelMonitorRadioCard"]').click({ force: true }); }); it('by extraction query editor', () => { // Select extraction query for method of definition - cy.get('[data-test-subj="extractionQueryEditorRadioCard"]').click(); + cy.get('[data-test-subj="extractionQueryEditorRadioCard"]').click({ force: true }); // Wait for input to load and then type in the monitor name cy.get('input[name="name"]').type(SAMPLE_EXTRACTION_QUERY_MONITOR); @@ -113,7 +171,7 @@ describe('DocumentLevelMonitor', () => { // TODO: Test with Notifications plugin // Click the create button - cy.get('button').contains('Create').click(); + cy.get('button').contains('Create').click({ force: true }); // Confirm we can see only one row in the trigger list by checking element cy.contains('This table contains 1 row'); @@ -122,7 +180,7 @@ describe('DocumentLevelMonitor', () => { cy.contains(sampleDocumentLevelMonitor.triggers[0].document_level_trigger.name); // Go back to the Monitors list - cy.get('a').contains('Monitors').click(); + cy.get('a').contains('Monitors').click({ force: true }); // Confirm we can see the created monitor in the list cy.contains(SAMPLE_EXTRACTION_QUERY_MONITOR); @@ -130,7 +188,7 @@ describe('DocumentLevelMonitor', () => { it('by visual editor', () => { // Select visual editor for method of definition - cy.get('[data-test-subj="visualEditorRadioCard"]').click(); + cy.get('[data-test-subj="visualEditorRadioCard"]').click({ force: true }); // Wait for input to load and then type in the monitor name cy.get('input[name="name"]').type(SAMPLE_VISUAL_EDITOR_MONITOR); @@ -138,25 +196,35 @@ describe('DocumentLevelMonitor', () => { // Wait for input to load and then type in the index name cy.get('#index').type(`${TESTING_INDEX}{enter}`, { force: true }); - // Enter query name - cy.get('[data-test-subj="documentLevelQuery_queryName0"]').type( - sampleDocumentLevelMonitor.inputs[0].doc_level_input.queries[0].name - ); - - // Enter query field - cy.get('[data-test-subj="documentLevelQuery_field0"]').type('region{downarrow}{enter}'); - - // Enter query operator - cy.get('[data-test-subj="documentLevelQuery_operator0"]').type('is{enter}'); - - // Enter query - cy.get('[data-test-subj="documentLevelQuery_query0"]').type('us-west-2'); - - // Enter query tags - cy.get('[data-test-subj="addDocLevelQueryTagButton_query0"]').click().click(); - cy.get('[data-test-subj="documentLevelQueryTag_text_field_query0_tag0"]').type( - sampleDocumentLevelMonitor.inputs[0].doc_level_input.queries[0].tags[0] - ); + const testQueries = [ + { + queryName: sampleDocumentLevelMonitor.inputs[0].doc_level_input.queries[0].name, + queryField: 'region', + operator: 'is', + operatorValue: 'is', + query: 'us-west-2', + tags: [sampleDocumentLevelMonitor.inputs[0].doc_level_input.queries[0].tags[0]], + }, + ]; + + // Enter first query + addQuery(testQueries[0]); + + // Create queries for each supported query operator + queryOperators.forEach((operator, index) => { + // Incrementing the query index by 1 to account for the query created above. + const queryIndex = index + 1; + const newQuery = { + queryIndex: queryIndex, + queryName: `Query${queryIndex}-${operator.value}`, + queryField: 'numberField', + operator: operator.text, + operatorValue: operator.value, + query: 1000 + queryIndex, + }; + addQuery(newQuery); + testQueries.push(newQuery); + }); // Add a trigger cy.contains('Add trigger').click({ force: true }); @@ -174,7 +242,7 @@ describe('DocumentLevelMonitor', () => { ); // Add another condition - cy.get('[data-test-subj="addTriggerConditionButton"]').click().click(); + cy.get('[data-test-subj="addTriggerConditionButton"]').click({ force: true }); // Define a second condition cy.get( @@ -190,7 +258,7 @@ describe('DocumentLevelMonitor', () => { // TODO: Test with Notifications plugin // Click the create button - cy.get('button').contains('Create').click(); + cy.get('button').contains('Create').click({ force: true }); // Confirm we can see only one row in the trigger list by checking element cy.contains('This table contains 1 row'); @@ -198,8 +266,55 @@ describe('DocumentLevelMonitor', () => { // Confirm we can see the new trigger cy.contains(sampleDocumentLevelMonitor.triggers[0].document_level_trigger.name); + // Click the 'Edit' button to confirm the monitor has the expected configuration + cy.contains('Edit').click({ force: true }); + + // Confirm each query has been configured correctly + testQueries.forEach((query, index) => { + // Confirm query name + cy.get(`[data-test-subj="documentLevelQuery_queryName${index}"]`).should( + 'have.value', + query.queryName + ); + + // Confirm query field + cy.get(`[data-test-subj="documentLevelQuery_field${index}"]`).contains(query.queryField); + + // Confirm query operator + cy.get(`[data-test-subj="documentLevelQuery_operator${index}"]`).should( + 'have.value', + query.operatorValue + ); + + // Confirm query + cy.get(`[data-test-subj="documentLevelQuery_query${index}"]`).should( + 'have.value', + query.query.toString() + ); + + // Confirm tags + query.tags?.forEach((tag, tagIndex) => { + cy.get( + `[data-test-subj="documentLevelQueryTag_badge_query${index}_tag${tagIndex}"]` + ).contains(tag); + }); + }); + + // Confirm the first trigger condition has been configured correctly + cy.get( + '[data-test-subj="documentLevelTriggerExpression_query_triggerDefinitions[0].triggerConditions.0"]' + ).contains(sampleDocumentLevelMonitor.inputs[0].doc_level_input.queries[0].tags[0]); + + // Confirm the second trigger condition has been configured correctly + cy.get( + '[data-test-subj="documentLevelTriggerExpression_andOr_triggerDefinitions[0].triggerConditions.1"]' + ).contains('OR'); + cy.get( + '[data-test-subj="documentLevelTriggerExpression_query_triggerDefinitions[0].triggerConditions.1"]' + ).contains(sampleDocumentLevelMonitor.inputs[0].doc_level_input.queries[0].name); + // Go back to the Monitors list - cy.get('a').contains('Monitors').click(); + cy.get('a').contains('Monitors').click({ force: true }); // Confirm we can see the created monitor in the list cy.contains(SAMPLE_VISUAL_EDITOR_MONITOR); @@ -235,7 +350,7 @@ describe('DocumentLevelMonitor', () => { cy.contains('Add another trigger').click({ force: true }); // Expand the accordion - cy.contains('New trigger').click(); + cy.contains('New trigger').click({ force: true }); // Type in the trigger name const newTriggerName = 'new-extraction-query-trigger'; @@ -266,7 +381,7 @@ describe('DocumentLevelMonitor', () => { // TODO: Test with Notifications plugin // Click the update button - cy.get('button').contains('Update').last().click(); + cy.get('button').contains('Update').last().click({ force: true }); // Confirm we can see only one row in the trigger list by checking element cy.contains('This table contains 2 rows'); @@ -308,7 +423,7 @@ describe('DocumentLevelMonitor', () => { cy.get('[data-test-subj="documentLevelQuery_query3"]').type('Unknown message'); // Enter query tags - cy.get('[data-test-subj="addDocLevelQueryTagButton_query3"]').click().click(); + cy.get('[data-test-subj="addDocLevelQueryTagButton_query3"]').click({ force: true }); cy.get('[data-test-subj="documentLevelQueryTag_text_field_query3_tag0"]').type('sev1'); // Remove existing trigger @@ -318,7 +433,7 @@ describe('DocumentLevelMonitor', () => { cy.contains('Add another trigger').click({ force: true }); // Expand the accordion - cy.contains('New trigger').click(); + cy.contains('New trigger').click({ force: true }); // Type in the trigger name const newTriggerName = 'new-visual-editor-trigger'; @@ -332,7 +447,7 @@ describe('DocumentLevelMonitor', () => { // TODO: Test with Notifications plugin // Click the create button - cy.get('button').contains('Update').last().click(); + cy.get('button').contains('Update').last().click({ force: true }); // Confirm we can see only one row in the trigger list by checking element cy.contains('This table contains 1 row'); @@ -375,7 +490,7 @@ describe('DocumentLevelMonitor', () => { cy.get('[data-test-subj="indicesComboBox"]').contains(TESTING_INDEX_B, { timeout: 20000 }); // Click the update button - cy.get('button').contains('Update').last().click(); + cy.get('button').contains('Update').last().click({ force: true }); // Confirm we're on the Monitor Details page by searching for the History element cy.contains('History', { timeout: 20000 }); diff --git a/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/DocumentLevelQuery.js b/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/DocumentLevelQuery.js index 8fa288e60..a3a31bd16 100644 --- a/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/DocumentLevelQuery.js +++ b/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/DocumentLevelQuery.js @@ -6,16 +6,24 @@ import React, { Component } from 'react'; import { connect } from 'formik'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; -import { FormikComboBox, FormikFieldText, FormikSelect } from '../../../../components/FormControls'; +import { + FormikComboBox, + FormikFieldNumber, + FormikFieldText, + FormikSelect, +} from '../../../../components/FormControls'; import { hasError, isInvalid, required, + requiredNumber, validateIllegalCharacters, } from '../../../../utils/validate'; import ConfigureDocumentLevelQueryTags from './ConfigureDocumentLevelQueryTags'; -import { getIndexFields } from '../MonitorExpressions/expressions/utils/dataTypes'; -import { QUERY_OPERATORS } from '../../../Dashboard/components/FindingsDashboard/findingsUtils'; +import { getIndexFields, getTypeForField } from '../MonitorExpressions/expressions/utils/dataTypes'; +import { DATA_TYPES } from '../../../../utils/constants'; +import { getOperators } from '../MonitorExpressions/expressions/utils/whereHelpers'; +import { getDocLevelQueryOperators } from './utils/helpers'; const ALLOWED_DATA_TYPES = ['number', 'text', 'keyword', 'boolean']; @@ -25,11 +33,31 @@ export const ILLEGAL_QUERY_NAME_CHARACTERS = [' ']; class DocumentLevelQuery extends Component { constructor(props) { super(props); - this.state = {}; + this.state = { + fieldDataType: DATA_TYPES.TEXT, + indexFieldOptions: [], + supportedOperators: getDocLevelQueryOperators(), + }; + } + + componentDidMount() { + this.initializeFieldDataType(); + } + + componentDidUpdate(prevProps) { + if (prevProps.dataTypes !== this.props.dataTypes) this.initializeFieldDataType(); + } + + initializeFieldDataType() { + const { dataTypes, query } = this.props; + const indexFieldOptions = getIndexFields(dataTypes, ALLOWED_DATA_TYPES); + const fieldDataType = getTypeForField(query.field, indexFieldOptions); + this.setState({ fieldDataType, indexFieldOptions }); } render() { - const { dataTypes, formFieldName = '', query, queryIndex, queriesArrayHelpers } = this.props; + const { formFieldName = '', query, queryIndex, queriesArrayHelpers } = this.props; + const { fieldDataType, indexFieldOptions, supportedOperators } = this.state; return (
@@ -83,8 +111,11 @@ class DocumentLevelQuery extends Component { }} inputProps={{ placeholder: 'Enter the field to query', - options: getIndexFields(dataTypes, ALLOWED_DATA_TYPES), - onChange: (e, field, form) => form.setFieldValue(field.name, e[0].label), + options: indexFieldOptions, + onChange: (e, field, form) => { + this.setState({ fieldDataType: e[0].type }); + form.setFieldValue(field.name, e[0].label); + }, onBlur: (e, field, form) => form.setFieldTouched(field.name, true), singleSelection: { asPlainText: true }, isClearable: false, @@ -97,33 +128,56 @@ class DocumentLevelQuery extends Component { field.onChange(e), - options: QUERY_OPERATORS, + options: getOperators(fieldDataType, supportedOperators), 'data-test-subj': `documentLevelQuery_operator${queryIndex}`, }} /> - + {fieldDataType === DATA_TYPES.NUMBER ? ( + + ) : ( + + )} diff --git a/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/utils/constants.js b/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/utils/constants.js new file mode 100644 index 000000000..497d882eb --- /dev/null +++ b/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/utils/constants.js @@ -0,0 +1,76 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { OPERATORS_MAP } from '../../MonitorExpressions/expressions/utils/constants'; + +export const DOC_LEVEL_INPUT_FIELD = 'doc_level_input'; + +/** + * A list of the operators currently supported for defining queries through the UI. + */ +export const SUPPORTED_DOC_LEVEL_QUERY_OPERATORS = [ + OPERATORS_MAP.IS.value, + OPERATORS_MAP.IS_NOT.value, + OPERATORS_MAP.IS_GREATER.value, + OPERATORS_MAP.IS_GREATER_EQUAL.value, + OPERATORS_MAP.IS_LESS.value, + OPERATORS_MAP.IS_LESS_EQUAL.value, +]; + +/** + * Initial implementation of document level monitors used these feature-specific operator values. + * Refactored document level monitors assets to reuse the OPERATORS_MAP to reduce duplicate code + * as new query types become supported. + */ +export const LEGACY_QUERY_OPERATORS = { + IS: { text: 'is', value: '==' }, + IS_NOT: { text: 'is not', value: '!=' }, +}; + +/** + * These patterns delineate the field being queries from the query value + * when using query string query syntax. + */ +export const QUERY_STRING_QUERY_OPERATORS = { + [OPERATORS_MAP.IS_GREATER.value]: ':>', + [OPERATORS_MAP.IS_GREATER_EQUAL.value]: ':>=', + [OPERATORS_MAP.IS_LESS.value]: ':<', + [OPERATORS_MAP.IS_LESS_EQUAL.value]: ':<=', +}; + +/** + * Similar to OPERATORS_QUERY_MAP, this const contains the query patterns + * for the SUPPORTED_DOC_LEVEL_QUERY_OPERATORS. + */ +export const DOC_LEVEL_QUERY_MAP = { + [LEGACY_QUERY_OPERATORS.IS.value]: { + query: ({ field, query }) => `${field}:\"${query}\"`, + }, + [LEGACY_QUERY_OPERATORS.IS_NOT.value]: { + query: ({ field, query }) => `NOT (${field}:\"${query}\")`, + }, + [OPERATORS_MAP.IS.value]: { + query: ({ field, query }) => `${field}:\"${query}\"`, + }, + [OPERATORS_MAP.IS_NOT.value]: { + query: ({ field, query }) => `NOT (${field}:\"${query}\")`, + }, + [OPERATORS_MAP.IS_GREATER.value]: { + query: ({ field, query }) => + `${field}${QUERY_STRING_QUERY_OPERATORS[OPERATORS_MAP.IS_GREATER.value]}${query}`, + }, + [OPERATORS_MAP.IS_GREATER_EQUAL.value]: { + query: ({ field, query }) => + `${field}${QUERY_STRING_QUERY_OPERATORS[OPERATORS_MAP.IS_GREATER_EQUAL.value]}${query}`, + }, + [OPERATORS_MAP.IS_LESS.value]: { + query: ({ field, query }) => + `${field}${QUERY_STRING_QUERY_OPERATORS[OPERATORS_MAP.IS_LESS.value]}${query}`, + }, + [OPERATORS_MAP.IS_LESS_EQUAL.value]: { + query: ({ field, query }) => + `${field}${QUERY_STRING_QUERY_OPERATORS[OPERATORS_MAP.IS_LESS_EQUAL.value]}${query}`, + }, +}; diff --git a/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/utils/helpers.js b/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/utils/helpers.js new file mode 100644 index 000000000..ff1dc655e --- /dev/null +++ b/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/utils/helpers.js @@ -0,0 +1,43 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import _ from 'lodash'; +import { SUPPORTED_DOC_LEVEL_QUERY_OPERATORS } from './constants'; +import { OPERATORS_MAP } from '../../MonitorExpressions/expressions/utils/constants'; +import { FORMIK_INITIAL_DOCUMENT_LEVEL_QUERY_VALUES } from '../../../containers/CreateMonitor/utils/constants'; + +/** + * Returns an array of { text: string, value: string, dataTypes: string[] } objects. + * Used by the DocumentLevelQuery.js to populate the operator options FormikSelect element. + * @return {*[]} + */ +export const getDocLevelQueryOperators = () => { + return SUPPORTED_DOC_LEVEL_QUERY_OPERATORS.map((type) => OPERATORS_MAP[_.toUpper(type)]); +}; + +/** + * Validates whether the document level monitor query can be executed. + * @param query - a FORMIK_INITIAL_DOCUMENT_LEVEL_QUERY_VALUES object. + * @return {boolean} - TRUE if the query is valid; otherwise FALSE. + */ +export const validateDocLevelGraphQuery = (query = FORMIK_INITIAL_DOCUMENT_LEVEL_QUERY_VALUES) => { + // The 'queryName', 'field', 'operator', and 'query' fields are required to execute a doc level query. + // If any of those fields are undefined for any queries, the monitor cannot be executed. + return ( + !_.isEmpty(query.queryName) && + !_.isEmpty(query.field) && + !_.isEmpty(query.operator) && + !_.isEmpty(query.query?.toString()) + ); +}; + +/** + * Validates whether the array of document level monitor queries can be executed. + * @param queries - an array of FORMIK_INITIAL_DOCUMENT_LEVEL_QUERY_VALUES objects. + * @return {boolean} - TRUE if there is at least 1 defined query, and all queries are valid; otherwise FALSE. + */ +export const validDocLevelGraphQueries = (queries = []) => { + return !_.isEmpty(queries) && !queries.some((query) => !validateDocLevelGraphQuery(query)); +}; diff --git a/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/utils/helpers.test.js b/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/utils/helpers.test.js new file mode 100644 index 000000000..06d0d99e2 --- /dev/null +++ b/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/utils/helpers.test.js @@ -0,0 +1,124 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { validDocLevelGraphQueries } from './helpers'; +import { SUPPORTED_DOC_LEVEL_QUERY_OPERATORS } from './constants'; + +describe('validDocLevelGraphQueries', () => { + test('when no queries are supplied', () => { + const queries = []; + expect(validDocLevelGraphQueries(queries)).toEqual(false); + }); + + test('when a query does not have a queryName value', () => { + const queries = [ + { + id: 'query1', + queryName: '', + field: 'field.name', + operator: SUPPORTED_DOC_LEVEL_QUERY_OPERATORS[0], + query: 'value1', + tags: ['tag1', 'tag2'], + }, + { + id: 'query2', + queryName: 'query2', + field: 'another.field.name', + operator: SUPPORTED_DOC_LEVEL_QUERY_OPERATORS[0], + query: 'value2', + tags: ['tag3'], + }, + ]; + expect(validDocLevelGraphQueries(queries)).toEqual(false); + }); + + test('when a query does not have a field value', () => { + const queries = [ + { + id: 'query1', + queryName: 'query2', + field: '', + operator: SUPPORTED_DOC_LEVEL_QUERY_OPERATORS[0], + query: 'value1', + tags: ['tag1', 'tag2'], + }, + { + id: 'query2', + queryName: 'query2', + field: 'another.field.name', + operator: SUPPORTED_DOC_LEVEL_QUERY_OPERATORS[0], + query: 'value2', + tags: ['tag3'], + }, + ]; + expect(validDocLevelGraphQueries(queries)).toEqual(false); + }); + + test('when a query does not have an operator value', () => { + const queries = [ + { + id: 'query1', + queryName: 'query2', + field: 'field.name', + operator: '', + query: 'value1', + tags: ['tag1', 'tag2'], + }, + { + id: 'query2', + queryName: 'query2', + field: 'another.field.name', + operator: SUPPORTED_DOC_LEVEL_QUERY_OPERATORS[0], + query: 'value2', + tags: ['tag3'], + }, + ]; + expect(validDocLevelGraphQueries(queries)).toEqual(false); + }); + + test('when a query does not have a query value', () => { + const queries = [ + { + id: 'query1', + queryName: 'query1', + field: 'field.name', + operator: SUPPORTED_DOC_LEVEL_QUERY_OPERATORS[0], + query: '', + tags: ['tag1', 'tag2'], + }, + { + id: 'query2', + queryName: 'query2', + field: 'another.field.name', + operator: SUPPORTED_DOC_LEVEL_QUERY_OPERATORS[0], + query: 'value2', + tags: ['tag3'], + }, + ]; + expect(validDocLevelGraphQueries(queries)).toEqual(false); + }); + + test('when all queries are defined', () => { + const queries = [ + { + id: 'query1', + queryName: 'query2', + field: 'field.name', + operator: SUPPORTED_DOC_LEVEL_QUERY_OPERATORS[0], + query: 'value1', + tags: ['tag1', 'tag2'], + }, + { + id: 'query2', + queryName: 'query2', + field: 'another.field.name', + operator: SUPPORTED_DOC_LEVEL_QUERY_OPERATORS[0], + query: 'value2', + tags: ['tag3'], + }, + ]; + expect(validDocLevelGraphQueries(queries)).toEqual(true); + }); +}); diff --git a/public/pages/CreateMonitor/components/MonitorExpressions/expressions/WhereExpression.test.js b/public/pages/CreateMonitor/components/MonitorExpressions/expressions/WhereExpression.test.js index caf0b1396..151c4dca3 100644 --- a/public/pages/CreateMonitor/components/MonitorExpressions/expressions/WhereExpression.test.js +++ b/public/pages/CreateMonitor/components/MonitorExpressions/expressions/WhereExpression.test.js @@ -74,7 +74,7 @@ describe('WhereExpression', () => { wrapper.update(); const values = wrapper.find(WhereExpression).props().formik.values; expect(values.where.fieldName).toEqual([{ label: 'cityName', type: 'text' }]); - expect(values.where.operator).toEqual(OPERATORS_MAP.IS); + expect(values.where.operator).toEqual(OPERATORS_MAP.IS.value); expect(wrapper.find(FormikFieldText).length).toBe(1); expect(wrapper.find(FormikFieldNumber).length).toBe(0); }); diff --git a/public/pages/CreateMonitor/components/MonitorExpressions/expressions/utils/constants.js b/public/pages/CreateMonitor/components/MonitorExpressions/expressions/utils/constants.js index fd85c9720..5d0645bb0 100644 --- a/public/pages/CreateMonitor/components/MonitorExpressions/expressions/utils/constants.js +++ b/public/pages/CreateMonitor/components/MonitorExpressions/expressions/utils/constants.js @@ -38,50 +38,78 @@ export const WHERE_BOOLEAN_FILTERS = [ ]; export const OPERATORS_MAP = { - IS: 'is', - IS_NOT: 'is_not', - IS_NULL: 'is_null', - IS_NOT_NULL: 'is_not_null', - IS_GREATER: 'is_greater', - IS_GREATER_EQUAL: 'is_greater_equal', - IS_LESS: 'is_less', - IS_LESS_EQUAL: 'is_less_equal', - STARTS_WITH: 'starts_with', - ENDS_WITH: 'ends_with', - CONTAINS: 'contains', - NOT_CONTAINS: 'does_not_contains', - IN_RANGE: 'in_range', - NOT_IN_RANGE: 'not_in_range', -}; - -export const COMPARISON_OPERATORS = [ - { text: 'is', value: OPERATORS_MAP.IS, dataTypes: ['number', 'text', 'keyword', 'boolean'] }, - { + IS: { + text: 'is', + value: 'is', + dataTypes: ['number', 'text', 'keyword', 'boolean'], + }, + IS_NOT: { text: 'is not', - value: OPERATORS_MAP.IS_NOT, + value: 'is_not', dataTypes: ['number', 'text', 'keyword', 'boolean'], }, - { + IS_NULL: { text: 'is null', - value: OPERATORS_MAP.IS_NULL, + value: 'is_null', dataTypes: ['number', 'text', 'keyword', 'boolean'], }, - { + IS_NOT_NULL: { text: 'is not null', - value: OPERATORS_MAP.IS_NOT_NULL, + value: 'is_not_null', dataTypes: ['number', 'text', 'keyword'], }, - { text: 'is greater than', value: OPERATORS_MAP.IS_GREATER, dataTypes: ['number'] }, - { text: 'is greater than equal', value: OPERATORS_MAP.IS_GREATER_EQUAL, dataTypes: ['number'] }, - { text: 'is less than', value: OPERATORS_MAP.IS_LESS, dataTypes: ['number'] }, - { text: 'is less than equal', value: OPERATORS_MAP.IS_LESS_EQUAL, dataTypes: ['number'] }, - { text: 'is in range', value: OPERATORS_MAP.IN_RANGE, dataTypes: ['number'] }, - { text: 'is not in range', value: OPERATORS_MAP.NOT_IN_RANGE, dataTypes: ['number'] }, - { text: 'starts with', value: OPERATORS_MAP.STARTS_WITH, dataTypes: ['text', 'keyword'] }, - { text: 'ends with', value: OPERATORS_MAP.ENDS_WITH, dataTypes: ['text', 'keyword'] }, - { text: 'contains', value: OPERATORS_MAP.CONTAINS, dataTypes: ['text', 'keyword'] }, - { text: 'does not contains', value: OPERATORS_MAP.NOT_CONTAINS, dataTypes: ['text'] }, -]; + IS_GREATER: { + text: 'is greater than', + value: 'is_greater', + dataTypes: ['number'], + }, + IS_GREATER_EQUAL: { + text: 'is greater than equal', + value: 'is_greater_equal', + dataTypes: ['number'], + }, + IS_LESS: { + text: 'is less than', + value: 'is_less', + dataTypes: ['number'], + }, + IS_LESS_EQUAL: { + text: 'is less than equal', + value: 'is_less_equal', + dataTypes: ['number'], + }, + STARTS_WITH: { + text: 'starts with', + value: 'starts_with', + dataTypes: ['text', 'keyword'], + }, + ENDS_WITH: { + text: 'ends with', + value: 'ends_with', + dataTypes: ['text', 'keyword'], + }, + CONTAINS: { + text: 'contains', + value: 'contains', + dataTypes: ['text', 'keyword'], + }, + DOES_NOT_CONTAINS: { + text: 'does not contain', + value: 'does_not_contains', + dataTypes: ['text'], + }, + IN_RANGE: { + text: 'is in range', + value: 'in_range', + dataTypes: ['number'], + }, + NOT_IN_RANGE: { + text: 'is not in range', + value: 'not_in_range', + dataTypes: ['number'], + }, +}; + export const OVER_TYPES = [{ value: 'all documents', text: 'all documents' }]; export const AGGREGATION_TYPES = [ diff --git a/public/pages/CreateMonitor/components/MonitorExpressions/expressions/utils/dataTypes.js b/public/pages/CreateMonitor/components/MonitorExpressions/expressions/utils/dataTypes.js index 9488dd7f1..13ef4c0ac 100644 --- a/public/pages/CreateMonitor/components/MonitorExpressions/expressions/utils/dataTypes.js +++ b/public/pages/CreateMonitor/components/MonitorExpressions/expressions/utils/dataTypes.js @@ -3,10 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ +import _ from 'lodash'; import { NUMBER_TYPES } from './constants'; +import { DATA_TYPES } from '../../../../../../utils/constants'; export function getFieldsForType(dataTypes, type) { - if (type === 'number') { + if (type === DATA_TYPES.NUMBER) { return NUMBER_TYPES.reduce( (options, type) => options.concat(getFieldsForType(dataTypes, type)), [] @@ -31,3 +33,17 @@ export const getFilteredIndexFields = (dataTypes, allowedTypes, fieldsToInclude) .filter((field) => fieldsToInclude.includes(field)) .map((field) => ({ label: field, type })), })); + +/** + * Searches the output from 'getIndexFields' for the provided field to determine the field's type. + * @param queryField - a string representing the field being queried. + * @param indexFields - the array output from 'getIndexFields'. + * @return {*|string} - a string representing the data type of the queryField. + */ +export function getTypeForField(queryField = '', indexFields = []) { + if (_.isEmpty(queryField) || _.isEmpty(indexFields)) return DATA_TYPES.TEXT; + const match = indexFields.find( + (entry) => entry.options?.find((dataField) => dataField.label === queryField) !== undefined + ); + return match?.label || DATA_TYPES.TEXT; +} diff --git a/public/pages/CreateMonitor/components/MonitorExpressions/expressions/utils/whereHelpers.js b/public/pages/CreateMonitor/components/MonitorExpressions/expressions/utils/whereHelpers.js index b6101ecbb..e6c3845cd 100644 --- a/public/pages/CreateMonitor/components/MonitorExpressions/expressions/utils/whereHelpers.js +++ b/public/pages/CreateMonitor/components/MonitorExpressions/expressions/utils/whereHelpers.js @@ -4,13 +4,17 @@ */ import _ from 'lodash'; -import { COMPARISON_OPERATORS, OPERATORS_MAP } from './constants'; +import { OPERATORS_MAP } from './constants'; import { TRIGGER_COMPARISON_OPERATORS } from '../../../../../CreateTrigger/containers/DefineBucketLevelTrigger/DefineBucketLevelTrigger'; +import { DATA_TYPES } from '../../../../../../utils/constants'; export const DEFAULT_WHERE_EXPRESSION_TEXT = 'All fields are included'; -export const getOperators = (fieldType) => - COMPARISON_OPERATORS.reduce( +export const getOperators = ( + fieldType = DATA_TYPES.TEXT, + supportedOperators = Object.values(OPERATORS_MAP) +) => + supportedOperators.reduce( (acc, currentOperator) => currentOperator.dataTypes.includes(fieldType) ? [...acc, { text: currentOperator.text, value: currentOperator.value }] @@ -19,12 +23,12 @@ export const getOperators = (fieldType) => ); export const isRangeOperator = (selectedOperator) => - [OPERATORS_MAP.IN_RANGE, OPERATORS_MAP.NOT_IN_RANGE].includes(selectedOperator); + [OPERATORS_MAP.IN_RANGE.value, OPERATORS_MAP.NOT_IN_RANGE.value].includes(selectedOperator); export const isNullOperator = (selectedOperator) => - [OPERATORS_MAP.IS_NULL, OPERATORS_MAP.IS_NOT_NULL].includes(selectedOperator); + [OPERATORS_MAP.IS_NULL.value, OPERATORS_MAP.IS_NOT_NULL.value].includes(selectedOperator); export const displayText = (whereValues) => { - const comparisonOperators = _.concat(COMPARISON_OPERATORS, TRIGGER_COMPARISON_OPERATORS); + const comparisonOperators = _.concat(Object.values(OPERATORS_MAP), TRIGGER_COMPARISON_OPERATORS); const whereFieldName = _.get(whereValues, 'fieldName[0].label', undefined); if (!whereFieldName) { diff --git a/public/pages/CreateMonitor/components/MonitorExpressions/expressions/utils/whereHelpers.test.js b/public/pages/CreateMonitor/components/MonitorExpressions/expressions/utils/whereHelpers.test.js index b1dd1c7bd..3de2816d1 100644 --- a/public/pages/CreateMonitor/components/MonitorExpressions/expressions/utils/whereHelpers.test.js +++ b/public/pages/CreateMonitor/components/MonitorExpressions/expressions/utils/whereHelpers.test.js @@ -89,7 +89,7 @@ describe('whereHelpers', () => { value: 'contains', }, { - text: 'does not contains', + text: 'does not contain', value: 'does_not_contains', }, ]); @@ -145,27 +145,27 @@ describe('whereHelpers', () => { }); describe('isRangeOperator', () => { test('should return true for IN_RANGE operator', () => { - expect(isRangeOperator(OPERATORS_MAP.IN_RANGE)).toBe(true); + expect(isRangeOperator(OPERATORS_MAP.IN_RANGE.value)).toBe(true); }); test('should return true for NOT_IN_RANGE operator', () => { - expect(isRangeOperator(OPERATORS_MAP.NOT_IN_RANGE)).toBe(true); + expect(isRangeOperator(OPERATORS_MAP.NOT_IN_RANGE.value)).toBe(true); }); test('should return false for any other operators', () => { - expect(isRangeOperator(OPERATORS_MAP.IS)).toBe(false); - expect(isRangeOperator(OPERATORS_MAP.IS_GREATER_EQUAL)).toBe(false); + expect(isRangeOperator(OPERATORS_MAP.IS.value)).toBe(false); + expect(isRangeOperator(OPERATORS_MAP.IS_GREATER_EQUAL.value)).toBe(false); }); }); describe('isNullOperator', () => { test('should return true for IS_NULL operator', () => { - expect(isNullOperator(OPERATORS_MAP.IS_NULL)).toBe(true); + expect(isNullOperator(OPERATORS_MAP.IS_NULL.value)).toBe(true); }); test('should return true for IS_NOT_NULL operator', () => { - expect(isNullOperator(OPERATORS_MAP.IS_NOT_NULL)).toBe(true); + expect(isNullOperator(OPERATORS_MAP.IS_NOT_NULL.value)).toBe(true); }); test('should return false for any other operators', () => { - expect(isNullOperator(OPERATORS_MAP.IS)).toBe(false); - expect(isNullOperator(OPERATORS_MAP.IS_GREATER_EQUAL)).toBe(false); + expect(isNullOperator(OPERATORS_MAP.IS.value)).toBe(false); + expect(isNullOperator(OPERATORS_MAP.IS_GREATER_EQUAL.value)).toBe(false); }); }); @@ -194,7 +194,7 @@ describe('whereHelpers', () => { expect( displayText({ fieldName: [{ label: 'age', type: 'number' }], - operator: OPERATORS_MAP.IN_RANGE, + operator: OPERATORS_MAP.IN_RANGE.value, fieldRangeStart: 20, fieldRangeEnd: 40, }) @@ -204,7 +204,7 @@ describe('whereHelpers', () => { expect( displayText({ fieldName: [{ label: 'age', type: 'number' }], - operator: OPERATORS_MAP.NOT_IN_RANGE, + operator: OPERATORS_MAP.NOT_IN_RANGE.value, fieldRangeStart: 20, fieldRangeEnd: 40, }) @@ -214,7 +214,7 @@ describe('whereHelpers', () => { expect( displayText({ fieldName: [{ label: 'age', type: 'number' }], - operator: OPERATORS_MAP.IS_NULL, + operator: OPERATORS_MAP.IS_NULL.value, }) ).toBe('age is null'); }); @@ -222,7 +222,7 @@ describe('whereHelpers', () => { expect( displayText({ fieldName: [{ label: 'age', type: 'number' }], - operator: OPERATORS_MAP.IS_NOT_NULL, + operator: OPERATORS_MAP.IS_NOT_NULL.value, }) ).toBe('age is not null'); }); @@ -230,7 +230,7 @@ describe('whereHelpers', () => { expect( displayText({ fieldName: [{ label: 'age', type: 'number' }], - operator: OPERATORS_MAP.IS_GREATER, + operator: OPERATORS_MAP.IS_GREATER.value, fieldValue: 20, }) ).toBe('age is greater than 20'); diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js index acd51d282..49ff0f29a 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js @@ -5,7 +5,7 @@ import { OPERATORS_MAP } from '../../../components/MonitorExpressions/expressions/utils/constants'; import { MONITOR_TYPE } from '../../../../../utils/constants'; -import { QUERY_OPERATORS } from '../../../../Dashboard/components/FindingsDashboard/findingsUtils'; +import { SUPPORTED_DOC_LEVEL_QUERY_OPERATORS } from '../../../components/DocumentLevelMonitorQueries/utils/constants'; export const BUCKET_COUNT = 5; @@ -49,7 +49,7 @@ export const FORMIK_INITIAL_VALUES = { bucketUnitOfTime: 'h', // m = minute, h = hour, d = day where: { fieldName: [], - operator: OPERATORS_MAP.IS, + operator: OPERATORS_MAP.IS.value, fieldValue: '', fieldRangeStart: 0, fieldRangeEnd: 0, @@ -66,7 +66,7 @@ export const FORMIK_INITIAL_DOCUMENT_LEVEL_QUERY_VALUES = { id: undefined, queryName: '', field: '', - operator: QUERY_OPERATORS[0].value, + operator: SUPPORTED_DOC_LEVEL_QUERY_OPERATORS[0], query: '', tags: [], }; diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js index a610d9abb..90401a598 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js @@ -13,6 +13,10 @@ import { getApiPath, getApiType, } from '../../../components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers'; +import { + DOC_LEVEL_INPUT_FIELD, + DOC_LEVEL_QUERY_MAP, +} from '../../../components/DocumentLevelMonitorQueries/utils/constants'; export function formikToMonitor(values) { const uiSchedule = formikToUiSchedule(values); @@ -22,7 +26,7 @@ export function formikToMonitor(values) { switch (values.monitor_type) { case MONITOR_TYPE.DOC_LEVEL: return { - doc_level_input: formikToDocLevelQueriesUiMetadata(values), + [DOC_LEVEL_INPUT_FIELD]: formikToDocLevelQueriesUiMetadata(values), search: { searchType: values.searchType }, }; default: @@ -228,13 +232,9 @@ export function formikToDocLevelInput(values) { case SEARCH_TYPE.GRAPH: description = values.description; queries = queries.map((query) => { - const formikToQuery = - query.operator === '==' - ? `${query.field}:\"${query.query}\"` - : `NOT (${query.field}:\"${query.query}\")`; + const formikToQuery = DOC_LEVEL_QUERY_MAP[query.operator].query(query); return { - // id: query.id, // TODO FIXME: Refactor to this assignment logic once backend generates its own ID value - id: query.queryName, + id: query.id, name: query.queryName, query: formikToQuery, tags: query.tags, @@ -261,7 +261,7 @@ export function formikToDocLevelInput(values) { } return { - doc_level_input: { + [DOC_LEVEL_INPUT_FIELD]: { description: description, indices: indices, queries: queries, diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.test.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.test.js index 4edd969cc..e87dc87c2 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.test.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.test.js @@ -83,7 +83,7 @@ describe('formikToUiSearch', () => { test('can build ui search with term where field', () => { formikValues.where = { fieldName: [{ label: 'age', type: 'number' }], - operator: OPERATORS_MAP.IS_GREATER_EQUAL, + operator: OPERATORS_MAP.IS_GREATER_EQUAL.value, fieldValue: 20, }; expect(formikToUiSearch(formikValues)).toMatchSnapshot(); @@ -92,7 +92,7 @@ describe('formikToUiSearch', () => { test('can build ui search with range where field', () => { formikValues.where = { fieldName: [{ label: 'age', type: 'number' }], - operator: OPERATORS_MAP.IN_RANGE, + operator: OPERATORS_MAP.IN_RANGE.value, fieldRangeStart: 20, fieldRangeEnd: 40, }; @@ -212,38 +212,43 @@ describe('formikToWhereClause', () => { const keywordField = [{ label: 'city.keyword', type: 'keyword' }]; test.each([ - [numericFieldName, OPERATORS_MAP.IS, 20, { term: { age: 20 } }], - [textField, OPERATORS_MAP.IS, 'Seattle', { match_phrase: { city: 'Seattle' } }], - [numericFieldName, OPERATORS_MAP.IS_NOT, 20, { bool: { must_not: { term: { age: 20 } } } }], + [numericFieldName, OPERATORS_MAP.IS.value, 20, { term: { age: 20 } }], + [textField, OPERATORS_MAP.IS.value, 'Seattle', { match_phrase: { city: 'Seattle' } }], + [ + numericFieldName, + OPERATORS_MAP.IS_NOT.value, + 20, + { bool: { must_not: { term: { age: 20 } } } }, + ], [ textField, - OPERATORS_MAP.IS_NOT, + OPERATORS_MAP.IS_NOT.value, 'Seattle', { bool: { must_not: { match_phrase: { city: 'Seattle' } } } }, ], [ numericFieldName, - OPERATORS_MAP.IS_NULL, + OPERATORS_MAP.IS_NULL.value, undefined, { bool: { must_not: { exists: { field: 'age' } } } }, ], - [numericFieldName, OPERATORS_MAP.IS_NOT_NULL, undefined, { exists: { field: 'age' } }], - [numericFieldName, OPERATORS_MAP.IS_GREATER, 20, { range: { age: { gt: 20 } } }], - [numericFieldName, OPERATORS_MAP.IS_GREATER_EQUAL, 20, { range: { age: { gte: 20 } } }], - [numericFieldName, OPERATORS_MAP.IS_LESS, 20, { range: { age: { lt: 20 } } }], - [numericFieldName, OPERATORS_MAP.IS_LESS_EQUAL, 20, { range: { age: { lte: 20 } } }], - [textField, OPERATORS_MAP.STARTS_WITH, 'Se', { prefix: { city: 'Se' } }], - [textField, OPERATORS_MAP.ENDS_WITH, 'Se', { wildcard: { city: '*Se' } }], + [numericFieldName, OPERATORS_MAP.IS_NOT_NULL.value, undefined, { exists: { field: 'age' } }], + [numericFieldName, OPERATORS_MAP.IS_GREATER.value, 20, { range: { age: { gt: 20 } } }], + [numericFieldName, OPERATORS_MAP.IS_GREATER_EQUAL.value, 20, { range: { age: { gte: 20 } } }], + [numericFieldName, OPERATORS_MAP.IS_LESS.value, 20, { range: { age: { lt: 20 } } }], + [numericFieldName, OPERATORS_MAP.IS_LESS_EQUAL.value, 20, { range: { age: { lte: 20 } } }], + [textField, OPERATORS_MAP.STARTS_WITH.value, 'Se', { prefix: { city: 'Se' } }], + [textField, OPERATORS_MAP.ENDS_WITH.value, 'Se', { wildcard: { city: '*Se' } }], [ textField, - OPERATORS_MAP.CONTAINS, + OPERATORS_MAP.CONTAINS.value, 'Se', { query_string: { query: `*Se*`, default_field: 'city' } }, ], - [keywordField, OPERATORS_MAP.CONTAINS, 'Se', { wildcard: { 'city.keyword': '*Se*' } }], + [keywordField, OPERATORS_MAP.CONTAINS.value, 'Se', { wildcard: { 'city.keyword': '*Se*' } }], [ textField, - OPERATORS_MAP.NOT_CONTAINS, + OPERATORS_MAP.DOES_NOT_CONTAINS.value, 'Se', { bool: { must_not: { query_string: { query: `*Se*`, default_field: 'city' } } } }, ], diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js index f76f3bab6..400a0bf7d 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js @@ -6,6 +6,11 @@ import _ from 'lodash'; import { FORMIK_INITIAL_DOCUMENT_LEVEL_QUERY_VALUES, FORMIK_INITIAL_VALUES } from './constants'; import { SEARCH_TYPE, INPUTS_DETECTOR_ID, MONITOR_TYPE } from '../../../../../utils/constants'; +import { OPERATORS_MAP } from '../../../components/MonitorExpressions/expressions/utils/constants'; +import { + DOC_LEVEL_INPUT_FIELD, + QUERY_STRING_QUERY_OPERATORS, +} from '../../../components/DocumentLevelMonitorQueries/utils/constants'; // Convert Monitor JSON to Formik values used in UI forms export default function monitorToFormik(monitor) { @@ -65,11 +70,15 @@ export default function monitorToFormik(monitor) { }; } +export function indicesToFormik(indices) { + return indices.map((index) => ({ label: index })); +} + export function docLevelInputToFormik(monitor) { - const input = monitor.inputs[0]['doc_level_input']; + const input = monitor.inputs[0][DOC_LEVEL_INPUT_FIELD]; const { description, indices, queries } = input; return { - description: description, // TODO DRAFT: DocLevelInput 'description' field isn't currently represented in the mocks. Remove it from frontend? + description: description, index: indicesToFormik(indices), query: JSON.stringify(_.omit(input, 'indices'), null, 4), queries: queriesToFormik(queries), @@ -78,27 +87,15 @@ export function docLevelInputToFormik(monitor) { export function queriesToFormik(queries) { return queries.map((query) => { - let querySource = ''; + let querySource; try { querySource = JSON.parse(query.query); } catch (e) { querySource = query.query; } - const parsedQuerySource = {}; - const usesIsNotOperator = _.startsWith(querySource, 'NOT (') && _.endsWith(querySource, ')'); - const operator = usesIsNotOperator ? '!=' : '=='; - - if (usesIsNotOperator) { - querySource = querySource.substring(5, querySource.length - 1); - querySource = _.split(querySource, ':'); - parsedQuerySource['field'] = _.trim(querySource[0], '"'); - parsedQuerySource['query'] = _.trim(querySource[1], '"'); - } else { - const splitQuery = _.split(querySource, '"'); - parsedQuerySource['field'] = _.trim(splitQuery[0], '":'); - parsedQuerySource['query'] = _.trim(splitQuery[1], '"'); - } + const operator = getQueryOperator(querySource); + const parsedQuerySource = parseQueryString(querySource, operator); return { ...FORMIK_INITIAL_DOCUMENT_LEVEL_QUERY_VALUES, @@ -111,6 +108,62 @@ export function queriesToFormik(queries) { }); } -export function indicesToFormik(indices) { - return indices.map((index) => ({ label: index })); +export function getQueryOperator(query = FORMIK_INITIAL_DOCUMENT_LEVEL_QUERY_VALUES.query) { + if (_.startsWith(query, 'NOT (') && _.endsWith(query, ')')) return OPERATORS_MAP.IS_NOT.value; + if (_.includes(query, QUERY_STRING_QUERY_OPERATORS[OPERATORS_MAP.IS_GREATER_EQUAL.value])) + return OPERATORS_MAP.IS_GREATER_EQUAL.value; + if (_.includes(query, QUERY_STRING_QUERY_OPERATORS[OPERATORS_MAP.IS_GREATER.value])) + return OPERATORS_MAP.IS_GREATER.value; + if (_.includes(query, QUERY_STRING_QUERY_OPERATORS[OPERATORS_MAP.IS_LESS_EQUAL.value])) + return OPERATORS_MAP.IS_LESS_EQUAL.value; + if (_.includes(query, QUERY_STRING_QUERY_OPERATORS[OPERATORS_MAP.IS_LESS.value])) + return OPERATORS_MAP.IS_LESS.value; + return OPERATORS_MAP.IS.value; +} + +export function parseQueryString( + query = FORMIK_INITIAL_DOCUMENT_LEVEL_QUERY_VALUES.query, + operator = OPERATORS_MAP.IS.value +) { + let field = FORMIK_INITIAL_DOCUMENT_LEVEL_QUERY_VALUES.field; + let parsedQuery = FORMIK_INITIAL_DOCUMENT_LEVEL_QUERY_VALUES.query; + switch (operator) { + case OPERATORS_MAP.IS.value: + parsedQuery = _.split(query, '"'); + field = _.trim(parsedQuery[0], '":'); + parsedQuery = _.trim(parsedQuery[1], '"'); + break; + case OPERATORS_MAP.IS_NOT.value: + parsedQuery = query.substring(5, query.length - 1); + parsedQuery = _.split(parsedQuery, ':'); + field = _.trim(parsedQuery[0], '"'); + parsedQuery = _.trim(parsedQuery[1], '"'); + break; + case OPERATORS_MAP.IS_GREATER.value: + parsedQuery = _.split(query, QUERY_STRING_QUERY_OPERATORS[OPERATORS_MAP.IS_GREATER.value]); + field = field = _.trim(parsedQuery[0], '"'); + parsedQuery = _.trim(parsedQuery[1], '"'); + break; + case OPERATORS_MAP.IS_GREATER_EQUAL.value: + parsedQuery = _.split( + query, + QUERY_STRING_QUERY_OPERATORS[OPERATORS_MAP.IS_GREATER_EQUAL.value] + ); + field = field = _.trim(parsedQuery[0], '"'); + parsedQuery = _.trim(parsedQuery[1], '"'); + break; + case OPERATORS_MAP.IS_LESS.value: + parsedQuery = _.split(query, QUERY_STRING_QUERY_OPERATORS[OPERATORS_MAP.IS_LESS.value]); + field = field = _.trim(parsedQuery[0], '"'); + parsedQuery = _.trim(parsedQuery[1], '"'); + break; + case OPERATORS_MAP.IS_LESS_EQUAL.value: + parsedQuery = _.split(query, QUERY_STRING_QUERY_OPERATORS[OPERATORS_MAP.IS_LESS_EQUAL.value]); + field = field = _.trim(parsedQuery[0], '"'); + parsedQuery = _.trim(parsedQuery[1], '"'); + break; + default: + console.log('Unknown query operator detected:', operator); + } + return { field: field, query: parsedQuery }; } diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/whereFilters.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/whereFilters.js index b12922bbe..144f83480 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/whereFilters.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/whereFilters.js @@ -6,15 +6,18 @@ import { OPERATORS_MAP } from '../../../components/MonitorExpressions/expressions/utils/constants'; import { DATA_TYPES } from '../../../../../utils/constants'; -//TODO:: Breakdown to factory pattern for rules in-case we support multiple filters. This is just ease for the single one +/** + * Breakdown to factory pattern for rules in-case we support multiple filters. + * This is just ease for the single one. + */ export const OPERATORS_QUERY_MAP = { - [OPERATORS_MAP.IS]: { + [OPERATORS_MAP.IS.value]: { query: ({ fieldName: [{ label, type }], fieldValue }) => type === DATA_TYPES.TEXT ? { match_phrase: { [label]: fieldValue } } : { term: { [label]: fieldValue } }, }, - [OPERATORS_MAP.IS_NOT]: { + [OPERATORS_MAP.IS_NOT.value]: { query: ({ fieldName: [{ label, type }], fieldValue }) => type === DATA_TYPES.TEXT ? { @@ -24,60 +27,60 @@ export const OPERATORS_QUERY_MAP = { bool: { must_not: { term: { [label]: fieldValue } } }, }, }, - [OPERATORS_MAP.IS_NULL]: { + [OPERATORS_MAP.IS_NULL.value]: { query: ({ fieldName: [{ label: fieldKey }] }) => ({ bool: { must_not: { exists: { field: fieldKey } } }, }), }, - [OPERATORS_MAP.IS_NOT_NULL]: { + [OPERATORS_MAP.IS_NOT_NULL.value]: { query: ({ fieldName: [{ label: fieldKey }] }) => ({ exists: { field: fieldKey } }), }, - [OPERATORS_MAP.IS_GREATER]: { + [OPERATORS_MAP.IS_GREATER.value]: { query: ({ fieldName: [{ label: fieldKey }], fieldValue }) => ({ range: { [fieldKey]: { gt: fieldValue } }, }), }, - [OPERATORS_MAP.IS_GREATER_EQUAL]: { + [OPERATORS_MAP.IS_GREATER_EQUAL.value]: { query: ({ fieldName: [{ label: fieldKey }], fieldValue }) => ({ range: { [fieldKey]: { gte: fieldValue } }, }), }, - [OPERATORS_MAP.IS_LESS]: { + [OPERATORS_MAP.IS_LESS.value]: { query: ({ fieldName: [{ label: fieldKey }], fieldValue }) => ({ range: { [fieldKey]: { lt: fieldValue } }, }), }, - [OPERATORS_MAP.IS_LESS_EQUAL]: { + [OPERATORS_MAP.IS_LESS_EQUAL.value]: { query: ({ fieldName: [{ label: fieldKey }], fieldValue }) => ({ range: { [fieldKey]: { lte: fieldValue } }, }), }, - [OPERATORS_MAP.IN_RANGE]: { + [OPERATORS_MAP.IN_RANGE.value]: { query: ({ fieldName: [{ label: fieldKey }], fieldRangeStart, fieldRangeEnd }) => ({ range: { [fieldKey]: { gte: fieldRangeStart, lte: fieldRangeEnd } }, }), }, - [OPERATORS_MAP.NOT_IN_RANGE]: { + [OPERATORS_MAP.NOT_IN_RANGE.value]: { query: ({ fieldName: [{ label: fieldKey }], fieldRangeStart, fieldRangeEnd }) => ({ bool: { must_not: { range: { [fieldKey]: { gte: fieldRangeStart, lte: fieldRangeEnd } } } }, }), }, - [OPERATORS_MAP.STARTS_WITH]: { + [OPERATORS_MAP.STARTS_WITH.value]: { query: ({ fieldName: [{ label: fieldKey }], fieldValue }) => ({ prefix: { [fieldKey]: fieldValue }, }), }, - [OPERATORS_MAP.ENDS_WITH]: { + [OPERATORS_MAP.ENDS_WITH.value]: { query: ({ fieldName: [{ label: fieldKey }], fieldValue }) => ({ wildcard: { [fieldKey]: `*${fieldValue}` }, }), }, - [OPERATORS_MAP.CONTAINS]: { + [OPERATORS_MAP.CONTAINS.value]: { query: ({ fieldName: [{ label, type }], fieldValue }) => type === DATA_TYPES.TEXT ? { @@ -87,7 +90,7 @@ export const OPERATORS_QUERY_MAP = { wildcard: { [label]: `*${fieldValue}*` }, }, }, - [OPERATORS_MAP.NOT_CONTAINS]: { + [OPERATORS_MAP.DOES_NOT_CONTAINS.value]: { query: ({ fieldName: [{ label, type }], fieldValue }) => type === DATA_TYPES.TEXT ? { diff --git a/public/pages/CreateMonitor/containers/DefineMonitor/DefineMonitor.js b/public/pages/CreateMonitor/containers/DefineMonitor/DefineMonitor.js index 3e60cd8b8..e34e39178 100644 --- a/public/pages/CreateMonitor/containers/DefineMonitor/DefineMonitor.js +++ b/public/pages/CreateMonitor/containers/DefineMonitor/DefineMonitor.js @@ -29,7 +29,7 @@ import { FORMIK_INITIAL_VALUES } from '../CreateMonitor/utils/constants'; import { API_TYPES } from '../../components/ClusterMetricsMonitor/utils/clusterMetricsMonitorConstants'; import ConfigureDocumentLevelQueries from '../../components/DocumentLevelMonitorQueries/ConfigureDocumentLevelQueries'; import FindingsDashboard from '../../../Dashboard/containers/FindingsDashboard'; -import { validDocLevelGraphQueries } from '../../../Dashboard/components/FindingsDashboard/findingsUtils'; +import { validDocLevelGraphQueries } from '../../components/DocumentLevelMonitorQueries/utils/helpers'; function renderEmptyMessage(message) { return ( diff --git a/public/pages/Dashboard/components/FindingsDashboard/findingsUtils.js b/public/pages/Dashboard/components/FindingsDashboard/findingsUtils.js index 98fa5345f..5222eedcd 100644 --- a/public/pages/Dashboard/components/FindingsDashboard/findingsUtils.js +++ b/public/pages/Dashboard/components/FindingsDashboard/findingsUtils.js @@ -11,11 +11,8 @@ import FindingsPopover from './FindingsPopover'; import queryString from 'query-string'; import { backendErrorNotification } from '../../../../utils/helpers'; import { MAX_FINDINGS_COUNT } from '../../containers/FindingsDashboard'; - -export const QUERY_OPERATORS = [ - { text: 'is', value: '==' }, - { text: 'is not', value: '!=' }, -]; +import { OPERATORS_MAP } from '../../../CreateMonitor/components/MonitorExpressions/expressions/utils/constants'; +import { validDocLevelGraphQueries } from '../../../CreateMonitor/components/DocumentLevelMonitorQueries/utils/helpers'; export const TABLE_TAB_IDS = { ALERTS: { id: 'alerts', name: 'Alerts' }, @@ -144,7 +141,7 @@ export const parseFindingsForPreview = (previewResponse = {}, index = '', querie docIdsToQueries[id].push({ name: queryName, query: query.query }); } else { const query = _.find(queries, { queryName: queryName }); - const operator = _.find(QUERY_OPERATORS, { value: query.operator }).text; + const operator = OPERATORS_MAP[_.toUpper(query.operator)].text; const querySource = `${query.field} ${operator} ${query.query}`; docIdsToQueries[id] = [{ name: queryName, query: querySource }]; } @@ -164,19 +161,6 @@ export const parseFindingsForPreview = (previewResponse = {}, index = '', querie return findings; }; -export const validDocLevelGraphQueries = (queries = []) => { - // The 'queryName', 'field', 'operator', and 'query' fields are required to execute a doc level query. - // If any of those fields are undefined for any queries, the monitor cannot be executed. - const incompleteQueries = queries.find( - (query) => - _.isEmpty(query.queryName) || - _.isEmpty(query.field) || - _.isEmpty(query.operator) || - _.isEmpty(query.query) - ); - return !_.isEmpty(queries) && _.isEmpty(incompleteQueries); -}; - export async function getFindings({ id, from, diff --git a/public/pages/Dashboard/components/FindingsDashboard/findingsUtils.test.js b/public/pages/Dashboard/components/FindingsDashboard/findingsUtils.test.js index a634b134c..b25f5e76f 100644 --- a/public/pages/Dashboard/components/FindingsDashboard/findingsUtils.test.js +++ b/public/pages/Dashboard/components/FindingsDashboard/findingsUtils.test.js @@ -4,12 +4,8 @@ */ import _ from 'lodash'; -import { - getFindingsForMonitor, - parseFindingsForPreview, - QUERY_OPERATORS, - validDocLevelGraphQueries, -} from './findingsUtils'; +import { getFindingsForMonitor, parseFindingsForPreview } from './findingsUtils'; +import { SUPPORTED_DOC_LEVEL_QUERY_OPERATORS } from '../../../CreateMonitor/components/DocumentLevelMonitorQueries/utils/constants'; describe('findingsUtils', () => { describe('getFindingsForMonitor', () => { @@ -130,7 +126,7 @@ describe('findingsUtils', () => { id: 'unknownQuery1', queryName: 'unknownQuery1', field: 'field.name', - operator: QUERY_OPERATORS[0].value, + operator: SUPPORTED_DOC_LEVEL_QUERY_OPERATORS[0], query: 'value1', tags: ['tag1', 'tag2'], }, @@ -138,7 +134,7 @@ describe('findingsUtils', () => { id: 'unknownQuery2', queryName: 'unknownQuery2', field: 'another.field.name', - operator: QUERY_OPERATORS[0].value, + operator: SUPPORTED_DOC_LEVEL_QUERY_OPERATORS[0], query: 'value2', tags: ['tag3'], }, @@ -166,7 +162,7 @@ describe('findingsUtils', () => { id: 'unknownQuery1', queryName: 'unknownQuery1', field: 'field.name', - operator: QUERY_OPERATORS[0].value, + operator: SUPPORTED_DOC_LEVEL_QUERY_OPERATORS[0], query: 'value1', tags: ['tag1', 'tag2'], }, @@ -174,7 +170,7 @@ describe('findingsUtils', () => { id: 'unknownQuery2', queryName: 'unknownQuery2', field: 'another.field.name', - operator: QUERY_OPERATORS[0].value, + operator: SUPPORTED_DOC_LEVEL_QUERY_OPERATORS[0], query: 'value2', tags: ['tag3'], }, @@ -189,7 +185,7 @@ describe('findingsUtils', () => { id: 'unknownQuery1', queryName: 'unknownQuery1', field: 'field.name', - operator: QUERY_OPERATORS[0].value, + operator: SUPPORTED_DOC_LEVEL_QUERY_OPERATORS[0], query: 'value1', tags: ['tag1', 'tag2'], }, @@ -197,7 +193,7 @@ describe('findingsUtils', () => { id: 'query2', queryName: 'query2', field: 'another.field.name', - operator: QUERY_OPERATORS[0].value, + operator: SUPPORTED_DOC_LEVEL_QUERY_OPERATORS[0], query: 'value2', tags: ['tag3'], }, @@ -236,121 +232,4 @@ describe('findingsUtils', () => { }); }); }); - - describe('validDocLevelGraphQueries', () => { - test('when no queries are supplied', () => { - const queries = []; - expect(validDocLevelGraphQueries(queries)).toEqual(false); - }); - - test('when a query does not have a queryName value', () => { - const queries = [ - { - id: 'query1', - queryName: '', - field: 'field.name', - operator: QUERY_OPERATORS[0].value, - query: 'value1', - tags: ['tag1', 'tag2'], - }, - { - id: 'query2', - queryName: 'query2', - field: 'another.field.name', - operator: QUERY_OPERATORS[0].value, - query: 'value2', - tags: ['tag3'], - }, - ]; - expect(validDocLevelGraphQueries(queries)).toEqual(false); - }); - - test('when a query does not have a field value', () => { - const queries = [ - { - id: 'query1', - queryName: 'query2', - field: '', - operator: QUERY_OPERATORS[0].value, - query: 'value1', - tags: ['tag1', 'tag2'], - }, - { - id: 'query2', - queryName: 'query2', - field: 'another.field.name', - operator: QUERY_OPERATORS[0].value, - query: 'value2', - tags: ['tag3'], - }, - ]; - expect(validDocLevelGraphQueries(queries)).toEqual(false); - }); - - test('when a query does not have an operator value', () => { - const queries = [ - { - id: 'query1', - queryName: 'query2', - field: 'field.name', - operator: '', - query: 'value1', - tags: ['tag1', 'tag2'], - }, - { - id: 'query2', - queryName: 'query2', - field: 'another.field.name', - operator: QUERY_OPERATORS[0].value, - query: 'value2', - tags: ['tag3'], - }, - ]; - expect(validDocLevelGraphQueries(queries)).toEqual(false); - }); - - test('when a query does not have a query value', () => { - const queries = [ - { - id: 'query1', - queryName: 'query1', - field: 'field.name', - operator: QUERY_OPERATORS[0].value, - query: '', - tags: ['tag1', 'tag2'], - }, - { - id: 'query2', - queryName: 'query2', - field: 'another.field.name', - operator: QUERY_OPERATORS[0].value, - query: 'value2', - tags: ['tag3'], - }, - ]; - expect(validDocLevelGraphQueries(queries)).toEqual(false); - }); - - test('when all queries are defined', () => { - const queries = [ - { - id: 'query1', - queryName: 'query2', - field: 'field.name', - operator: QUERY_OPERATORS[0].value, - query: 'value1', - tags: ['tag1', 'tag2'], - }, - { - id: 'query2', - queryName: 'query2', - field: 'another.field.name', - operator: QUERY_OPERATORS[0].value, - query: 'value2', - tags: ['tag3'], - }, - ]; - expect(validDocLevelGraphQueries(queries)).toEqual(true); - }); - }); }); diff --git a/public/utils/validate.js b/public/utils/validate.js index 6e7ee63fa..dbd6c13a4 100644 --- a/public/utils/validate.js +++ b/public/utils/validate.js @@ -59,25 +59,31 @@ export const required = (value) => { if (!value) return 'Required.'; }; -export const validateIllegalCharacters = (illegalCharacters = ILLEGAL_CHARACTERS) => (value) => { - if (_.isEmpty(value)) return required(value); +export const requiredNumber = (value) => { + if (isNaN(parseFloat(value))) return 'Requires numerical value.'; +}; - const illegalCharactersString = illegalCharacters.join(' '); - let errorText = `Contains invalid characters. Cannot contain: ${illegalCharactersString}`; +export const validateIllegalCharacters = + (illegalCharacters = ILLEGAL_CHARACTERS) => + (value) => { + if (_.isEmpty(value)) return required(value); - if (_.includes(illegalCharacters, ' ')) { - errorText = - illegalCharacters.length === 1 - ? 'Cannot contain spaces.' - : `Contains invalid characters or spaces. Cannot contain: ${illegalCharactersString}`; - } + const illegalCharactersString = illegalCharacters.join(' '); + let errorText = `Contains invalid characters. Cannot contain: ${illegalCharactersString}`; - let includesIllegalCharacter = false; - illegalCharacters.forEach((character) => { - if (_.includes(value, character)) includesIllegalCharacter = true; - }); - if (includesIllegalCharacter) return errorText; -}; + if (_.includes(illegalCharacters, ' ')) { + errorText = + illegalCharacters.length === 1 + ? 'Cannot contain spaces.' + : `Contains invalid characters or spaces. Cannot contain: ${illegalCharactersString}`; + } + + let includesIllegalCharacter = false; + illegalCharacters.forEach((character) => { + if (_.includes(value, character)) includesIllegalCharacter = true; + }); + if (includesIllegalCharacter) return errorText; + }; export const validateRequiredNumber = (value) => { if (value === undefined || typeof value === 'string') return 'Provide a value.'; diff --git a/public/utils/validate.test.js b/public/utils/validate.test.js index af5094193..e076f1b47 100644 --- a/public/utils/validate.test.js +++ b/public/utils/validate.test.js @@ -14,6 +14,7 @@ import { ILLEGAL_CHARACTERS, validateIndex, isIndexPatternQueryValid, + requiredNumber, } from './validate'; import { FORMIK_INITIAL_VALUES } from '../pages/CreateMonitor/containers/CreateMonitor/utils/constants'; import { TRIGGER_TYPE } from '../pages/CreateTrigger/containers/CreateTrigger/utils/constants'; @@ -202,3 +203,37 @@ describe('validateIndex', () => { expect(validateIndex([{ label: 'valid- index$' }])).toBe(invalidText); }); }); + +describe('requiredNumber', () => { + test('returns undefined for negative integers', () => { + expect(requiredNumber(-10)).toBeUndefined(); + }); + + test('returns undefined for negative decimal number', () => { + expect(requiredNumber(-10.25)).toBeUndefined(); + }); + + test('returns undefined for 0', () => { + expect(requiredNumber(0)).toBeUndefined(); + }); + + test('returns undefined for positive decimal number', () => { + expect(requiredNumber(10.25)).toBeUndefined(); + }); + + test('returns undefined for positive integer', () => { + expect(requiredNumber(10)).toBeUndefined(); + }); + + test('returns error text for string value', () => { + expect(requiredNumber('NaN')).toBe('Requires numerical value.'); + }); + + test('returns error text for undefined value', () => { + expect(requiredNumber(undefined)).toBe('Requires numerical value.'); + }); + + test('returns error text for null value', () => { + expect(requiredNumber(null)).toBe('Requires numerical value.'); + }); +});