From cf19c7d117275055fb6deb85f97f9567cfff1183 Mon Sep 17 00:00:00 2001 From: AWSHurneyt Date: Thu, 10 Mar 2022 20:47:11 -0800 Subject: [PATCH] 1.3release - cherry-picking from main to 1.x branch (#192) * OpenSearch 1.1.0 release (#103) * Create opensearch-alerting-dashboards-plugin.release-notes-1.1.0.0.md (#101) * Create opensearch-alerting-dashboards-plugin.release-notes-1.1.0.0.md * Create opensearch-alerting-dashboards-plugin.release-notes-1.1.0.0.md Signed-off-by: Annie Lee * Update opensearch-alerting-dashboards-plugin.release-notes-1.1.0.0.md Signed-off-by: Annie Lee * Update comments Signed-off-by: Annie Lee * Update version in package.json (#102) * Update package.json * Update opensearch-alerting-dashboards-plugin.release-notes-1.1.0.0.md Signed-off-by: Annie Lee * Update opensearch-alerting-dashboards-plugin.release-notes-1.1.0.0.md Signed-off-by: Annie Lee * Text updates (#105) * Add icon tooltip * Update text and rename MonitorDefinitionCard directory * Update Schedule.js * Update Schedule.js Signed-off-by: Annie Lee * Update opensearch-alerting-dashboards-plugin.release-notes-1.1.0.0.md * Remove license Signed-off-by: Annie Lee Signed-off-by: AWSHurneyt * Updates to workflow, unit tests, and some appearance (#114) * Added badges to the package README, and the Uploads coverage job to the unit tests workflow. (#104) * Added badges to the package README, and the Uploads coverage job to the unit tests workflow. * Removing code coverage upload token. * Update jest unit tests (#112) * Update .opensearch_dashboards-plugin-helpers.json * Update snapshots * Update whereExpression.test and some snapshots * Update whereExpression.test and some snapshots Signed-off-by: Annie Lee * Update snapshots * Update formikToTrigger.test.js * Update formikToTrigger.test.js * Update formikToTrigger.test.js * Add icon tooltip * Add test * Update tests * remove license * Update TriggerExpressions.test.js * Update Triggers.test.js * Update validation test * Update getOverviewStats.test.js * Update validate.test.js * Update helpers.test.js and remove unused import * Update Triggers.test.js * Update helpers.js * Update CreateMonitor test and clean up code * Update CreateMonitor test and clean up code Signed-off-by: Annie Lee * Update release note and adding more tests * Add test and modify cypress common-utils branch * Update MonitorDefinitionCard.test.js.snap * Update cypress-workflow.yml Signed-off-by: Annie Lee Co-authored-by: AWSHurneyt Signed-off-by: AWSHurneyt * Cherry-pick commits from main branch to 1.x branch (#131) * Bumps version to 1.2 (#128) * Bumps version to 1.2 * Changes test workflows to follow Dashboards 1.x Signed-off-by: Clay Downs * Fixed a bug that displayed all alerts for a monitor on individual triggers' flyouts. Fixed a bug that displayed incorrect source for the condition field on the alerts flyout. Fixed a bug that displayed incorrect severity on the alerts flyout. Fixed a bug that prevented selecting query-level monitor alerts 1 by 1. Fixed bug relating to validation of popovers when defining monitor queries. (#123) (#130) * Create opensearch-alerting-dashboards-plugin.release-notes-1.1.0.0.md (#101) * Create opensearch-alerting-dashboards-plugin.release-notes-1.1.0.0.md * Create opensearch-alerting-dashboards-plugin.release-notes-1.1.0.0.md Signed-off-by: Annie Lee * Update opensearch-alerting-dashboards-plugin.release-notes-1.1.0.0.md Signed-off-by: Annie Lee * Update comments Signed-off-by: Annie Lee * Update version in package.json (#102) * Update package.json * Update opensearch-alerting-dashboards-plugin.release-notes-1.1.0.0.md Signed-off-by: Annie Lee * Update opensearch-alerting-dashboards-plugin.release-notes-1.1.0.0.md Signed-off-by: Annie Lee * Text updates (#105) * Add icon tooltip * Update text and rename MonitorDefinitionCard directory * Update Schedule.js * Update Schedule.js Signed-off-by: Annie Lee * Update opensearch-alerting-dashboards-plugin.release-notes-1.1.0.0.md * Remove license Signed-off-by: Annie Lee * Added badges to the package README, and the Uploads coverage job to the unit tests workflow. (#104) * Added badges to the package README, and the Uploads coverage job to the unit tests workflow. * Removing code coverage upload token. * Update jest unit tests (#112) * Update .opensearch_dashboards-plugin-helpers.json * Update snapshots * Update whereExpression.test and some snapshots * Update whereExpression.test and some snapshots Signed-off-by: Annie Lee * Update snapshots * Update formikToTrigger.test.js * Update formikToTrigger.test.js * Update formikToTrigger.test.js * Add icon tooltip * Add test * Update tests * remove license * Update TriggerExpressions.test.js * Update Triggers.test.js * Update validation test * Update getOverviewStats.test.js * Update validate.test.js * Update helpers.test.js and remove unused import * Update Triggers.test.js * Update helpers.js * Update CreateMonitor test and clean up code * Update CreateMonitor test and clean up code Signed-off-by: Annie Lee * Update release note and adding more tests * Add test and modify cypress common-utils branch * Update MonitorDefinitionCard.test.js.snap * Update cypress-workflow.yml * Update jest unit tests (#112) * Update .opensearch_dashboards-plugin-helpers.json * Update snapshots * Update whereExpression.test and some snapshots * Update whereExpression.test and some snapshots Signed-off-by: Annie Lee * Update snapshots * Update formikToTrigger.test.js * Update formikToTrigger.test.js * Update formikToTrigger.test.js * Add icon tooltip * Add test * Update tests * remove license * Update TriggerExpressions.test.js * Update Triggers.test.js * Update validation test * Update getOverviewStats.test.js * Update validate.test.js * Update helpers.test.js and remove unused import * Update Triggers.test.js * Update helpers.js * Update CreateMonitor test and clean up code * Update CreateMonitor test and clean up code Signed-off-by: Annie Lee * Update release note and adding more tests * Add test and modify cypress common-utils branch * Update MonitorDefinitionCard.test.js.snap * Update cypress-workflow.yml Signed-off-by: Annie Lee * Fixed a few bugs * Update opensearch-alerting-dashboards-plugin.release-notes-1.1.0.0.md Signed-off-by: Annie Lee * Fixed a bug that displayed all alerts for a monitor on individual trigger flyouts. Fixed a bug that diplayed incorrect source for the condition field on the alerts flyout. Fixed a bug that diplaying incorrect severity on the alerts flyout. * Updated release notes to reflect PR 122 bug fix. * Fixing number of alerts displayed on Monitors tab. * Update opensearch-alerting-dashboards-plugin.release-notes-1.1.0.0.md Signed-off-by: Annie Lee * More bug fix Signed-off-by: Annie Lee * Skip test based on modification Signed-off-by: Annie Lee * Skip test based on modification Signed-off-by: Annie Lee * Update popover windows to remove item when filed is not defined * Update field validation * Fixed a bug that prevented selecting query-level monitor alerts 1 by 1. Removed experimental code and comments. * Merge remote-tracking branch 'thomas/alertFlyoutBugFix' into bug-fix * Update Dashboard.js * Support data filter when using null operator * Update WhereExpression.js * Fixed a bug that was causing incorrect pagination display on alerts flyout. * Removed redundant validation from filter values that was generating error messages that prevented preview graphs from displaying data. * Removed redundant validation from filter values that was generating error messages that prevented preview graphs from displaying data. * Update metric error for query monitors * Update MetricExpression.js * Removed experimental dev code. * Updated release notes. Co-authored-by: Annie Lee Co-authored-by: Annie Lee Co-authored-by: AWSHurneyt Co-authored-by: Clay Downs Co-authored-by: AWSHurneyt Signed-off-by: AWSHurneyt * Cherry-picking commits from main to 1.x branch for 1.2 release (#142) * Fixes flaky test and removes local publishing of plugin dependencies (#135) * Fixes 'bucket level monitor can be created by extraction query' flaky test * Removes local publishing of plugin dependencies for github cypress tests Signed-off-by: Clay Downs * Update copyright notice (#140) Signed-off-by: Mohammad Qureshi * Added 1.2 release notes. (#141) * Added badges to the package README, and the Uploads coverage job to the unit tests workflow. * Removing code coverage upload token. * Added 1.2 release notes. Signed-off-by: Thomas Hurney Signed-off-by: AWSHurneyt * Added 1.2 release notes. Signed-off-by: Thomas Hurney Signed-off-by: AWSHurneyt * Updated 1.2 release notes. Signed-off-by: Thomas Hurney Signed-off-by: AWSHurneyt Co-authored-by: Clay Downs Co-authored-by: Mohammad Qureshi Signed-off-by: AWSHurneyt * Added DCO section to CONTRIBUTING.md and GitHub workflows. Updated copyright notice. Signed-off-by: Thomas Hurney (#145) Signed-off-by: AWSHurneyt * support creating monitor for anomaly detector with custom result index (#143) (#147) * support creating monitor for anomaly detector with custom result index Signed-off-by: Yaliang Wu * add release note Signed-off-by: Yaliang Wu Signed-off-by: AWSHurneyt * Bumping version to 1.3. Signed-off-by: AWSHurneyt (#159) Signed-off-by: AWSHurneyt <79280347+AWSHurneyt@users.noreply.github.com> Signed-off-by: AWSHurneyt * initial commit (#150) Signed-off-by: CEHENKLE Signed-off-by: AWSHurneyt * Add .whitesource configuration file (#153) Co-authored-by: whitesource-for-github-com[bot] <50673670+whitesource-for-github-com[bot]@users.noreply.github.com> Signed-off-by: AWSHurneyt * Implemented a toast to display successful attempts to acknowledge alerts. Refactored alerts dashboard flyout to refresh its alerts table when alerts are acknowledged. (#160) * Implemented unit and integ tests for the alerts dashboard flyout. Refactored AlertsDashboardFlyoutComponent::getBucketLevelGraphConditions to return a string with line breaks instead of an array of HTML elements. Signed-off-by: AWSHurneyt Signed-off-by: AWSHurneyt <79280347+AWSHurneyt@users.noreply.github.com> * Removed an unused test variable. Signed-off-by: AWSHurneyt <79280347+AWSHurneyt@users.noreply.github.com> * Removed debug logs. Signed-off-by: AWSHurneyt <79280347+AWSHurneyt@users.noreply.github.com> * Implemented unit test. Refactored integration tests to use fewer wait periods. Signed-off-by: AWSHurneyt <79280347+AWSHurneyt@users.noreply.github.com> * Examining flakiness in cypress test. Signed-off-by: AWSHurneyt <79280347+AWSHurneyt@users.noreply.github.com> * Added short wait period to flyout cypress tests to alleviate flakiness. Signed-off-by: AWSHurneyt <79280347+AWSHurneyt@users.noreply.github.com> * Refactored flyout cypress tests to use aliases. Signed-off-by: AWSHurneyt <79280347+AWSHurneyt@users.noreply.github.com> Signed-off-by: AWSHurneyt * Adding basic unit tests (#151) * Add flyout render Signed-off-by: Annie Lee * Add some basic tests Signed-off-by: Annie Lee * Add more tests Signed-off-by: Annie Lee * Update jest.config.js Signed-off-by: Annie Lee * Add tests Signed-off-by: Annie Lee * Update dashboard sample alerttime Signed-off-by: Annie Lee * Update dashboard test alert time snapshot Signed-off-by: Annie Lee * Update Dashboard.test.js Signed-off-by: Annie Lee * Remove test alert start time Signed-off-by: Annie Lee * Update VisualGraph test and code Signed-off-by: Annie Lee * Update package.json to run unit tests in UTC timezone Signed-off-by: Annie Lee * Update dashboard sample alert start time Signed-off-by: Annie Lee * Update Dashboard test Signed-off-by: Annie Lee * Update Dashboard.test.js.snap Signed-off-by: Annie Lee * Update snapshot file Signed-off-by: Annie Lee Signed-off-by: AWSHurneyt * Fix the error handling when config index is not found (#173) Signed-off-by: Annie Lee Signed-off-by: AWSHurneyt * Updated copyright notices and headers. (#168) * Updated CONTRIBUTING.md and NOTICE files. Signed-off-by: AWSHurneyt <79280347+AWSHurneyt@users.noreply.github.com> * Updated license headers. Signed-off-by: AWSHurneyt <79280347+AWSHurneyt@users.noreply.github.com> Signed-off-by: AWSHurneyt * Refactored acknowledge alerts button on Alerts by trigger dashboard page to be a modal experience. (#167) * Refactored Acknowledge button on Alerts by trigger dashboard to display a modal. Signed-off-by: AWSHurneyt <79280347+AWSHurneyt@users.noreply.github.com> * Updated license headers. Signed-off-by: AWSHurneyt <79280347+AWSHurneyt@users.noreply.github.com> Signed-off-by: AWSHurneyt * Adding a few more basic unit tests (#180) * Add alerting dashboards flyout render unit test Signed-off-by: Annie Lee * Add MonitorTimeFields validation tests Signed-off-by: Annie Lee * Refactor error messages to add period Signed-off-by: Annie Lee Signed-off-by: AWSHurneyt * Remove node version declaration in package.json (#166) * Remove node version declaration in package.json Signed-off-by: Annie Lee * Remove react-router-dom version declaration Signed-off-by: Annie Lee Signed-off-by: AWSHurneyt * Update DestinationsService.js (#182) Signed-off-by: Annie Lee Signed-off-by: AWSHurneyt * Add backport workflow (#176) * Add backport workflow Signed-off-by: Annie Lee * Create delete_backport_branch.yml Signed-off-by: Annie Lee Signed-off-by: AWSHurneyt * Configure test workflows to run on 1.x branches (#183) Signed-off-by: Annie Lee Signed-off-by: AWSHurneyt * Add 1.3 release notes (#175) (#185) * Create opensearch-alerting-dashboards-plugin.release-notes-1.3.0.0.md Signed-off-by: Annie Lee * Update opensearch-alerting-dashboards-plugin.release-notes-1.3.0.0.md Signed-off-by: Annie Lee (cherry picked from commit 98f790486297c7cf003f025eb778029c28a351c3) Co-authored-by: Annie Lee Signed-off-by: AWSHurneyt * Add backport documentation link (#184) (#187) * Create BACKPORT.md Signed-off-by: Annie Lee * Replace documentation with a link Signed-off-by: Annie Lee (cherry picked from commit 0a0c8c50ab97393fac4cd3cddabbfb86794d1794) Co-authored-by: Annie Lee Signed-off-by: AWSHurneyt * Implemented support for cluster metrics monitors (#162) (#189) * Implemented LocalUriInput component, and supporting methods. Implemented unit and integration tests for LocalUriInput. Refactored various other classes and components to support LocalUriInput. Signed-off-by: AWSHurneyt * Refactored learn more links to open new tabs. Signed-off-by: AWSHurneyt * Implemented default trigger conditions for API supported by the LocalUriInput feature. Signed-off-by: AWSHurneyt * Refactored button text and size based on UX reviewer feedback. Signed-off-by: AWSHurneyt * Added documentation link for cluster stats API. Signed-off-by: AWSHurneyt * Adjusted some wording based on UX reviewer feedback. Implemented modal that displays when changing request type. Signed-off-by: AWSHurneyt * Refactored feature assets to use ClusterMetrics naming convention instead of LocalUriInput. Signed-off-by: AWSHurneyt * Refactored behavior of cluster metrics clear triggers modal, and implemented tests. Signed-off-by: AWSHurneyt * Added periods to error messages. Signed-off-by: AWSHurneyt * Removed fixed width for clearTriggersModal. Signed-off-by: AWSHurneyt * Refactored the cluster metrics radio card to be a monitor type instead of monitor definition type. Refactored request type selection combobox so it's not clearable, and reworded the clearTriggersModal text and refactored tests accordingly. Signed-off-by: AWSHurneyt * Updated copyright headers. Signed-off-by: AWSHurneyt * Refactoring monitorType checking logic to accommodate cluster metrics monitors. Signed-off-by: AWSHurneyt * Refactored help text to use lowercase letters. Signed-off-by: AWSHurneyt * Implemented a helper method as a temporary solution to some incorrect formatting of cluster metrics monitors returned by the getMonitors API. Signed-off-by: AWSHurneyt * Refactored cluster metrics feature to remove support for CAT repositories. Signed-off-by: AWSHurneyt * Refactored cluster metrics request types labels to no longer reference CAT. Signed-off-by: AWSHurneyt * Adjusted the example trigger conditions for two request types. Signed-off-by: AWSHurneyt Co-authored-by: Annie Lee <71157062+leeyun-amzn@users.noreply.github.com> Co-authored-by: Clay Downs Co-authored-by: Mohammad Qureshi Co-authored-by: Yaliang <49084640+ylwu-amzn@users.noreply.github.com> Co-authored-by: CEHENKLE Co-authored-by: whitesource-for-github-com[bot] <50673670+whitesource-for-github-com[bot]@users.noreply.github.com> Co-authored-by: Annie Lee Co-authored-by: opensearch-trigger-bot[bot] <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> --- .../sample_cluster_metrics_monitor.json | 64 ++ .../cluster_metrics_monitor_spec.js | 388 ++++++++ .../AlertsDashboardFlyoutComponent.js | 15 +- .../ClusterMetricsMonitor.js | 293 ++++++ .../ClusterMetricsMonitor.test.js | 21 + .../ClusterMetricsMonitor.test.js.snap | 184 ++++ .../components/ClusterMetricsMonitor/index.js | 8 + .../utils/clusterMetricsMonitorConstants.js | 178 ++++ .../utils/clusterMetricsMonitorHelpers.js | 118 +++ .../clusterMetricsMonitorHelpers.test.js | 940 ++++++++++++++++++ .../MonitorDefinitionCard.js | 5 +- .../MonitorDefinitionCard.test.js.snap | 10 +- .../components/MonitorType/MonitorType.js | 26 +- .../__snapshots__/MonitorType.test.js.snap | 71 +- .../AnomalyDetector.test.js.snap | 36 + .../containers/CreateMonitor/CreateMonitor.js | 1 + .../__snapshots__/CreateMonitor.test.js.snap | 6 + .../formikToMonitor.test.js.snap | 33 + .../CreateMonitor/utils/constants.js | 6 + .../CreateMonitor/utils/formikToMonitor.js | 30 + .../utils/formikToMonitor.test.js | 23 + .../CreateMonitor/utils/monitorToFormik.js | 13 +- .../utils/monitorToFormik.test.js | 32 + .../containers/DefineMonitor/DefineMonitor.js | 107 +- .../__snapshots__/DefineMonitor.test.js.snap | 6 + .../MonitorDetails/MonitorDetails.js | 11 +- .../__snapshots__/MonitorIndex.test.js.snap | 30 + .../CreateTrigger/components/Action/Action.js | 1 + .../components/Action/utils/validate.js | 4 +- .../AddActionButton/AddActionButton.js | 1 + .../AddTriggerButton/AddTriggerButton.js | 10 +- .../BucketLevelTriggerQuery.js | 164 +-- .../TriggerEmptyPrompt/TriggerEmptyPrompt.js | 9 +- .../components/TriggerQuery/TriggerQuery.js | 195 ++-- .../__snapshots__/TriggerQuery.test.js.snap | 152 ++- .../ConfigureActions/ConfigureActions.js | 2 + .../ConfigureTriggers/ConfigureTriggers.js | 180 ++-- .../CreateTrigger/CreateTrigger.js | 48 +- .../CreateTrigger/utils/formikToTrigger.js | 15 +- .../utils/formikToTrigger.test.js | 7 + .../CreateTrigger/utils/triggerToFormik.js | 10 +- .../containers/DefineTrigger/DefineTrigger.js | 19 +- .../AcknowledgeAlertsModal.js | 2 + .../AcknowledgeAlertsModal.test.js.snap | 6 + .../MonitorOverview/utils/getOverviewStats.js | 18 +- .../Monitors/containers/Monitors/Monitors.js | 25 +- public/utils/constants.js | 2 + public/utils/validate.js | 17 +- 48 files changed, 3147 insertions(+), 395 deletions(-) create mode 100644 cypress/fixtures/sample_cluster_metrics_monitor.json create mode 100644 cypress/integration/cluster_metrics_monitor_spec.js create mode 100644 public/pages/CreateMonitor/components/ClusterMetricsMonitor/ClusterMetricsMonitor.js create mode 100644 public/pages/CreateMonitor/components/ClusterMetricsMonitor/ClusterMetricsMonitor.test.js create mode 100644 public/pages/CreateMonitor/components/ClusterMetricsMonitor/__snapshots__/ClusterMetricsMonitor.test.js.snap create mode 100644 public/pages/CreateMonitor/components/ClusterMetricsMonitor/index.js create mode 100644 public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorConstants.js create mode 100644 public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers.js create mode 100644 public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers.test.js diff --git a/cypress/fixtures/sample_cluster_metrics_monitor.json b/cypress/fixtures/sample_cluster_metrics_monitor.json new file mode 100644 index 000000000..9b6ccfae9 --- /dev/null +++ b/cypress/fixtures/sample_cluster_metrics_monitor.json @@ -0,0 +1,64 @@ +{ + "name": "sample_cluster_metrics_health_monitor", + "type": "monitor", + "monitor_type": "cluster_metrics_monitor", + "enabled": true, + "schedule": { + "period": { + "unit": "MINUTES", + "interval": 1 + } + }, + "inputs": [ + { + "uri": { + "api_type": "CLUSTER_HEALTH", + "path": "_cluster/health/", + "path_params": "", + "url": "http://localhost:9200/_cluster/health/" + } + } + ], + "triggers": [], + "ui_metadata": { + "schedule": { + "timezone": null, + "frequency": "interval", + "period": { + "unit": "MINUTES", + "interval": 1 + }, + "daily": 0, + "weekly": { + "tue": false, + "wed": false, + "thur": false, + "sat": false, + "fri": false, + "mon": false, + "sun": false + }, + "monthly": { + "type": "day", + "day": 1 + }, + "cronExpression": "0 */1 * * *" + }, + "search": { + "searchType": "clusterMetrics", + "timeField": "", + "aggregations": [], + "groupBy": [], + "bucketValue": 1, + "bucketUnitOfTime": "h", + "where": { + "fieldName": [], + "fieldRangeEnd": 0, + "fieldRangeStart": 0, + "fieldValue": "", + "operator": "is" + } + }, + "monitor_type": "cluster_metrics_monitor" + } +} diff --git a/cypress/integration/cluster_metrics_monitor_spec.js b/cypress/integration/cluster_metrics_monitor_spec.js new file mode 100644 index 000000000..5bb6d81a1 --- /dev/null +++ b/cypress/integration/cluster_metrics_monitor_spec.js @@ -0,0 +1,388 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import sampleDestination from '../fixtures/sample_destination_custom_webhook'; +import sampleClusterMetricsMonitor from '../fixtures/sample_cluster_metrics_monitor.json'; +import { INDEX, PLUGIN_NAME } from '../../cypress/support/constants'; + +const SAMPLE_CLUSTER_METRICS_HEALTH_MONITOR = 'sample_cluster_metrics_health_monitor'; +const SAMPLE_CLUSTER_METRICS_NODES_STATS_MONITOR = 'sample_cluster_metrics_nodes_stats_monitor'; +const SAMPLE_CLUSTER_METRICS_CAT_SNAPSHOTS_MONITOR = 'sample_cluster_metrics_cat_snapshots_monitor'; +const SAMPLE_TRIGGER = 'sample_trigger'; +const SAMPLE_ACTION = 'sample_action'; +const SAMPLE_DESTINATION = 'sample_destination'; + +const addClusterMetricsTrigger = (triggerName, triggerIndex, actionName, isEdit, source) => { + // Click 'Add trigger' button + cy.contains('Add trigger', { timeout: 20000 }).click({ force: true }); + + if (isEdit === true) { + // TODO: Passing button props in EUI accordion was added in newer versions (31.7.0+). + // If this ever becomes available, it can be used to pass data-test-subj for the button. + // Since the above is currently not possible, referring to the accordion button using its content + cy.get('button').contains('New trigger').click(); + } + + // Type in the trigger name + cy.get(`input[name="triggerDefinitions[${triggerIndex}].name"]`).type(triggerName); + + // Clear the default trigger condition source, and type the sample source + cy.get('[data-test-subj="triggerQueryCodeEditor"]').within(() => { + // If possible, a data-test-subj attribute should be added to access the code editor input directly + cy.get('.ace_text-input') + .focus() + .clear({ force: true }) + .type(JSON.stringify(source), { + force: true, + parseSpecialCharSequences: false, + delay: 5, + timeout: 20000, + }) + .trigger('blur', { force: true }); + }); + + // Type in the action name + cy.get(`input[name="triggerDefinitions[${triggerIndex}].actions.0.name"]`).type(actionName, { + force: true, + }); + + // Click the combo box to list all the destinations + // Using key typing instead of clicking the menu option to avoid occasional failure + cy.get(`[data-test-subj="triggerDefinitions[${triggerIndex}].actions.0_actionDestination"]`) + .click({ force: true }) + .type(`${SAMPLE_DESTINATION}{downarrow}{enter}`); +}; + +describe('ClusterMetricsMonitor', () => { + before(() => { + cy.createDestination(sampleDestination); + + // Load sample data + cy.loadSampleEcommerceData(); + }); + + beforeEach(() => { + // Set welcome screen tracking to false + localStorage.setItem('home:welcome:show', 'false'); + + // Visit Alerting OpenSearch Dashboards + cy.visit(`${Cypress.env('opensearch_dashboards')}/app/${PLUGIN_NAME}#/monitors`); + + // Common text to wait for to confirm page loaded, give up to 20 seconds for initial load + cy.contains('Create monitor', { timeout: 20000 }); + }); + + describe('can be created', () => { + beforeEach(() => { + cy.deleteAllMonitors(); + cy.reload(); + }); + + it('for the Cluster Health API', () => { + // Confirm empty monitor list is loaded + cy.contains('There are no existing monitors'); + + // Go to create monitor page + cy.contains('Create monitor').click(); + + // Select ClusterMetrics radio card + cy.get('[data-test-subj="clusterMetricsMonitorRadioCard"]').click(); + + // Wait for input to load and then type in the monitor name + cy.get('input[name="name"]').type(SAMPLE_CLUSTER_METRICS_HEALTH_MONITOR); + + // Wait for the API types to load and then type in the Cluster Health API + cy.get('[data-test-subj="clusterMetricsApiTypeComboBox"]').type('cluster health{enter}'); + + // Confirm the Query parameters field is present and described as "optional" + cy.contains('Query parameters - optional'); + cy.get('[data-test-subj="clusterMetricsParamsFieldText"]'); + + // Press the 'Run for response' button + cy.get('[data-test-subj="clusterMetricsPreviewButton"]').click(); + + // Add a trigger + cy.contains('Add trigger').click({ force: true }); + + // Type in the trigger name + cy.get('input[name="triggerDefinitions[0].name"]').type(SAMPLE_TRIGGER); + + // Type in the action name + cy.get('input[name="triggerDefinitions[0].actions.0.name"]').type(SAMPLE_ACTION); + + // Click the combo box to list all the destinations + // Using key typing instead of clicking the menu option to avoid occasional failure + cy.get('div[name="triggerDefinitions[0].actions.0.destination_id"]') + .click({ force: true }) + .type('{downarrow}{enter}'); + + // Click the create button + cy.get('button').contains('Create').click(); + + // Confirm we can see only one row in the trigger list by checking element + cy.contains('This table contains 1 row'); + + // Confirm we can see the new trigger + cy.contains(SAMPLE_TRIGGER); + + // Go back to the Monitors list + cy.get('a').contains('Monitors').click(); + + // Confirm we can see the created monitor in the list + cy.contains(SAMPLE_CLUSTER_METRICS_HEALTH_MONITOR); + }); + + it('for the Nodes Stats API', () => { + // Confirm empty monitor list is loaded + cy.contains('There are no existing monitors'); + + // Go to create monitor page + cy.contains('Create monitor').click(); + + // Select ClusterMetrics radio card + cy.get('[data-test-subj="clusterMetricsMonitorRadioCard"]').click(); + + // Wait for input to load and then type in the monitor name + cy.get('input[name="name"]').type(SAMPLE_CLUSTER_METRICS_NODES_STATS_MONITOR); + + // Wait for the API types to load and then type in the Cluster Health API + cy.get('[data-test-subj="clusterMetricsApiTypeComboBox"]').type('nodes stats{enter}'); + + // Confirm the Query parameters field is not present + cy.contains('Query parameters').should('not.exist'); + cy.get('[data-test-subj="clusterMetricsParamsFieldText"]').should('not.exist'); + + // Press the 'Run for response' button + cy.get('[data-test-subj="clusterMetricsPreviewButton"]').click(); + + // Add a trigger + cy.contains('Add trigger').click({ force: true }); + + // Type in the trigger name + cy.get('input[name="triggerDefinitions[0].name"]').type(SAMPLE_TRIGGER); + + // Type in the action name + cy.get('input[name="triggerDefinitions[0].actions.0.name"]').type(SAMPLE_ACTION); + + // Click the combo box to list all the destinations + // Using key typing instead of clicking the menu option to avoid occasional failure + cy.get('div[name="triggerDefinitions[0].actions.0.destination_id"]') + .click({ force: true }) + .type('{downarrow}{enter}'); + + // Click the create button + cy.get('button').contains('Create').click(); + + // Confirm we can see only one row in the trigger list by checking element + cy.contains('This table contains 1 row'); + + // Confirm we can see the new trigger + cy.contains(SAMPLE_TRIGGER); + + // Go back to the Monitors list + cy.get('a').contains('Monitors').click(); + + // Confirm we can see the created monitor in the list + cy.contains(SAMPLE_CLUSTER_METRICS_NODES_STATS_MONITOR); + }); + }); + + describe('displays Query parameters field appropriately', () => { + beforeEach(() => { + cy.deleteAllMonitors(); + cy.reload(); + }); + + it('for the CAT Snapshots API', () => { + // Confirm empty monitor list is loaded + cy.contains('There are no existing monitors'); + + // Go to create monitor page + cy.contains('Create monitor').click(); + + // Select ClusterMetrics radio card + cy.get('[data-test-subj="clusterMetricsMonitorRadioCard"]').click(); + + // Wait for input to load and then type in the monitor name + cy.get('input[name="name"]').type(SAMPLE_CLUSTER_METRICS_CAT_SNAPSHOTS_MONITOR); + + // Wait for the API types to load and then type in the Cluster Health API + cy.get('[data-test-subj="clusterMetricsApiTypeComboBox"]').type('list snapshots{enter}'); + + // Confirm the Query parameters field is present and is not described as "optional" + cy.contains('Query parameters - optional').should('not.exist'); + cy.contains('Query parameters'); + cy.get('[data-test-subj="clusterMetricsParamsFieldText"]'); + }); + }); + + describe('clearTriggersModal renders and behaves as expected', () => { + beforeEach(() => { + // Visit Alerting OpenSearch Dashboards + cy.visit(`${Cypress.env('opensearch_dashboards')}/app/${PLUGIN_NAME}#/monitors`); + + // Begin monitor creation + // Confirm empty monitor list is loaded + cy.contains('There are no existing monitors'); + + // Go to create monitor page + cy.contains('Create monitor').click(); + + // Select ClusterMetrics radio card + cy.get('[data-test-subj="clusterMetricsMonitorRadioCard"]').click(); + + // Wait for input to load and then type in the monitor name + cy.get('input[name="name"]').type(SAMPLE_CLUSTER_METRICS_HEALTH_MONITOR); + }); + + it('when no triggers exist', () => { + // Confirm there are 0 triggers defined + cy.contains('Triggers (0)'); + + describe('blank API type is defined', () => { + // Select the Cluster Health API + cy.get('[data-test-subj="clusterMetricsApiTypeComboBox"]').type('cluster health{enter}'); + + // Confirm clearTriggersModal is not displayed + cy.get('[data-test-subj="clusterMetricsClearTriggersModal"]').should('not.exist'); + }); + + describe('API type is changed', () => { + // Change the API type to Cluster Stats + cy.get('[data-test-subj="clusterMetricsApiTypeComboBox"]').type('cluster stats{enter}'); + + // Confirm clearTriggersModal is not displayed + cy.get('[data-test-subj="clusterMetricsClearTriggersModal"]').should('not.exist'); + }); + }); + + it('when triggers exist', () => { + // Add a trigger for testing purposes + addClusterMetricsTrigger( + SAMPLE_TRIGGER, + 0, + SAMPLE_ACTION, + false, + 'ctx.results[0].number_of_pending_tasks >= 0' + ); + + // Confirm there is 1 trigger defined + cy.contains('Triggers (1)'); + + describe('blank API type is defined', () => { + // Select the Cluster Health API + cy.get('[data-test-subj="clusterMetricsApiTypeComboBox"]').type('cluster health{enter}'); + + // Confirm clearTriggersModal did not open + cy.get('[data-test-subj="clusterMetricsClearTriggersModal"]').should('not.exist'); + }); + + describe('API type is changed', () => { + // Change the API type to Cluster Stats + cy.get('[data-test-subj="clusterMetricsApiTypeComboBox"]').type('cluster stats{enter}'); + + // Confirm clearTriggersModal displays appropriate text + cy.contains( + 'You are about to change the request type. The existing trigger conditions may not be supported. Would you like to clear the existing trigger conditions?' + ); + }); + + describe('the modal CLOSE (i.e., the X button) button is clicked', () => { + // Click the CLOSE button + cy.get('[class="euiButtonIcon euiButtonIcon--text euiModal__closeIcon"]').click(); + + // Confirm clearTriggersModal closed + cy.get('[data-test-subj="clusterMetricsClearTriggersModal"]').should('not.exist'); + + // Confirm API type reverted back to Cluster Health + cy.get('[data-test-subj="clusterMetricsApiTypeComboBox"]').contains('Cluster health'); + + // Confirm there is 1 trigger defined + cy.contains('Triggers (1)'); + }); + + describe('the modal KEEP button is clicked', () => { + // Change the API type to Cluster Stats + cy.get('[data-test-subj="clusterMetricsApiTypeComboBox"]').type('cluster stats{enter}'); + + // Click the KEEP button + cy.get('[data-test-subj="clusterMetricsClearTriggersModalKeepButton"]').click(); + + // Confirm clearTriggersModal closed + cy.get('[data-test-subj="clusterMetricsClearTriggersModal"]').should('not.exist'); + + // Confirm there is 1 trigger defined + cy.contains('Triggers (1)'); + }); + + describe('the modal CLEAR button is clicked', () => { + // Change the API type to Cluster Settings + cy.get('[data-test-subj="clusterMetricsApiTypeComboBox"]').type('cluster settings{enter}'); + + // Click the CLEAR button + cy.get('[data-test-subj="clusterMetricsClearTriggersModalClearButton"]').click(); + + // Confirm clearTriggersModal closed + cy.get('[data-test-subj="clusterMetricsClearTriggersModal"]').should('not.exist'); + + // Confirm API type changed to Cluster Stats + cy.get('[data-test-subj="clusterMetricsApiTypeComboBox"]').contains('Cluster settings'); + + // Confirm there are 0 triggers defined + cy.contains('Triggers (0)', { timeout: 20000 }); + }); + }); + }); + + describe('can update', () => { + beforeEach(() => { + cy.deleteAllMonitors(); + }); + + describe('Cluster Health API monitor', () => { + it('with a new trigger', () => { + // Create the sample monitor + cy.createMonitor(sampleClusterMetricsMonitor); + cy.reload(); + + // Confirm the created monitor can be seen + cy.contains(SAMPLE_CLUSTER_METRICS_HEALTH_MONITOR); + + // Select the monitor + cy.get('a').contains(SAMPLE_CLUSTER_METRICS_HEALTH_MONITOR).click({ force: true }); + + // Click Edit button + cy.contains('Edit').click({ force: true }); + + // Add a trigger + addClusterMetricsTrigger( + SAMPLE_TRIGGER, + 0, + SAMPLE_ACTION, + true, + 'ctx.results[0].number_of_pending_tasks >= 0' + ); + + // Click update button to save monitor changes + 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'); + + // Confirm we can see the new trigger + cy.contains(SAMPLE_TRIGGER); + }); + }); + }); + + after(() => { + // Delete all monitors and destinations + cy.deleteAllMonitors(); + cy.deleteAllDestinations(); + + // Delete sample data + cy.deleteIndexByName(`${INDEX.SAMPLE_DATA_ECOMMERCE}`); + }); +}); diff --git a/public/components/Flyout/flyouts/components/AlertsDashboardFlyoutComponent.js b/public/components/Flyout/flyouts/components/AlertsDashboardFlyoutComponent.js index ebd3b0878..886c197cb 100644 --- a/public/components/Flyout/flyouts/components/AlertsDashboardFlyoutComponent.js +++ b/public/components/Flyout/flyouts/components/AlertsDashboardFlyoutComponent.js @@ -300,14 +300,13 @@ export default class AlertsDashboardFlyoutComponent extends Component { const searchType = _.get(monitor, 'ui_metadata.search.searchType', SEARCH_TYPE.GRAPH); const detectorId = _.get(monitor, MONITOR_INPUT_DETECTOR_ID); - const triggerType = - monitorType === MONITOR_TYPE.QUERY_LEVEL - ? TRIGGER_TYPE.QUERY_LEVEL - : TRIGGER_TYPE.BUCKET_LEVEL; + const triggerType = monitorType === MONITOR_TYPE.BUCKET_LEVEL + ? TRIGGER_TYPE.BUCKET_LEVEL + : TRIGGER_TYPE.QUERY_LEVEL; - let trigger = _.get(monitor, 'triggers', []).find((trigger) => { - return trigger[triggerType].id === triggerID; - }); + let trigger = _.get(monitor, 'triggers', []).find( + (trigger) => trigger[triggerType].id === triggerID + ); trigger = _.get(trigger, triggerType); const severity = _.get(trigger, 'severity'); @@ -360,6 +359,7 @@ export default class AlertsDashboardFlyoutComponent extends Component { const getItemId = (item) => { switch (monitorType) { case MONITOR_TYPE.QUERY_LEVEL: + case MONITOR_TYPE.CLUSTER_METRICS: return `${item.id}-${item.version}`; case MONITOR_TYPE.BUCKET_LEVEL: return item.id; @@ -384,6 +384,7 @@ export default class AlertsDashboardFlyoutComponent extends Component { let columns = []; switch (monitorType) { case MONITOR_TYPE.QUERY_LEVEL: + case MONITOR_TYPE.CLUSTER_METRICS: columns = queryColumns; break; case MONITOR_TYPE.BUCKET_LEVEL: diff --git a/public/pages/CreateMonitor/components/ClusterMetricsMonitor/ClusterMetricsMonitor.js b/public/pages/CreateMonitor/components/ClusterMetricsMonitor/ClusterMetricsMonitor.js new file mode 100644 index 000000000..d4fc032f0 --- /dev/null +++ b/public/pages/CreateMonitor/components/ClusterMetricsMonitor/ClusterMetricsMonitor.js @@ -0,0 +1,293 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import _ from 'lodash'; +import { + EuiButton, + EuiButtonEmpty, + EuiCodeEditor, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiLink, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiOverlayMask, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { isInvalidApiPath } from '../../../../utils/validate'; +import { FormikComboBox, FormikFieldText } from '../../../../components/FormControls'; +import { + API_PATH_REQUIRED_PLACEHOLDER_TEXT, + EMPTY_PATH_PARAMS_TEXT, + REST_API_REFERENCE, + API_TYPES, +} from './utils/clusterMetricsMonitorConstants'; +import { + getApiPath, + getExamplePathParams, + isInvalidApiPathParameter, + validateApiPathParameter, +} from './utils/clusterMetricsMonitorHelpers'; +import { FORMIK_INITIAL_VALUES } from '../../containers/CreateMonitor/utils/constants'; + +const setApiType = (form, apiType) => { + const pathParams = _.get(form, 'uri.path_params', FORMIK_INITIAL_VALUES.uri.path_params); + form.setFieldValue('uri.api_type', apiType); + form.setFieldValue('uri.path', getApiPath(_.isEmpty(pathParams), apiType)); +}; + +const renderModal = (closeModal, prevApiType, selectedApiType, form) => { + const onClear = () => { + setApiType(form, selectedApiType); + form.setFieldValue('triggerDefinitions', []); + closeModal(); + }; + const onKeep = () => { + setApiType(form, selectedApiType); + closeModal(); + }; + const onClose = () => { + setApiType(form, prevApiType); + closeModal(); + }; + return ( + + + + Clear all trigger conditions? + + + You are about to change the request type. The existing trigger conditions may not be + supported. Would you like to clear the existing trigger conditions? + + + + + + + Keep + + + + + + Clear + + + + + + + ); +}; + +const ClusterMetricsMonitor = ({ + isDarkMode, + loadingResponse = false, + loadingSupportedApiList = false, + onRunQuery, + resetResponse, + response, + supportedApiList = [], + values, +}) => { + const apiType = _.get(values, 'uri.api_type'); + const path = _.get(values, 'uri.path'); + const pathIsEmpty = _.isEmpty(path); + const pathParams = _.get(values, 'uri.path_params', FORMIK_INITIAL_VALUES.uri.path_params); + const supportsPathParams = !_.isEmpty(_.get(API_TYPES, `${apiType}.paths.withPathParams`)); + const requirePathParams = _.isEmpty(_.get(API_TYPES, `${apiType}.paths.withoutPathParams`)); + const hidePathParams = pathIsEmpty || loadingSupportedApiList || !supportsPathParams; + const disableRunButton = pathIsEmpty || (_.isEmpty(pathParams) && requirePathParams); + const hasTriggers = _.get(values, 'triggerDefinitions', []).length > 0; + + const [displayingModal, setDisplayingModal] = useState(false); + const [clearTriggersModal, setClearTriggersModal] = useState(undefined); + const closeModal = () => { + setDisplayingModal(false); + setClearTriggersModal(undefined); + }; + const openModal = (prevApiType, selectedApiType, form) => { + setDisplayingModal(true); + setClearTriggersModal(renderModal(closeModal, prevApiType, selectedApiType, form)); + }; + + return ( +
+ + + + + Request type + + + Specify a request type to monitor cluster metrics such as health, JVM, and CPU + usage.{' '} + + Learn more + + +
+ ), + isInvalid: isInvalidApiPath, + error: API_PATH_REQUIRED_PLACEHOLDER_TEXT, + }} + inputProps={{ + placeholder: loadingSupportedApiList ? 'Loading API options' : 'Select an API', + options: supportedApiList, + onBlur: (e, field, form) => { + form.setFieldTouched('uri.api_type'); + }, + onChange: (options, field, form) => { + const selectedApiType = _.get(options, '0.value'); + let changingApiType = true; + if (selectedApiType !== apiType) { + const doesNotUsePathParams = _.isEmpty( + _.get(API_TYPES, `${selectedApiType}.paths.withPathParams`) + ); + if (doesNotUsePathParams) + form.setFieldValue('uri.path_params', FORMIK_INITIAL_VALUES.uri.path_params); + if (hasTriggers && !_.isEmpty(apiType)) { + changingApiType = false; + openModal(apiType, selectedApiType, form); + } + resetResponse(); + form.setFieldTouched('uri.path_params', false); + } + if (changingApiType) setApiType(form, selectedApiType); + }, + isClearable: false, + singleSelection: { asPlainText: true }, + selectedOptions: !_.isEmpty(apiType) + ? [ + { + value: _.get(API_TYPES, `${apiType}.type`), + label: _.get(API_TYPES, `${apiType}.label`), + }, + ] + : undefined, + isDisabled: loadingSupportedApiList, + isLoading: loadingSupportedApiList, + 'data-test-subj': 'clusterMetricsApiTypeComboBox', + }} + /> + + {displayingModal && clearTriggersModal} + + + {!hidePathParams ? ( +
+ + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams), + }} + rowProps={{ + label: ( +
+ + Query parameters + {!requirePathParams && - optional } + + + Filter responses by providing path parameters for the{' '} + {_.lowerCase(_.get(API_TYPES, `${apiType}.label`)) || 'selected'} API.{' '} + {!pathIsEmpty && !_.isEmpty(_.get(API_TYPES, `${apiType}.documentation`)) && ( + + Learn more + + )} + +
+ ), + style: { maxWidth: '600px' }, + isInvalid: (value, field) => + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams), + error: (value, field) => + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams), + }} + inputProps={{ + placeholder: pathIsEmpty ? EMPTY_PATH_PARAMS_TEXT : getExamplePathParams(apiType), + fullWidth: true, + prepend: ( + + GET {_.get(API_TYPES, `${apiType}.prependText`)} + + ), + append: !_.isEmpty(_.get(API_TYPES, `${apiType}.appendText`)) && ( + + {_.get(API_TYPES, `${apiType}.appendText`)} + + ), + 'data-test-subj': 'clusterMetricsParamsFieldText', + }} + /> + + +
+ ) : null} + + + Preview query + + + + + + + + + ); +}; + +export default ClusterMetricsMonitor; diff --git a/public/pages/CreateMonitor/components/ClusterMetricsMonitor/ClusterMetricsMonitor.test.js b/public/pages/CreateMonitor/components/ClusterMetricsMonitor/ClusterMetricsMonitor.test.js new file mode 100644 index 000000000..f9d2f7482 --- /dev/null +++ b/public/pages/CreateMonitor/components/ClusterMetricsMonitor/ClusterMetricsMonitor.test.js @@ -0,0 +1,21 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from 'enzyme'; +import { Formik } from 'formik'; + +import ClusterMetricsMonitor from './ClusterMetricsMonitor'; + +describe('ClusterMetricsMonitor', () => { + test('renders', () => { + const component = ( + + + + ); + expect(render(component)).toMatchSnapshot(); + }); +}); diff --git a/public/pages/CreateMonitor/components/ClusterMetricsMonitor/__snapshots__/ClusterMetricsMonitor.test.js.snap b/public/pages/CreateMonitor/components/ClusterMetricsMonitor/__snapshots__/ClusterMetricsMonitor.test.js.snap new file mode 100644 index 000000000..ce04d6453 --- /dev/null +++ b/public/pages/CreateMonitor/components/ClusterMetricsMonitor/__snapshots__/ClusterMetricsMonitor.test.js.snap @@ -0,0 +1,184 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ClusterMetricsMonitor renders 1`] = ` +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+
+
+

+ Press Enter to start editing. +

+

+ When you're done, press Escape to stop editing. +

+
+
+
+
+
+
+`; diff --git a/public/pages/CreateMonitor/components/ClusterMetricsMonitor/index.js b/public/pages/CreateMonitor/components/ClusterMetricsMonitor/index.js new file mode 100644 index 000000000..f0dc16876 --- /dev/null +++ b/public/pages/CreateMonitor/components/ClusterMetricsMonitor/index.js @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import ClusterMetricsImport from './ClusterMetricsMonitor'; + +export default ClusterMetricsImport; diff --git a/public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorConstants.js b/public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorConstants.js new file mode 100644 index 000000000..9245d1aee --- /dev/null +++ b/public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorConstants.js @@ -0,0 +1,178 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const API_PATH_REQUIRED_PLACEHOLDER_TEXT = 'Select an API.'; +export const EMPTY_PATH_PARAMS_TEXT = 'Enter remaining path components and path parameters'; +export const GET_API_TYPE_DEBUG_TEXT = + 'Cannot determine ApiType in clusterMetricsMonitorHelpers::getSelectedApiType.'; +export const ILLEGAL_PATH_PARAMETER_CHARACTERS = [ + ':', + '"', + '+', + '\\', + '|', + '?', + '#', + '>', + '<', + ' ', +]; +export const NO_PATH_PARAMS_PLACEHOLDER_TEXT = 'No path parameter options'; +export const PATH_PARAMETER_ILLEGAL_CHARACTER_TEXT = `The provided path parameters contain invalid characters or spaces. Please omit: ${ILLEGAL_PATH_PARAMETER_CHARACTERS.join( + ' ' +)}`; +export const PATH_PARAMETERS_REQUIRED_TEXT = 'Path parameters are required for this API.'; +export const REST_API_REFERENCE = 'https://opensearch.org/docs/latest/opensearch/rest-api/index/'; +export const DEFAULT_CLUSTER_METRICS_SCRIPT = { + lang: 'painless', + source: 'ctx.results[0] != null', +}; + +export const API_TYPES = { + CLUSTER_HEALTH: { + type: 'CLUSTER_HEALTH', + documentation: 'https://opensearch.org/docs/latest/opensearch/rest-api/cluster-health/', + exampleText: 'indexAlias1,indexAlias2...', + label: 'Cluster health', + paths: { + withPathParams: '_cluster/health/', + withoutPathParams: '_cluster/health', + }, + get prependText() { + return this.paths.withPathParams || this.paths.withoutPathParams; + }, + appendText: '', + defaultCondition: { + ...DEFAULT_CLUSTER_METRICS_SCRIPT, + source: 'ctx.results[0].status != "green"', + }, + }, + CLUSTER_STATS: { + type: 'CLUSTER_STATS', + documentation: 'https://opensearch.org/docs/latest/opensearch/rest-api/cluster-stats/', + exampleText: 'nodeFilter1,nodeFilter2...', + label: 'Cluster stats', + paths: { + withPathParams: '_cluster/stats/nodes/', + withoutPathParams: '_cluster/stats', + }, + get prependText() { + return this.paths.withPathParams || this.paths.withoutPathParams; + }, + appendText: '', + defaultCondition: { + ...DEFAULT_CLUSTER_METRICS_SCRIPT, + source: 'ctx.results[0].indices.count <= 0', + }, + }, + CLUSTER_SETTINGS: { + type: 'CLUSTER_SETTINGS', + documentation: 'https://opensearch.org/docs/latest/opensearch/rest-api/cluster-settings/', + exampleText: undefined, + label: 'Cluster settings', + paths: { + withPathParams: undefined, + withoutPathParams: '_cluster/settings', + }, + get prependText() { + return this.paths.withPathParams || this.paths.withoutPathParams; + }, + appendText: '', + defaultCondition: { + ...DEFAULT_CLUSTER_METRICS_SCRIPT, + source: 'ctx.results[0].transient != null', + }, + }, + NODES_STATS: { + type: 'NODES_STATS', + documentation: 'https://opensearch.org/docs/latest/opensearch/popular-api/#get-node-statistics', + exampleText: undefined, + label: 'Nodes stats', + paths: { + withPathParams: undefined, + withoutPathParams: '_nodes/stats', + }, + get prependText() { + return this.paths.withPathParams || this.paths.withoutPathParams; + }, + appendText: '', + defaultCondition: { + ...DEFAULT_CLUSTER_METRICS_SCRIPT, + source: 'ctx.results[0].nodes.NODE_ID.jvm.mem.heap_used_percent > 60', + }, + }, + CAT_PENDING_TASKS: { + type: 'CAT_PENDING_TASKS', + documentation: 'https://opensearch.org/docs/latest/opensearch/rest-api/cat/cat-pending-tasks/', + exampleText: undefined, + label: 'List pending tasks', + paths: { + withPathParams: undefined, + withoutPathParams: '_cat/pending_tasks', + }, + get prependText() { + return this.paths.withPathParams || this.paths.withoutPathParams; + }, + appendText: '', + defaultCondition: { + ...DEFAULT_CLUSTER_METRICS_SCRIPT, + source: 'ctx.results[0].tasks.size() >= 0', + }, + }, + CAT_RECOVERY: { + type: 'CAT_RECOVERY', + documentation: 'https://opensearch.org/docs/latest/opensearch/rest-api/cat/cat-recovery/', + exampleText: 'index1,index2...', + label: 'Recovery', + paths: { + withPathParams: '_cat/recovery/', + withoutPathParams: '_cat/recovery', + }, + get prependText() { + return this.paths.withPathParams || this.paths.withoutPathParams; + }, + appendText: '', + defaultCondition: { + ...DEFAULT_CLUSTER_METRICS_SCRIPT, + source: 'ctx.results[0].INDEX_NAME.shards.length <= 0', + }, + }, + CAT_SNAPSHOTS: { + type: 'CAT_SNAPSHOTS', + documentation: 'https://opensearch.org/docs/latest/opensearch/rest-api/cat/cat-snapshots/', + exampleText: 'repositoryName', + label: 'List snapshots', + paths: { + withPathParams: '_cat/snapshots/', + withoutPathParams: undefined, + }, + get prependText() { + return this.paths.withPathParams || this.paths.withoutPathParams; + }, + appendText: '', + defaultCondition: { + ...DEFAULT_CLUSTER_METRICS_SCRIPT, + source: 'ctx.results[0].SNAPSHOT_ID.status == "FAILED"', + }, + }, + CAT_TASKS: { + type: 'CAT_TASKS', + documentation: 'https://opensearch.org/docs/latest/opensearch/rest-api/cat/cat-tasks/', + exampleText: undefined, + label: 'List tasks', + paths: { + withPathParams: undefined, + withoutPathParams: '_cat/tasks', + }, + get prependText() { + return this.paths.withPathParams || this.paths.withoutPathParams; + }, + appendText: '', + defaultCondition: { + ...DEFAULT_CLUSTER_METRICS_SCRIPT, + source: 'ctx.results[0].tasks.length > 0', + }, + }, +}; diff --git a/public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers.js b/public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers.js new file mode 100644 index 000000000..929f64aad --- /dev/null +++ b/public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers.js @@ -0,0 +1,118 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import _ from 'lodash'; +import { formikToClusterMetricsInput } from '../../../containers/CreateMonitor/utils/formikToMonitor'; +import { + DEFAULT_CLUSTER_METRICS_SCRIPT, + ILLEGAL_PATH_PARAMETER_CHARACTERS, + PATH_PARAMETER_ILLEGAL_CHARACTER_TEXT, + PATH_PARAMETERS_REQUIRED_TEXT, + API_TYPES, + NO_PATH_PARAMS_PLACEHOLDER_TEXT, + GET_API_TYPE_DEBUG_TEXT, +} from './clusterMetricsMonitorConstants'; +import { SEARCH_TYPE } from '../../../../../utils/constants'; +import { FORMIK_INITIAL_TRIGGER_VALUES } from '../../../../CreateTrigger/containers/CreateTrigger/utils/constants'; +import { FORMIK_INITIAL_VALUES } from '../../../containers/CreateMonitor/utils/constants'; + +export function buildClusterMetricsRequest(values) { + return _.get(formikToClusterMetricsInput(values), 'uri'); +} + +export const canExecuteClusterMetricsMonitor = (uri = {}) => { + const { + api_type = FORMIK_INITIAL_VALUES.uri.api_type, + path_params = FORMIK_INITIAL_VALUES.uri.path_params, + } = uri; + if (_.isEmpty(api_type)) return false; + const requiresPathParams = _.isEmpty(_.get(API_TYPES, `${api_type}.paths.withoutPathParams`)); + const hasPathParams = !_.isEmpty(path_params); + if (pathParamsContainIllegalCharacters(path_params)) return false; + return requiresPathParams ? hasPathParams : true; +}; + +export const pathParamsContainIllegalCharacters = (pathParams) => { + if (_.isEmpty(pathParams)) return false; + const foundIllegalCharacters = ILLEGAL_PATH_PARAMETER_CHARACTERS.find((illegalCharacter) => + _.includes(pathParams, illegalCharacter) + ); + return !_.isEmpty(foundIllegalCharacters); +}; + +export const getApiPath = (hasPathParams = false, apiType) => { + let path = hasPathParams + ? _.get( + API_TYPES, + `${apiType}.paths.withPathParams`, + _.get(API_TYPES, `${apiType}.paths.withoutPathParams`) + ) + : _.get(API_TYPES, `${apiType}.paths.withoutPathParams`); + return path || FORMIK_INITIAL_VALUES.uri.path; +}; + +export const getApiTypesRequiringPathParams = () => { + const apiList = []; + _.keys(API_TYPES).forEach((api) => { + const withoutPathParams = _.get(API_TYPES, `${api}.paths.withoutPathParams`, ''); + if (_.isEmpty(withoutPathParams)) + apiList.push({ + value: _.get(API_TYPES, `${api}.type`), + label: _.get(API_TYPES, `${api}.label`), + }); + }); + return apiList; +}; + +export const getDefaultScript = (monitorValues) => { + const searchType = _.get(monitorValues, 'searchType', FORMIK_INITIAL_VALUES.searchType); + switch (searchType) { + case SEARCH_TYPE.CLUSTER_METRICS: + const apiType = _.get(monitorValues, 'uri.api_type'); + return _.get(API_TYPES, `${apiType}.defaultCondition`, DEFAULT_CLUSTER_METRICS_SCRIPT); + default: + return FORMIK_INITIAL_TRIGGER_VALUES.script; + } +}; + +export const getExamplePathParams = (apiType) => { + if (_.isEmpty(apiType)) return NO_PATH_PARAMS_PLACEHOLDER_TEXT; + let exampleText = _.get(API_TYPES, `${apiType}.exampleText`, ''); + _.isEmpty(exampleText) + ? (exampleText = NO_PATH_PARAMS_PLACEHOLDER_TEXT) + : (exampleText = `e.g., ${exampleText}`); + return exampleText; +}; + +export const getApiType = (uri) => { + let apiType = ''; + const path = _.get(uri, 'path'); + if (_.isEmpty(path)) return apiType; + _.keys(API_TYPES).forEach((apiTypeKey) => { + const withPathParams = _.get(API_TYPES, `${apiTypeKey}.paths.withPathParams`); + const withoutPathParams = _.get(API_TYPES, `${apiTypeKey}.paths.withoutPathParams`); + if (path === withPathParams || path === withoutPathParams) apiType = apiTypeKey; + }); + if (_.isEmpty(apiType)) console.debug(GET_API_TYPE_DEBUG_TEXT); + return apiType; +}; + +export const isInvalidApiPathParameter = (field, hidePathParams, pathParams, requirePathParams) => { + if (hidePathParams) return false; + const pathParamsTouched = _.get(field, 'touched.uri.path_params', false); + if (pathParamsTouched) { + if (requirePathParams && _.isEmpty(pathParams)) return true; + return pathParamsContainIllegalCharacters(pathParams); + } else return pathParamsTouched; +}; + +export const validateApiPathParameter = (field, hidePathParams, pathParams, requirePathParams) => { + if (hidePathParams) return NO_PATH_PARAMS_PLACEHOLDER_TEXT; + const pathParamsTouched = _.get(field, 'touched.uri.path_params', false); + if (requirePathParams && pathParamsTouched && _.isEmpty(pathParams)) + return PATH_PARAMETERS_REQUIRED_TEXT; + if (isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams)) + return PATH_PARAMETER_ILLEGAL_CHARACTER_TEXT; +}; diff --git a/public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers.test.js b/public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers.test.js new file mode 100644 index 000000000..f2c2cf35f --- /dev/null +++ b/public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers.test.js @@ -0,0 +1,940 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import _ from 'lodash'; +import { + API_TYPES, + DEFAULT_CLUSTER_METRICS_SCRIPT, + GET_API_TYPE_DEBUG_TEXT, + ILLEGAL_PATH_PARAMETER_CHARACTERS, + NO_PATH_PARAMS_PLACEHOLDER_TEXT, + PATH_PARAMETER_ILLEGAL_CHARACTER_TEXT, + PATH_PARAMETERS_REQUIRED_TEXT, +} from './clusterMetricsMonitorConstants'; +import { + buildClusterMetricsRequest, + canExecuteClusterMetricsMonitor, + getApiPath, + getApiType, + getApiTypesRequiringPathParams, + getDefaultScript, + getExamplePathParams, + isInvalidApiPathParameter, + pathParamsContainIllegalCharacters, + validateApiPathParameter, +} from './clusterMetricsMonitorHelpers'; +import { FORMIK_INITIAL_VALUES } from '../../../containers/CreateMonitor/utils/constants'; +import { SEARCH_TYPE } from '../../../../../utils/constants'; +import { FORMIK_INITIAL_TRIGGER_VALUES } from '../../../../CreateTrigger/containers/CreateTrigger/utils/constants'; + +describe('clusterMetricsMonitorHelpers', () => { + describe('buildClusterMetricsRequest', () => { + test('when all fields are blank', () => { + const values = { + uri: { + api_type: '', + path: '', + path_params: '', + url: '', + }, + }; + expect(buildClusterMetricsRequest(values)).toEqual(FORMIK_INITIAL_VALUES.uri); + }); + + test('when all fields are undefined', () => { + const values = { + uri: { + api_type: undefined, + path: undefined, + path_params: undefined, + url: undefined, + }, + }; + expect(buildClusterMetricsRequest(values)).toEqual(FORMIK_INITIAL_VALUES.uri); + }); + + describe('when api_type is blank', () => { + test('path is provided', () => { + const path = API_TYPES[API_TYPES.CLUSTER_HEALTH.type].paths.withoutPathParams; + const values = { + uri: { + api_type: '', + path: path, + path_params: '', + url: '', + }, + }; + const expectedResult = { + uri: { + api_type: API_TYPES.CLUSTER_HEALTH.type, + path: path, + path_params: '', + url: `http://localhost:9200/${path}`, + }, + }; + expect(buildClusterMetricsRequest(values)).toEqual(expectedResult.uri); + }); + + test('path and path_params are provided', () => { + const path = API_TYPES[API_TYPES.CLUSTER_HEALTH.type].paths.withoutPathParams; + const pathParams = 'params'; + const values = { + uri: { + api_type: '', + path: path, + path_params: pathParams, + url: '', + }, + }; + const expectedResult = { + uri: { + api_type: API_TYPES.CLUSTER_HEALTH.type, + path: path, + path_params: pathParams, + url: `http://localhost:9200/${path}${pathParams}`, + }, + }; + expect(buildClusterMetricsRequest(values)).toEqual(expectedResult.uri); + }); + }); + }); + + describe('canExecuteClusterMetricsMonitor', () => { + test('with blank apiType and other fields defined', () => { + const testUri = { + ...FORMIK_INITIAL_VALUES.uri, + api_type: '', + path: '_cluster/health/', + path_params: 'params', + }; + expect(canExecuteClusterMetricsMonitor(testUri)).toEqual(false); + }); + test('with blank apiType and other fields empty', () => { + const testUri = { + ...FORMIK_INITIAL_VALUES.uri, + api_type: '', + }; + expect(canExecuteClusterMetricsMonitor(testUri)).toEqual(false); + }); + test('with undefined apiType and other fields defined', () => { + const testUri = { + ...FORMIK_INITIAL_VALUES.uri, + api_type: undefined, + path: '_cluster/health/', + path_params: 'params', + }; + expect(canExecuteClusterMetricsMonitor(testUri)).toEqual(false); + }); + test('with undefined apiType and other fields empty', () => { + const testUri = { + ...FORMIK_INITIAL_VALUES.uri, + api_type: undefined, + }; + expect(canExecuteClusterMetricsMonitor(testUri)).toEqual(false); + }); + test('with pathParams when apiType requires pathParams', () => { + const testUri = { + ...FORMIK_INITIAL_VALUES.uri, + api_type: 'CAT_SNAPSHOTS', + path_params: 'params', + }; + expect(canExecuteClusterMetricsMonitor(testUri)).toEqual(true); + }); + test('with pathParams containing illegal characters when apiType requires pathParams', () => { + const testUri = { + ...FORMIK_INITIAL_VALUES.uri, + api_type: 'CAT_SNAPSHOTS', + path_params: 'par?ams', + }; + expect(canExecuteClusterMetricsMonitor(testUri)).toEqual(false); + }); + test('with blank pathParams when apiType requires pathParams', () => { + const testUri = { + ...FORMIK_INITIAL_VALUES.uri, + api_type: 'CAT_SNAPSHOTS', + path_params: '', + }; + expect(canExecuteClusterMetricsMonitor(testUri)).toEqual(false); + }); + test('with undefined pathParams when apiType requires pathParams', () => { + const testUri = { + ...FORMIK_INITIAL_VALUES.uri, + api_type: 'CAT_SNAPSHOTS', + path_params: undefined, + }; + expect(canExecuteClusterMetricsMonitor(testUri)).toEqual(false); + }); + + test('with pathParams when apiType supports pathParams', () => { + const testUri = { + ...FORMIK_INITIAL_VALUES.uri, + api_type: 'CLUSTER_HEALTH', + path_params: 'params', + }; + expect(canExecuteClusterMetricsMonitor(testUri)).toEqual(true); + }); + test('with pathParams containing illegal characters when apiType supports pathParams', () => { + const testUri = { + ...FORMIK_INITIAL_VALUES.uri, + api_type: 'CLUSTER_HEALTH', + path_params: 'par?ams', + }; + expect(canExecuteClusterMetricsMonitor(testUri)).toEqual(false); + }); + test('with blank pathParams when apiType supports pathParams', () => { + const testUri = { + ...FORMIK_INITIAL_VALUES.uri, + api_type: 'CLUSTER_HEALTH', + path_params: '', + }; + expect(canExecuteClusterMetricsMonitor(testUri)).toEqual(true); + }); + test('with undefined pathParams when apiType supports pathParams', () => { + const testUri = { + ...FORMIK_INITIAL_VALUES.uri, + api_type: 'CLUSTER_HEALTH', + path_params: undefined, + }; + expect(canExecuteClusterMetricsMonitor(testUri)).toEqual(true); + }); + }); + + describe('pathParamsContainIllegalCharacters', () => { + test('with blank pathParams', () => { + const testPathParams = ''; + expect(pathParamsContainIllegalCharacters(testPathParams)).toEqual(false); + }); + test('with undefined pathParams', () => { + const testPathParams = undefined; + expect(pathParamsContainIllegalCharacters(testPathParams)).toEqual(false); + }); + test('with pathParams containing illegal characters', () => { + ILLEGAL_PATH_PARAMETER_CHARACTERS.forEach((char) => { + const testPathParams = `par${char}ams`; + expect(pathParamsContainIllegalCharacters(testPathParams)).toEqual(true); + }); + }); + test('with valid pathParams', () => { + const testPathParams = 'params'; + expect(pathParamsContainIllegalCharacters(testPathParams)).toEqual(false); + }); + }); + + describe('getApiPath', () => { + describe('when hasPathParams is FALSE', () => { + test('apiType is blank', () => { + const apiType = ''; + expect(getApiPath(false, apiType)).toEqual(FORMIK_INITIAL_VALUES.uri.path); + }); + test('apiType is undefined', () => { + const apiType = undefined; + expect(getApiPath(false, apiType)).toEqual(FORMIK_INITIAL_VALUES.uri.path); + }); + }); + + describe('when hasPathParams is TRUE', () => { + test('apiType is blank', () => { + const apiType = ''; + expect(getApiPath(true, apiType)).toEqual(FORMIK_INITIAL_VALUES.uri.path); + }); + test('apiType is undefined', () => { + const apiType = undefined; + expect(getApiPath(true, apiType)).toEqual(FORMIK_INITIAL_VALUES.uri.path); + }); + }); + + _.keys(API_TYPES).forEach((apiType) => { + test(`for ${apiType} when hasPathParams is FALSE`, () => { + const withoutPathParams = _.get(API_TYPES, `${apiType}.paths.withoutPathParams`, ''); + expect(getApiPath(false, apiType)).toEqual(withoutPathParams); + }); + test(`for ${apiType} when hasPathParams is TRUE`, () => { + const withPathParams = _.get( + API_TYPES, + `${apiType}.paths.withPathParams`, + _.get(API_TYPES, `${apiType}.paths.withoutPathParams`) + ); + expect(getApiPath(true, apiType)).toEqual(withPathParams); + }); + }); + }); + + describe('getApiTypesRequiringPathParams', () => { + const expectedApiTypes = [API_TYPES.CAT_SNAPSHOTS.type]; + test('returns expected apiTypes', () => { + const results = getApiTypesRequiringPathParams(); + expect(results.length).toEqual(expectedApiTypes.length); + results.forEach((entry) => { + const entryExpected = _.includes(expectedApiTypes, entry.value); + expect(entryExpected).toEqual(true); + }); + }); + }); + + describe('getDefaultScript', () => { + test('when searchType is undefined', () => { + const monitorValues = undefined; + expect(getDefaultScript(monitorValues)).toEqual(FORMIK_INITIAL_TRIGGER_VALUES.script); + }); + test('when searchType is clusterMetrics and api_type is undefined', () => { + const monitorValues = { + searchType: SEARCH_TYPE.CLUSTER_METRICS, + uri: undefined, + }; + expect(getDefaultScript(monitorValues)).toEqual(DEFAULT_CLUSTER_METRICS_SCRIPT); + }); + test('when searchType is clusterMetrics and api_type is empty', () => { + const monitorValues = { + searchType: SEARCH_TYPE.CLUSTER_METRICS, + uri: { + api_type: '', + }, + }; + expect(getDefaultScript(monitorValues)).toEqual(DEFAULT_CLUSTER_METRICS_SCRIPT); + }); + test('when searchType is clusterMetrics and api_type does not have a default condition', () => { + const monitorValues = { + searchType: SEARCH_TYPE.CLUSTER_METRICS, + uri: { + api_type: 'unknownApi', + }, + }; + expect(getDefaultScript(monitorValues)).toEqual(DEFAULT_CLUSTER_METRICS_SCRIPT); + }); + + _.keys(SEARCH_TYPE).forEach((searchType) => { + test(`when searchType is ${searchType}`, () => { + if (SEARCH_TYPE[searchType] !== SEARCH_TYPE.CLUSTER_METRICS) { + const monitorValues = { searchType: searchType }; + expect(getDefaultScript(monitorValues)).toEqual(FORMIK_INITIAL_TRIGGER_VALUES.script); + } + }); + }); + + _.keys(API_TYPES).forEach((apiType) => { + test(`when searchType is clusterMetrics and api_type is ${apiType}`, () => { + const monitorValues = { + searchType: SEARCH_TYPE.CLUSTER_METRICS, + uri: { + api_type: apiType, + }, + }; + const expectedOutput = _.get(API_TYPES, `${apiType}.defaultCondition`); + if (!_.isEmpty(expectedOutput)) + expect(getDefaultScript(monitorValues)).toEqual(expectedOutput); + }); + }); + }); + + describe('getExamplePathParams', () => { + test('when apiType has no example text', () => { + const apiType = 'apiTypeWithoutExampleText'; + expect(getExamplePathParams(apiType)).toEqual(NO_PATH_PARAMS_PLACEHOLDER_TEXT); + }); + test('when apiType is blank', () => { + const apiType = ''; + expect(getExamplePathParams(apiType)).toEqual(NO_PATH_PARAMS_PLACEHOLDER_TEXT); + }); + test('when apiType is undefined', () => { + const apiType = undefined; + expect(getExamplePathParams(apiType)).toEqual(NO_PATH_PARAMS_PLACEHOLDER_TEXT); + }); + _.keys(API_TYPES).forEach((apiType) => { + const withPathParams = _.get(API_TYPES, `${apiType}.paths.withPathParams`); + const supportsPathParams = !_.isEmpty(withPathParams); + test(`when apiType is ${apiType}`, () => { + const expectedResults = supportsPathParams + ? `e.g., ${API_TYPES[apiType].exampleText}` + : NO_PATH_PARAMS_PLACEHOLDER_TEXT; + expect(getExamplePathParams(apiType)).toEqual(expectedResults); + }); + }); + }); + + describe('getApiType', () => { + test(`when uri.path is blank`, () => { + const uri = { + ...FORMIK_INITIAL_VALUES.uri, + path: '', + }; + expect(getApiType(uri)).toEqual(''); + }); + test(`when uri.path is blank and has path params`, () => { + const uri = { + ...FORMIK_INITIAL_VALUES.uri, + path: '', + path_params: 'params', + }; + expect(getApiType(uri)).toEqual(''); + }); + + test(`when uri.path is undefined`, () => { + const uri = { + ...FORMIK_INITIAL_VALUES.uri, + path: undefined, + }; + expect(getApiType(uri)).toEqual(''); + }); + test(`when uri.path is undefined and has path params`, () => { + const uri = { + ...FORMIK_INITIAL_VALUES.uri, + path: undefined, + path_params: 'params', + }; + expect(getApiType(uri)).toEqual(''); + }); + + test(`when uri.path is unsupported`, () => { + const uri = { + ...FORMIK_INITIAL_VALUES.uri, + path: '_unsupported/api', + }; + console.debug = jest.fn(); + expect(getApiType(uri)).toEqual(''); + expect(console.debug).toHaveBeenCalledWith(GET_API_TYPE_DEBUG_TEXT); + }); + test(`when uri.path is unsupported and has path params`, () => { + const uri = { + ...FORMIK_INITIAL_VALUES.uri, + path: '_unsupported/api', + path_params: 'params', + }; + console.debug = jest.fn(); + expect(getApiType(uri)).toEqual(''); + expect(console.debug).toHaveBeenCalledWith(GET_API_TYPE_DEBUG_TEXT); + }); + + _.keys(API_TYPES).forEach((apiType) => { + const withPathParams = _.get(API_TYPES, `${apiType}.paths.withPathParams`); + if (!_.isEmpty(withPathParams)) { + test(`when apiType is ${apiType} and has path params`, () => { + const uri = { + ...FORMIK_INITIAL_VALUES.uri, + path: withPathParams, + path_params: 'params', + }; + expect(getApiType(uri)).toEqual(apiType); + }); + } + + const withoutPathParams = _.get(API_TYPES, `${apiType}.paths.withoutPathParams`); + if (!_.isEmpty(withoutPathParams)) { + test(`when apiType is ${apiType} and has no path params`, () => { + const uri = { + ...FORMIK_INITIAL_VALUES.uri, + path: withoutPathParams, + }; + expect(getApiType(uri)).toEqual(apiType); + }); + } + }); + }); + + describe('isInvalidApiPathParameter', () => { + describe('when hidePathParams is TRUE', () => { + const hidePathParams = true; + describe('pathParams fieldTouched is TRUE', () => { + const field = _.set({}, 'touched.uri.path_params', true); + describe('pathParams are required', () => { + const requirePathParams = true; + test('pathParams are blank', () => { + const pathParams = ''; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + test('pathParams are undefined', () => { + const pathParams = undefined; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + test('pathParams contain illegal character', () => { + ILLEGAL_PATH_PARAMETER_CHARACTERS.forEach((char) => { + const pathParams = `par${char}ams`; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + }); + test('pathParams are valid', () => { + const pathParams = 'params'; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + }); + describe('pathParams are not required', () => { + const requirePathParams = false; + test('pathParams are blank', () => { + const pathParams = ''; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + test('pathParams are undefined', () => { + const pathParams = undefined; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + test('pathParams contain illegal character', () => { + ILLEGAL_PATH_PARAMETER_CHARACTERS.forEach((char) => { + const pathParams = `par${char}ams`; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + }); + test('pathParams are valid', () => { + const pathParams = 'params'; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + }); + }); + describe('pathParams fieldTouched is false', () => { + const field = _.set({}, 'touched.uri.path_params', false); + describe('pathParams are required', () => { + const requirePathParams = true; + test('pathParams are blank', () => { + const pathParams = ''; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + test('pathParams are undefined', () => { + const pathParams = undefined; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + test('pathParams contain illegal character', () => { + ILLEGAL_PATH_PARAMETER_CHARACTERS.forEach((char) => { + const pathParams = `par${char}ams`; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + }); + test('pathParams are valid', () => { + const pathParams = 'params'; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + }); + describe('pathParams are not required', () => { + const requirePathParams = false; + test('pathParams are blank', () => { + const pathParams = ''; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + test('pathParams are undefined', () => { + const pathParams = undefined; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + test('pathParams contain illegal character', () => { + ILLEGAL_PATH_PARAMETER_CHARACTERS.forEach((char) => { + const pathParams = `par${char}ams`; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + }); + test('pathParams are valid', () => { + const pathParams = 'params'; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + }); + }); + }); + describe('when hidePathParams is FALSE', () => { + const hidePathParams = false; + describe('pathParams fieldTouched is TRUE', () => { + const field = _.set({}, 'touched.uri.path_params', true); + describe('pathParams are required', () => { + const requirePathParams = true; + test('pathParams are blank', () => { + const pathParams = ''; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(true); + }); + test('pathParams are undefined', () => { + const pathParams = undefined; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(true); + }); + test('pathParams contain illegal character', () => { + ILLEGAL_PATH_PARAMETER_CHARACTERS.forEach((char) => { + const pathParams = `par${char}ams`; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(true); + }); + }); + test('pathParams are valid', () => { + const pathParams = 'params'; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + }); + describe('pathParams are not required', () => { + const requirePathParams = false; + test('pathParams are blank', () => { + const pathParams = ''; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + test('pathParams are undefined', () => { + const pathParams = undefined; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + test('pathParams contain illegal character', () => { + ILLEGAL_PATH_PARAMETER_CHARACTERS.forEach((char) => { + const pathParams = `par${char}ams`; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(true); + }); + }); + test('pathParams are valid', () => { + const pathParams = 'params'; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + }); + }); + describe('pathParams fieldTouched is false', () => { + const field = _.set({}, 'touched.uri.path_params', false); + describe('pathParams are required', () => { + const requirePathParams = true; + test('pathParams are blank', () => { + const pathParams = ''; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + test('pathParams are undefined', () => { + const pathParams = undefined; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + test('pathParams contain illegal character', () => { + ILLEGAL_PATH_PARAMETER_CHARACTERS.forEach((char) => { + const pathParams = `par${char}ams`; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + }); + test('pathParams are valid', () => { + const pathParams = 'params'; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + }); + describe('pathParams are not required', () => { + const requirePathParams = false; + test('pathParams are blank', () => { + const pathParams = ''; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + test('pathParams are undefined', () => { + const pathParams = undefined; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + test('pathParams contain illegal character', () => { + ILLEGAL_PATH_PARAMETER_CHARACTERS.forEach((char) => { + const pathParams = `par${char}ams`; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + }); + test('pathParams are valid', () => { + const pathParams = 'params'; + expect( + isInvalidApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(false); + }); + }); + }); + }); + }); + + describe('validateApiPathParameter', () => { + describe('when hidePathParams is TRUE', () => { + const hidePathParams = true; + describe('pathParams fieldTouched is TRUE', () => { + const field = _.set({}, 'touched.uri.path_params', true); + describe('pathParams are required', () => { + const requirePathParams = true; + test('pathParams are blank', () => { + const pathParams = ''; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(NO_PATH_PARAMS_PLACEHOLDER_TEXT); + }); + test('pathParams are undefined', () => { + const pathParams = undefined; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(NO_PATH_PARAMS_PLACEHOLDER_TEXT); + }); + test('pathParams contain illegal character', () => { + ILLEGAL_PATH_PARAMETER_CHARACTERS.forEach((char) => { + const pathParams = `par${char}ams`; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(NO_PATH_PARAMS_PLACEHOLDER_TEXT); + }); + }); + test('pathParams are valid', () => { + const pathParams = 'params'; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(NO_PATH_PARAMS_PLACEHOLDER_TEXT); + }); + }); + describe('pathParams are not required', () => { + const requirePathParams = false; + test('pathParams are blank', () => { + const pathParams = ''; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(NO_PATH_PARAMS_PLACEHOLDER_TEXT); + }); + test('pathParams are undefined', () => { + const pathParams = undefined; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(NO_PATH_PARAMS_PLACEHOLDER_TEXT); + }); + test('pathParams contain illegal character', () => { + ILLEGAL_PATH_PARAMETER_CHARACTERS.forEach((char) => { + const pathParams = `par${char}ams`; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(NO_PATH_PARAMS_PLACEHOLDER_TEXT); + }); + }); + test('pathParams are valid', () => { + const pathParams = 'params'; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(NO_PATH_PARAMS_PLACEHOLDER_TEXT); + }); + }); + }); + describe('pathParams fieldTouched is false', () => { + const field = _.set({}, 'touched.uri.path_params', false); + describe('pathParams are required', () => { + const requirePathParams = true; + test('pathParams are blank', () => { + const pathParams = ''; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(NO_PATH_PARAMS_PLACEHOLDER_TEXT); + }); + test('pathParams are undefined', () => { + const pathParams = undefined; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(NO_PATH_PARAMS_PLACEHOLDER_TEXT); + }); + test('pathParams contain illegal character', () => { + ILLEGAL_PATH_PARAMETER_CHARACTERS.forEach((char) => { + const pathParams = `par${char}ams`; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(NO_PATH_PARAMS_PLACEHOLDER_TEXT); + }); + }); + test('pathParams are valid', () => { + const pathParams = 'params'; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(NO_PATH_PARAMS_PLACEHOLDER_TEXT); + }); + }); + describe('pathParams are not required', () => { + const requirePathParams = false; + test('pathParams are blank', () => { + const pathParams = ''; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(NO_PATH_PARAMS_PLACEHOLDER_TEXT); + }); + test('pathParams are undefined', () => { + const pathParams = undefined; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(NO_PATH_PARAMS_PLACEHOLDER_TEXT); + }); + test('pathParams contain illegal character', () => { + ILLEGAL_PATH_PARAMETER_CHARACTERS.forEach((char) => { + const pathParams = `par${char}ams`; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(NO_PATH_PARAMS_PLACEHOLDER_TEXT); + }); + }); + test('pathParams are valid', () => { + const pathParams = 'params'; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(NO_PATH_PARAMS_PLACEHOLDER_TEXT); + }); + }); + }); + }); + describe('when hidePathParams is FALSE', () => { + const hidePathParams = false; + describe('pathParams fieldTouched is TRUE', () => { + const field = _.set({}, 'touched.uri.path_params', true); + describe('pathParams are required', () => { + const requirePathParams = true; + test('pathParams are blank', () => { + const pathParams = ''; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(PATH_PARAMETERS_REQUIRED_TEXT); + }); + test('pathParams are undefined', () => { + const pathParams = undefined; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(PATH_PARAMETERS_REQUIRED_TEXT); + }); + test('pathParams contain illegal character', () => { + ILLEGAL_PATH_PARAMETER_CHARACTERS.forEach((char) => { + const pathParams = `par${char}ams`; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(PATH_PARAMETER_ILLEGAL_CHARACTER_TEXT); + }); + }); + test('pathParams are valid', () => { + const pathParams = 'params'; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(undefined); + }); + }); + describe('pathParams are not required', () => { + const requirePathParams = false; + test('pathParams are blank', () => { + const pathParams = ''; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(undefined); + }); + test('pathParams are undefined', () => { + const pathParams = undefined; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(undefined); + }); + test('pathParams contain illegal character', () => { + ILLEGAL_PATH_PARAMETER_CHARACTERS.forEach((char) => { + const pathParams = `par${char}ams`; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(PATH_PARAMETER_ILLEGAL_CHARACTER_TEXT); + }); + }); + test('pathParams are valid', () => { + const pathParams = 'params'; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(undefined); + }); + }); + }); + describe('pathParams fieldTouched is false', () => { + const field = _.set({}, 'touched.uri.path_params', false); + describe('pathParams are required', () => { + const requirePathParams = true; + test('pathParams are blank', () => { + const pathParams = ''; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(undefined); + }); + test('pathParams are undefined', () => { + const pathParams = undefined; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(undefined); + }); + test('pathParams contain illegal character', () => { + ILLEGAL_PATH_PARAMETER_CHARACTERS.forEach((char) => { + const pathParams = `par${char}ams`; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(undefined); + }); + }); + test('pathParams are valid', () => { + const pathParams = 'params'; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(undefined); + }); + }); + describe('pathParams are not required', () => { + const requirePathParams = false; + test('pathParams are blank', () => { + const pathParams = ''; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(undefined); + }); + test('pathParams are undefined', () => { + const pathParams = undefined; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(undefined); + }); + test('pathParams contain illegal character', () => { + ILLEGAL_PATH_PARAMETER_CHARACTERS.forEach((char) => { + const pathParams = `par${char}ams`; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(undefined); + }); + }); + test('pathParams are valid', () => { + const pathParams = 'params'; + expect( + validateApiPathParameter(field, hidePathParams, pathParams, requirePathParams) + ).toEqual(undefined); + }); + }); + }); + }); + }); +}); diff --git a/public/pages/CreateMonitor/components/MonitorDefinitionCard/MonitorDefinitionCard.js b/public/pages/CreateMonitor/components/MonitorDefinitionCard/MonitorDefinitionCard.js index 7f686ff56..bbeebfed2 100644 --- a/public/pages/CreateMonitor/components/MonitorDefinitionCard/MonitorDefinitionCard.js +++ b/public/pages/CreateMonitor/components/MonitorDefinitionCard/MonitorDefinitionCard.js @@ -4,15 +4,12 @@ */ import React from 'react'; -import _ from 'lodash'; import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText } from '@elastic/eui'; import FormikCheckableCard from '../../../../components/FormControls/FormikCheckableCard/FormikCheckableCard'; import { OS_AD_PLUGIN, MONITOR_TYPE, SEARCH_TYPE } from '../../../../utils/constants'; -import { MONITOR_TYPE_CARD_WIDTH } from '../MonitorType/MonitorType'; import { URL } from '../../../../../utils/constants'; -const MONITOR_DEFINITION_CARD_WIDTH = - (MONITOR_TYPE_CARD_WIDTH * _.keys(MONITOR_TYPE).length) / _.keys(SEARCH_TYPE).length; +const MONITOR_DEFINITION_CARD_WIDTH = '275'; const onChangeDefinition = (e, form) => { const type = e.target.value; diff --git a/public/pages/CreateMonitor/components/MonitorDefinitionCard/__snapshots__/MonitorDefinitionCard.test.js.snap b/public/pages/CreateMonitor/components/MonitorDefinitionCard/__snapshots__/MonitorDefinitionCard.test.js.snap index 906c9d308..ef4e5e1f9 100644 --- a/public/pages/CreateMonitor/components/MonitorDefinitionCard/__snapshots__/MonitorDefinitionCard.test.js.snap +++ b/public/pages/CreateMonitor/components/MonitorDefinitionCard/__snapshots__/MonitorDefinitionCard.test.js.snap @@ -36,7 +36,7 @@ exports[`MonitorDefinitionCard renders without AD plugin 1`] = ` >
); +const clusterMetricsDescription = ( + + Per cluster metrics monitors allow you to alert based on responses to common REST APIs. + +); + const MonitorType = ({ values }) => ( @@ -43,7 +49,6 @@ const MonitorType = ({ values }) => ( formRow rowProps={{ label: 'Monitor type', - style: { width: `${MONITOR_TYPE_CARD_WIDTH}px` }, }} inputProps={{ id: 'queryLevelMonitorRadioCard', @@ -62,7 +67,6 @@ const MonitorType = ({ values }) => ( ( }} /> + + { + form.setFieldValue('searchType', SEARCH_TYPE.CLUSTER_METRICS); + onChangeDefinition(e, form); + }, + children: clusterMetricsDescription, + 'data-test-subj': 'clusterMetricsMonitorRadioCard', + }} + /> + ); diff --git a/public/pages/CreateMonitor/components/MonitorType/__snapshots__/MonitorType.test.js.snap b/public/pages/CreateMonitor/components/MonitorType/__snapshots__/MonitorType.test.js.snap index 97be8378c..290af85b3 100644 --- a/public/pages/CreateMonitor/components/MonitorType/__snapshots__/MonitorType.test.js.snap +++ b/public/pages/CreateMonitor/components/MonitorType/__snapshots__/MonitorType.test.js.snap @@ -10,7 +10,6 @@ exports[`MonitorType renders 1`] = `
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+ Per cluster metrics monitors allow you to alert based on responses to common REST APIs. +
+
+
+
+
+
+
+
`; diff --git a/public/pages/CreateMonitor/containers/AnomalyDetectors/__tests__/__snapshots__/AnomalyDetector.test.js.snap b/public/pages/CreateMonitor/containers/AnomalyDetectors/__tests__/__snapshots__/AnomalyDetector.test.js.snap index 741d3566c..83fd74f79 100644 --- a/public/pages/CreateMonitor/containers/AnomalyDetectors/__tests__/__snapshots__/AnomalyDetector.test.js.snap +++ b/public/pages/CreateMonitor/containers/AnomalyDetectors/__tests__/__snapshots__/AnomalyDetector.test.js.snap @@ -43,6 +43,12 @@ exports[`AnomalyDetectors renders 1`] = ` "searchType": "graph", "timeField": "", "timezone": Array [], + "uri": Object { + "api_type": "", + "path": "", + "path_params": "", + "url": "", + }, "weekly": Object { "fri": false, "mon": false, @@ -105,6 +111,12 @@ exports[`AnomalyDetectors renders 1`] = ` "searchType": "graph", "timeField": "", "timezone": Array [], + "uri": Object { + "api_type": "", + "path": "", + "path_params": "", + "url": "", + }, "weekly": Object { "fri": false, "mon": false, @@ -228,6 +240,12 @@ exports[`AnomalyDetectors renders 1`] = ` "searchType": "graph", "timeField": "", "timezone": Array [], + "uri": Object { + "api_type": "", + "path": "", + "path_params": "", + "url": "", + }, "weekly": Object { "fri": false, "mon": false, @@ -309,6 +327,12 @@ exports[`AnomalyDetectors renders 1`] = ` "searchType": "graph", "timeField": "", "timezone": Array [], + "uri": Object { + "api_type": "", + "path": "", + "path_params": "", + "url": "", + }, "weekly": Object { "fri": false, "mon": false, @@ -438,6 +462,12 @@ exports[`AnomalyDetectors renders 1`] = ` "searchType": "graph", "timeField": "", "timezone": Array [], + "uri": Object { + "api_type": "", + "path": "", + "path_params": "", + "url": "", + }, "weekly": Object { "fri": false, "mon": false, @@ -519,6 +549,12 @@ exports[`AnomalyDetectors renders 1`] = ` "searchType": "graph", "timeField": "", "timezone": Array [], + "uri": Object { + "api_type": "", + "path": "", + "path_params": "", + "url": "", + }, "weekly": Object { "fri": false, "mon": false, diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js b/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js index 63e4a3050..70921780f 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js @@ -152,6 +152,7 @@ export default class CreateMonitor extends Component { let triggerType; switch (monitor_type) { case MONITOR_TYPE.QUERY_LEVEL: + case MONITOR_TYPE.CLUSTER_METRICS: triggerType = TRIGGER_TYPE.QUERY_LEVEL; break; case MONITOR_TYPE.BUCKET_LEVEL: diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap b/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap index 2edf2b131..f62889ba0 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap +++ b/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap @@ -51,6 +51,12 @@ exports[`CreateMonitor renders 1`] = ` "searchType": "graph", "timeField": "", "timezone": Array [], + "uri": Object { + "api_type": "", + "path": "", + "path_params": "", + "url": "", + }, "weekly": Object { "fri": false, "mon": false, diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/__snapshots__/formikToMonitor.test.js.snap b/public/pages/CreateMonitor/containers/CreateMonitor/utils/__snapshots__/formikToMonitor.test.js.snap index a898d4d74..250e4a6da 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/__snapshots__/formikToMonitor.test.js.snap +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/__snapshots__/formikToMonitor.test.js.snap @@ -45,6 +45,28 @@ Object { } `; +exports[`formikToClusterMetricsUri can build a ClusterMetricsMonitor request with path params 1`] = ` +Object { + "uri": Object { + "api_type": "", + "path": "params", + "path_params": "", + "url": "", + }, +} +`; + +exports[`formikToClusterMetricsUri can build a ClusterMetricsMonitor request without path params 1`] = ` +Object { + "uri": Object { + "api_type": "CLUSTER_HEALTH", + "path": "_cluster/health", + "path_params": "", + "url": "http://localhost:9200/_cluster/health", + }, +} +`; + exports[`formikToDetector can build detector 1`] = ` Object { "anomaly_detector": Object { @@ -91,6 +113,17 @@ Array [ ] `; +exports[`formikToInputs can call formikToClusterMetricsUri 1`] = ` +Object { + "uri": Object { + "api_type": "", + "path": "", + "path_params": "", + "url": "", + }, +} +`; + exports[`formikToMonitor can build monitor 1`] = ` Object { "enabled": false, diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js index 1536f76c0..c16b6ea42 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js @@ -25,6 +25,12 @@ export const FORMIK_INITIAL_VALUES = { /* DEFINE MONITOR */ monitor_type: MONITOR_TYPE.QUERY_LEVEL, searchType: 'graph', + uri: { + api_type: '', + path: '', + path_params: '', + url: '', + }, index: [], timeField: '', query: MATCH_ALL_QUERY, diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js index 27a7153b9..0c9fc50a5 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js @@ -8,6 +8,11 @@ import moment from 'moment-timezone'; import { BUCKET_COUNT, DEFAULT_COMPOSITE_AGG_SIZE, FORMIK_INITIAL_VALUES } from './constants'; import { MONITOR_TYPE, SEARCH_TYPE } from '../../../../../utils/constants'; import { OPERATORS_QUERY_MAP } from './whereFilters'; +import { API_TYPES } from '../../../components/ClusterMetricsMonitor/utils/clusterMetricsMonitorConstants'; +import { + getApiPath, + getApiType, +} from '../../../components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers'; export function formikToMonitor(values) { const uiSchedule = formikToUiSchedule(values); @@ -31,6 +36,8 @@ export function formikToMonitor(values) { export function formikToInputs(values) { switch (values.searchType) { + case SEARCH_TYPE.CLUSTER_METRICS: + return formikToClusterMetricsInput(values); default: return formikToSearch(values); } @@ -87,6 +94,29 @@ export function formikToAdQuery(values) { }; } +export function formikToClusterMetricsInput(values) { + let apiType = _.get(values, 'uri.api_type', FORMIK_INITIAL_VALUES.uri.api_type); + if (_.isEmpty(apiType)) apiType = getApiType(_.get(values, 'uri')); + let pathParams = _.get(values, 'uri.path_params', FORMIK_INITIAL_VALUES.uri.path_params); + pathParams = _.trim(pathParams); + const hasPathParams = !_.isEmpty(pathParams); + if (hasPathParams) _.concat(pathParams, _.get(API_TYPES, `${apiType}.appendText`, '')); + let path = _.get(values, 'uri.path', FORMIK_INITIAL_VALUES.uri.path); + if (_.isEmpty(path)) path = getApiPath(hasPathParams, apiType); + const canConstructUrl = !_.isEmpty(apiType); + const url = canConstructUrl + ? `http://localhost:9200/${path}${pathParams}` + : FORMIK_INITIAL_VALUES.uri.url; + return { + uri: { + api_type: apiType, + path: path, + path_params: pathParams, + url: url, + }, + }; +} + export function formikToAd(values) { return { anomaly_detector: { diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.test.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.test.js index e598bf555..4edd969cc 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.test.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.test.js @@ -19,6 +19,7 @@ import { formikToWhereClause, formikToAd, formikToInputs, + formikToClusterMetricsInput, } from './formikToMonitor'; import { FORMIK_INITIAL_VALUES } from './constants'; @@ -42,6 +43,14 @@ describe('formikToMonitor', () => { }); }); +describe('formikToInputs', () => { + const formikValues = _.cloneDeep(FORMIK_INITIAL_VALUES); + test('can call formikToClusterMetricsUri', () => { + formikValues.searchType = 'clusterMetrics'; + expect(formikToInputs(formikValues)).toMatchSnapshot(); + }); +}); + describe('formikToDetector', () => { const formikValues = _.cloneDeep(FORMIK_INITIAL_VALUES); formikValues.detectorId = 'temp_detector'; @@ -50,6 +59,20 @@ describe('formikToDetector', () => { }); }); +describe('formikToClusterMetricsUri', () => { + test('can build a ClusterMetricsMonitor request with path params', () => { + const formikValues = _.cloneDeep(FORMIK_INITIAL_VALUES); + formikValues.uri.path = '_cluster/health'; + formikValues.uri.path = 'params'; + expect(formikToClusterMetricsInput(formikValues)).toMatchSnapshot(); + }); + test('can build a ClusterMetricsMonitor request without path params', () => { + const formikValues = _.cloneDeep(FORMIK_INITIAL_VALUES); + formikValues.uri.path = '_cluster/health'; + expect(formikToClusterMetricsInput(formikValues)).toMatchSnapshot(); + }); +}); + describe('formikToUiSearch', () => { const formikValues = _.cloneDeep(FORMIK_INITIAL_VALUES); formikValues.fieldName = [{ label: 'bytes' }]; diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js index 7fe188e56..c97adf1fe 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js @@ -21,9 +21,10 @@ export default function monitorToFormik(monitor) { } = monitor; // Default searchType to query, because if there is no ui_metadata or search then it was created through API or overwritten by API // In that case we don't want to guess on the UI what selections a user made, so we will default to just showing the extraction query - const { searchType = 'query', fieldName } = search; + let { searchType = 'query', fieldName } = search; + if (_.isEmpty(search) && 'uri' in inputs[0]) searchType = SEARCH_TYPE.CLUSTER_METRICS; const isAD = searchType === SEARCH_TYPE.AD; - + const isClusterMetrics = searchType === SEARCH_TYPE.CLUSTER_METRICS; return { /* INITIALIZE WITH DEFAULTS */ ...formikValues, @@ -44,8 +45,10 @@ export default function monitorToFormik(monitor) { timezone: timezone ? [{ label: timezone }] : [], detectorId: isAD ? _.get(inputs, INPUTS_DETECTOR_ID) : undefined, - index: inputs[0].search.indices.map((index) => ({ label: index })), - adResultIndex: isAD ? _.get(inputs, '0.search.indices.0') : undefined, - query: JSON.stringify(inputs[0].search.query, null, 4), + index: !isClusterMetrics + ? inputs[0].search.indices.map((index) => ({ label: index })) + : FORMIK_INITIAL_VALUES.index, + query: !isClusterMetrics ? JSON.stringify(inputs[0].search.query, null, 4) : undefined, + uri: isClusterMetrics ? inputs[0].uri : undefined, }; } diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.test.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.test.js index 2faf440f6..d26f84283 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.test.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.test.js @@ -108,6 +108,7 @@ describe('monitorToFormik', () => { const formikValues = monitorToFormik(localExampleMonitor); expect(formikValues.cronExpression).toBe(FORMIK_INITIAL_VALUES.cronExpression); }); + test('can build AD monitor', () => { const adMonitor = _.cloneDeep(exampleMonitor); adMonitor.ui_metadata.search.searchType = 'ad'; @@ -151,4 +152,35 @@ describe('monitorToFormik', () => { expect(formikValues.query).toContain('zIqG0nABwoJjo1UZKHnL'); }); }); + + describe('can build ClusterMetricsMonitor', () => { + test('with path params', () => { + const clusterMetricsMonitor = _.cloneDeep(exampleMonitor); + clusterMetricsMonitor.ui_metadata.search.searchType = 'clusterMetrics'; + clusterMetricsMonitor.inputs = [ + { + uri: { + path: '/_cluster/health', + path_params: 'params', + }, + }, + ]; + const formikValues = monitorToFormik(clusterMetricsMonitor); + expect(formikValues.uri.path).toBe('/_cluster/health'); + expect(formikValues.uri.path_params).toBe('params'); + }); + test('without path params', () => { + const clusterMetricsMonitor = _.cloneDeep(exampleMonitor); + clusterMetricsMonitor.ui_metadata.search.searchType = 'clusterMetrics'; + clusterMetricsMonitor.inputs = [ + { + uri: { + path: '/_cluster/health', + }, + }, + ]; + const formikValues = monitorToFormik(clusterMetricsMonitor); + expect(formikValues.uri.path).toBe('/_cluster/health'); + }); + }); }); diff --git a/public/pages/CreateMonitor/containers/DefineMonitor/DefineMonitor.js b/public/pages/CreateMonitor/containers/DefineMonitor/DefineMonitor.js index eed26d5f7..559b85350 100644 --- a/public/pages/CreateMonitor/containers/DefineMonitor/DefineMonitor.js +++ b/public/pages/CreateMonitor/containers/DefineMonitor/DefineMonitor.js @@ -18,6 +18,14 @@ import { buildSearchRequest } from './utils/searchRequests'; import { SEARCH_TYPE, OS_AD_PLUGIN, MONITOR_TYPE } from '../../../../utils/constants'; import { backendErrorNotification } from '../../../../utils/helpers'; import DataSource from '../DataSource'; +import { + buildClusterMetricsRequest, + getApiType, + getApiTypesRequiringPathParams, +} from '../../components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers'; +import ClusterMetricsMonitor from '../../components/ClusterMetricsMonitor'; +import { FORMIK_INITIAL_VALUES } from '../CreateMonitor/utils/constants'; +import { API_TYPES } from '../../components/ClusterMetricsMonitor/utils/clusterMetricsMonitorConstants'; function renderEmptyMessage(message) { return ( @@ -53,6 +61,7 @@ class DefineMonitor extends Component { response: null, formikSnapshot: this.props.values, plugins: [], + loadingResponse: false, }; this.renderGraph = this.renderGraph.bind(this); @@ -62,8 +71,10 @@ class DefineMonitor extends Component { this.queryMappings = this.queryMappings.bind(this); this.renderVisualMonitor = this.renderVisualMonitor.bind(this); this.renderExtractionQuery = this.renderExtractionQuery.bind(this); + this.renderClusterMetricsMonitor = this.renderClusterMetricsMonitor.bind(this); this.getMonitorContent = this.getMonitorContent.bind(this); this.getPlugins = this.getPlugins.bind(this); + this.getSupportedApiList = this.getSupportedApiList.bind(this); this.showPluginWarning = this.showPluginWarning.bind(this); } @@ -77,6 +88,7 @@ class DefineMonitor extends Component { this.onQueryMappings(); if (hasTimeField) this.onRunQuery(); } + if (searchType === SEARCH_TYPE.CLUSTER_METRICS) this.getSupportedApiList(); } componentDidUpdate(prevProps) { @@ -139,8 +151,10 @@ class DefineMonitor extends Component { this.onRunQuery(); // Reset response when monitor type or definition method is changed - if (prevSearchType !== searchType || prevMonitorType !== monitor_type || groupByCleared) + if (prevSearchType !== searchType || prevMonitorType !== monitor_type || groupByCleared) { this.resetResponse(); + if (searchType === SEARCH_TYPE.CLUSTER_METRICS) this.getSupportedApiList(); + } } async getPlugins() { @@ -216,6 +230,7 @@ class DefineMonitor extends Component { } async onRunQuery() { + this.setState({ loadingResponse: true }); const { httpClient, values, notifications } = this.props; const formikSnapshot = _.cloneDeep(values); @@ -234,6 +249,9 @@ class DefineMonitor extends Component { requests = [buildSearchRequest(values)]; requests.push(buildSearchRequest(values, false)); break; + case SEARCH_TYPE.CLUSTER_METRICS: + requests = [buildClusterMetricsRequest(values)]; + break; } try { @@ -250,6 +268,9 @@ class DefineMonitor extends Component { case SEARCH_TYPE.GRAPH: _.set(monitor, 'inputs[0].search', request); break; + case SEARCH_TYPE.CLUSTER_METRICS: + _.set(monitor, 'inputs[0].uri', request); + break; default: console.log(`Unsupported searchType found: ${JSON.stringify(searchType)}`, searchType); } @@ -276,6 +297,7 @@ class DefineMonitor extends Component { } catch (err) { console.error('There was an error running the query', err); } + this.setState({ loadingResponse: false }); } resetResponse() { @@ -368,11 +390,94 @@ class DefineMonitor extends Component { }; } + renderClusterMetricsMonitor() { + const { values } = this.props; + const { + loadingResponse, + loadingSupportedApiList = false, + response, + supportedApiList, + } = this.state; + return { + content: ( +
+ +
+ ), + }; + } + + async getSupportedApiList() { + this.setState({ loadingSupportedApiList: true }); + const { httpClient, values } = this.props; + const requests = []; + _.keys(API_TYPES).forEach((apiKey) => { + let requiresPathParams = _.get(API_TYPES, `${apiKey}.paths.withoutPathParams`); + requiresPathParams = _.isEmpty(requiresPathParams); + if (!requiresPathParams) { + const path = _.get(API_TYPES, `${apiKey}.paths.withoutPathParams`); + const values = { uri: { ...FORMIK_INITIAL_VALUES.uri, path } }; + requests.push(buildClusterMetricsRequest(values)); + } + }); + + const promises = requests.map((request) => { + const monitor = formikToMonitor(values); + const tempMonitorName = getApiType(request); + _.set(monitor, 'name', tempMonitorName); + _.set(monitor, 'triggers', []); + _.set(monitor, 'inputs[0].uri', request); + return httpClient.post('../api/alerting/monitors/_execute', { + body: JSON.stringify(monitor), + }); + }); + + let supportedApiList = []; + await Promise.all(promises).then((responses) => { + responses.forEach((response) => { + if (response.ok) { + const supportedApi = _.get(response, 'resp.monitor_name'); + supportedApiList.push({ + value: supportedApi, + label: _.get(API_TYPES, `${supportedApi}.label`), + }); + } + }); + }); + + // DefineMonitor::getSupportedApiList attempts to create a list of API for which the user can create monitors. + // It does this by calling all of the feature-supported API without parameters, and adding successful API to a list. + // However, some API require path parameters. The below logic will add those API to the list by default. + // Attempting to create a monitor using one of those API will still throw an exception from the backend if the user + // has configured the OpenSearch-Alerting Plugin supported_json_payloads.json to restrict access to those API. + let clonedSupportedApiList = _.cloneDeep(supportedApiList); + getApiTypesRequiringPathParams().forEach((apiType) => { + if (!supportedApiList.includes(apiType)) clonedSupportedApiList.push(apiType); + }); + clonedSupportedApiList = _.orderBy(clonedSupportedApiList, (api) => api.label); + + this.setState({ + loadingSupportedApiList: false, + supportedApiList: clonedSupportedApiList, + }); + } + getMonitorContent() { const { values } = this.props; switch (values.searchType) { case SEARCH_TYPE.GRAPH: return this.renderVisualMonitor(); + case SEARCH_TYPE.CLUSTER_METRICS: + return this.renderClusterMetricsMonitor(); default: return this.renderExtractionQuery(); } diff --git a/public/pages/CreateMonitor/containers/DefineMonitor/__snapshots__/DefineMonitor.test.js.snap b/public/pages/CreateMonitor/containers/DefineMonitor/__snapshots__/DefineMonitor.test.js.snap index 667ce9e32..ca81b4831 100644 --- a/public/pages/CreateMonitor/containers/DefineMonitor/__snapshots__/DefineMonitor.test.js.snap +++ b/public/pages/CreateMonitor/containers/DefineMonitor/__snapshots__/DefineMonitor.test.js.snap @@ -48,6 +48,12 @@ exports[`DefineMonitor renders 1`] = ` "searchType": "graph", "timeField": "", "timezone": Array [], + "uri": Object { + "api_type": "", + "path": "", + "path_params": "", + "url": "", + }, "weekly": Object { "fri": false, "mon": false, diff --git a/public/pages/CreateMonitor/containers/MonitorDetails/MonitorDetails.js b/public/pages/CreateMonitor/containers/MonitorDetails/MonitorDetails.js index c8599ae36..327b886f5 100644 --- a/public/pages/CreateMonitor/containers/MonitorDetails/MonitorDetails.js +++ b/public/pages/CreateMonitor/containers/MonitorDetails/MonitorDetails.js @@ -12,6 +12,7 @@ import Schedule from '../../components/Schedule'; import MonitorDefinitionCard from '../../components/MonitorDefinitionCard'; import MonitorType from '../../components/MonitorType'; import AnomalyDetectors from '../AnomalyDetectors/AnomalyDetectors'; +import { MONITOR_TYPE } from '../../../../utils/constants'; const renderAnomalyDetector = (httpClient, values, detectorId) => { return { @@ -51,6 +52,7 @@ const MonitorDetails = ({ detectorId, }) => { const anomalyDetectorContent = isAd && renderAnomalyDetector(httpClient, values, detectorId); + const displayMonitorDefinitionCards = values.monitor_type !== MONITOR_TYPE.CLUSTER_METRICS; return ( - - + + {displayMonitorDefinitionCards ? ( +
+ + +
+ ) : null} {isAd ? (
diff --git a/public/pages/CreateMonitor/containers/MonitorIndex/__snapshots__/MonitorIndex.test.js.snap b/public/pages/CreateMonitor/containers/MonitorIndex/__snapshots__/MonitorIndex.test.js.snap index 84940ee25..edc02cc3b 100644 --- a/public/pages/CreateMonitor/containers/MonitorIndex/__snapshots__/MonitorIndex.test.js.snap +++ b/public/pages/CreateMonitor/containers/MonitorIndex/__snapshots__/MonitorIndex.test.js.snap @@ -43,6 +43,12 @@ exports[`MonitorIndex renders 1`] = ` "searchType": "graph", "timeField": "", "timezone": Array [], + "uri": Object { + "api_type": "", + "path": "", + "path_params": "", + "url": "", + }, "weekly": Object { "fri": false, "mon": false, @@ -177,6 +183,12 @@ exports[`MonitorIndex renders 1`] = ` "searchType": "graph", "timeField": "", "timezone": Array [], + "uri": Object { + "api_type": "", + "path": "", + "path_params": "", + "url": "", + }, "weekly": Object { "fri": false, "mon": false, @@ -258,6 +270,12 @@ exports[`MonitorIndex renders 1`] = ` "searchType": "graph", "timeField": "", "timezone": Array [], + "uri": Object { + "api_type": "", + "path": "", + "path_params": "", + "url": "", + }, "weekly": Object { "fri": false, "mon": false, @@ -403,6 +421,12 @@ exports[`MonitorIndex renders 1`] = ` "searchType": "graph", "timeField": "", "timezone": Array [], + "uri": Object { + "api_type": "", + "path": "", + "path_params": "", + "url": "", + }, "weekly": Object { "fri": false, "mon": false, @@ -484,6 +508,12 @@ exports[`MonitorIndex renders 1`] = ` "searchType": "graph", "timeField": "", "timezone": Array [], + "uri": Object { + "api_type": "", + "path": "", + "path_params": "", + "url": "", + }, "weekly": Object { "fri": false, "mon": false, diff --git a/public/pages/CreateTrigger/components/Action/Action.js b/public/pages/CreateTrigger/components/Action/Action.js index 5a5f88077..f8bf7179c 100644 --- a/public/pages/CreateTrigger/components/Action/Action.js +++ b/public/pages/CreateTrigger/components/Action/Action.js @@ -102,6 +102,7 @@ const Action = ({ }, singleSelection: { asPlainText: true }, isClearable: false, + 'data-test-subj': `${fieldPath}actions.${index}_actionDestination`, }} /> diff --git a/public/pages/CreateTrigger/components/Action/utils/validate.js b/public/pages/CreateTrigger/components/Action/utils/validate.js index e8a54a1f8..4bb6ec360 100644 --- a/public/pages/CreateTrigger/components/Action/utils/validate.js +++ b/public/pages/CreateTrigger/components/Action/utils/validate.js @@ -4,10 +4,10 @@ */ export const validateDestination = (destinations) => (value) => { - if (!value) return 'Required'; + if (!value) return 'Required.'; // In case existing destination doesn't exist in list , invalidate the field const destinationMatches = destinations.filter((destination) => destination.value === value); if (destinationMatches.length === 0) { - return 'Required'; + return 'Required.'; } }; diff --git a/public/pages/CreateTrigger/components/AddActionButton/AddActionButton.js b/public/pages/CreateTrigger/components/AddActionButton/AddActionButton.js index 9ac42a238..553d27ae3 100644 --- a/public/pages/CreateTrigger/components/AddActionButton/AddActionButton.js +++ b/public/pages/CreateTrigger/components/AddActionButton/AddActionButton.js @@ -24,6 +24,7 @@ const AddActionButton = ({ arrayHelpers, type = 'slack', numOfActions }) => { ); break; case MONITOR_TYPE.QUERY_LEVEL: + case MONITOR_TYPE.CLUSTER_METRICS: _.set( initialActionValues, 'message_template.source', diff --git a/public/pages/CreateTrigger/components/AddTriggerButton/AddTriggerButton.js b/public/pages/CreateTrigger/components/AddTriggerButton/AddTriggerButton.js index 89d78f47c..856399fa2 100644 --- a/public/pages/CreateTrigger/components/AddTriggerButton/AddTriggerButton.js +++ b/public/pages/CreateTrigger/components/AddTriggerButton/AddTriggerButton.js @@ -8,17 +8,21 @@ import _ from 'lodash'; import { EuiButton } from '@elastic/eui'; import { FORMIK_INITIAL_TRIGGER_VALUES } from '../../containers/CreateTrigger/utils/constants'; -const AddTriggerButton = ({ arrayHelpers, disabled }) => { +const AddTriggerButton = ({ + arrayHelpers, + disabled, + script = FORMIK_INITIAL_TRIGGER_VALUES.script, +}) => { const buttonText = _.get(arrayHelpers, 'form.values.triggerDefinitions', []).length === 0 ? 'Add trigger' : 'Add another trigger'; - + const values = _.cloneDeep({ ...FORMIK_INITIAL_TRIGGER_VALUES, script }); return ( arrayHelpers.push(_.cloneDeep(FORMIK_INITIAL_TRIGGER_VALUES))} + onClick={() => arrayHelpers.push(values)} disabled={disabled} > {buttonText} diff --git a/public/pages/CreateTrigger/components/BucketLevelTriggerQuery/BucketLevelTriggerQuery.js b/public/pages/CreateTrigger/components/BucketLevelTriggerQuery/BucketLevelTriggerQuery.js index 33f9d2fb1..bfc7545b7 100644 --- a/public/pages/CreateTrigger/components/BucketLevelTriggerQuery/BucketLevelTriggerQuery.js +++ b/public/pages/CreateTrigger/components/BucketLevelTriggerQuery/BucketLevelTriggerQuery.js @@ -62,90 +62,90 @@ const BucketLevelTriggerQuery = ({ _.set(trigger, `${TRIGGER_TYPE.BUCKET_LEVEL}.actions`, []); const fieldName = `${fieldPath}bucketSelector`; return ( - - - - - - {({ - field: { value }, - form: { errors, touched, setFieldValue, setFieldTouched }, - }) => ( - - - - Trigger condition - - - - - { - setFlyout({ type: 'triggerCondition', payload: context }); - }} - > - Info - - - - - } - fullWidth={true} - isInvalid={_.get(touched, fieldName, false) && !!_.get(errors, fieldName)} - error={_.get(errors, fieldName)} - > - { - setFieldValue(fieldName, source); - }} - onBlur={() => setFieldTouched(fieldName, true)} - value={value} - /> - - )} - - +
+ + + + + + {({ + field: { value }, + form: { errors, touched, setFieldValue, setFieldTouched }, + }) => ( + + + + Trigger condition + + + + + { + setFlyout({ type: 'triggerCondition', payload: context }); + }} + > + Info + + + + + } + fullWidth={true} + isInvalid={_.get(touched, fieldName, false) && !!_.get(errors, fieldName)} + error={_.get(errors, fieldName)} + > + { + setFieldValue(fieldName, source); + }} + onBlur={() => setFieldTouched(fieldName, true)} + value={value} + /> + + )} + + - - - Trigger condition response - - } - > - - - - - + + + Trigger condition response + + } + > + + + + + + - - onRun(_.isArray(trigger) ? trigger : [trigger])} - size={'s'} - style={{ width: '250px' }} - > - Run for condition response - - - + onRun(_.isArray(trigger) ? trigger : [trigger])} + size={'s'} + style={{ marginLeft: '10px' }} + > + Preview condition response + +
); }; diff --git a/public/pages/CreateTrigger/components/TriggerEmptyPrompt/TriggerEmptyPrompt.js b/public/pages/CreateTrigger/components/TriggerEmptyPrompt/TriggerEmptyPrompt.js index 38b91e83d..2fdaff087 100644 --- a/public/pages/CreateTrigger/components/TriggerEmptyPrompt/TriggerEmptyPrompt.js +++ b/public/pages/CreateTrigger/components/TriggerEmptyPrompt/TriggerEmptyPrompt.js @@ -6,10 +6,13 @@ import React from 'react'; import { EuiEmptyPrompt, EuiText } from '@elastic/eui'; import AddTriggerButton from '../AddTriggerButton'; +import { FORMIK_INITIAL_TRIGGER_VALUES } from '../../containers/CreateTrigger/utils/constants'; -const addTriggerButton = (arrayHelpers) => ; +const addTriggerButton = (arrayHelpers, script) => ( + +); -const TriggerEmptyPrompt = ({ arrayHelpers }) => ( +const TriggerEmptyPrompt = ({ arrayHelpers, script = FORMIK_INITIAL_TRIGGER_VALUES.script }) => ( (

Add a trigger to define conditions and actions.

} - actions={addTriggerButton(arrayHelpers)} + actions={addTriggerButton(arrayHelpers, script)} /> ); diff --git a/public/pages/CreateTrigger/components/TriggerQuery/TriggerQuery.js b/public/pages/CreateTrigger/components/TriggerQuery/TriggerQuery.js index 5d1b57ca1..7679ce1a7 100644 --- a/public/pages/CreateTrigger/components/TriggerQuery/TriggerQuery.js +++ b/public/pages/CreateTrigger/components/TriggerQuery/TriggerQuery.js @@ -55,106 +55,107 @@ const TriggerQuery = ({ const fieldName = `${fieldPath}script.source`; return ( - - - - -
- {isAd ? ( -
- - - - - -
- ) : null} +
+ + + + +
+ {isAd ? ( +
+ + + + + +
+ ) : null} - - {({ - field: { value }, - form: { errors, touched, setFieldValue, setFieldTouched }, - }) => ( - - - - Trigger condition - - - - - { - setFlyout({ type: 'triggerCondition', payload: context }); - }} - > - Info - - - - - } - fullWidth={true} - isInvalid={_.get(touched, fieldName, false) && !!_.get(errors, fieldName)} - error={_.get(errors, fieldName)} - > - { - setFieldValue(fieldName, source); - }} - onBlur={() => setFieldTouched(fieldName, true)} - value={value} - /> - - )} - -
-
+ + {({ + field: { value }, + form: { errors, touched, setFieldValue, setFieldTouched }, + }) => ( + + + + Trigger condition + + + + + { + setFlyout({ type: 'triggerCondition', payload: context }); + }} + > + Info + + + +
+ } + fullWidth={true} + isInvalid={_.get(touched, fieldName, false) && !!_.get(errors, fieldName)} + error={_.get(errors, fieldName)} + > + { + setFieldValue(fieldName, source); + }} + onBlur={() => setFieldTouched(fieldName, true)} + value={value} + data-test-subj={'triggerQueryCodeEditor'} + /> + + )} + +
+ - - - Trigger condition response - - } - > - - - - - + + + Trigger condition response + + } + > + + + + + + - - onRun([trigger])} size={'s'} style={{ width: '250px' }}> - Run for condition response - - - + onRun([trigger])} size={'s'} style={{ marginLeft: '10px' }}> + Preview condition response + +
); }; diff --git a/public/pages/CreateTrigger/components/TriggerQuery/__snapshots__/TriggerQuery.test.js.snap b/public/pages/CreateTrigger/components/TriggerQuery/__snapshots__/TriggerQuery.test.js.snap index 9a2b588b5..4f3710f0d 100644 --- a/public/pages/CreateTrigger/components/TriggerQuery/__snapshots__/TriggerQuery.test.js.snap +++ b/public/pages/CreateTrigger/components/TriggerQuery/__snapshots__/TriggerQuery.test.js.snap @@ -1,95 +1,89 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`TriggerQuery renders 1`] = ` - - + - - + -
- - - -
-
- - +
+ - - Trigger condition response - - + + +
+
+ - - - -
-
- + + Trigger condition response + + + } + labelType="label" + > + + + +
+
+
+ - - Run for condition response - -
-
+ Preview condition response +
+
`; diff --git a/public/pages/CreateTrigger/containers/ConfigureActions/ConfigureActions.js b/public/pages/CreateTrigger/containers/ConfigureActions/ConfigureActions.js index d1cfcb2cc..19bfd1f22 100644 --- a/public/pages/CreateTrigger/containers/ConfigureActions/ConfigureActions.js +++ b/public/pages/CreateTrigger/containers/ConfigureActions/ConfigureActions.js @@ -97,6 +97,7 @@ class ConfigureActions extends React.Component { ); break; case MONITOR_TYPE.QUERY_LEVEL: + case MONITOR_TYPE.CLUSTER_METRICS: _.set( initialActionValues, 'message_template.source', @@ -147,6 +148,7 @@ class ConfigureActions extends React.Component { _.set(testTrigger, `${TRIGGER_TYPE.BUCKET_LEVEL}.condition`, condition); break; case MONITOR_TYPE.QUERY_LEVEL: + case MONITOR_TYPE.CLUSTER_METRICS: action = _.get(testTrigger, `actions[${index}]`); condition = { ..._.get(testTrigger, 'condition'), diff --git a/public/pages/CreateTrigger/containers/ConfigureTriggers/ConfigureTriggers.js b/public/pages/CreateTrigger/containers/ConfigureTriggers/ConfigureTriggers.js index f7ef0c569..c46c39550 100644 --- a/public/pages/CreateTrigger/containers/ConfigureTriggers/ConfigureTriggers.js +++ b/public/pages/CreateTrigger/containers/ConfigureTriggers/ConfigureTriggers.js @@ -19,6 +19,12 @@ import { buildSearchRequest } from '../../../CreateMonitor/containers/DefineMoni import { backendErrorNotification, inputLimitText } from '../../../../utils/helpers'; import moment from 'moment'; import { formikToTrigger } from '../CreateTrigger/utils/formikToTrigger'; +import { + buildClusterMetricsRequest, + canExecuteClusterMetricsMonitor, + getDefaultScript, +} from '../../../CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers'; +import { FORMIK_INITIAL_VALUES } from '../../../CreateMonitor/containers/CreateMonitor/utils/constants'; class ConfigureTriggers extends React.Component { constructor(props) { @@ -31,14 +37,24 @@ class ConfigureTriggers extends React.Component { _.get(props, 'monitor.monitor_type', MONITOR_TYPE.QUERY_LEVEL) === MONITOR_TYPE.BUCKET_LEVEL, triggerDeleted: false, + addTriggerButton: this.prepareAddTriggerButton(), + triggerEmptyPrompt: this.prepareTriggerEmptyPrompt(), }; this.onQueryMappings = this.onQueryMappings.bind(this); this.onRunExecute = this.onRunExecute.bind(this); + this.prepareAddTriggerButton = this.prepareAddTriggerButton.bind(this); + this.prepareTriggerEmptyPrompt = this.prepareTriggerEmptyPrompt.bind(this); } componentDidMount() { - if (this.state.isBucketLevelMonitor) this.onQueryMappings(); + const { + monitorValues: { searchType, uri }, + } = this.props; + const { isBucketLevelMonitor } = this.state; + if (searchType === SEARCH_TYPE.CLUSTER_METRICS && canExecuteClusterMetricsMonitor(uri)) + this.onRunExecute(); + if (isBucketLevelMonitor) this.onQueryMappings(); } componentDidUpdate(prevProps) { @@ -47,13 +63,66 @@ class ConfigureTriggers extends React.Component { if (prevMonitorType !== currMonitorType) _.set(this.state, 'isBucketLevelMonitor', currMonitorType === MONITOR_TYPE.BUCKET_LEVEL); + const prevSearchType = _.get( + prevProps, + 'monitorValues.searchType', + FORMIK_INITIAL_VALUES.searchType + ); + const currSearchType = _.get( + this.props, + 'monitorValues.searchType', + FORMIK_INITIAL_VALUES.searchType + ); + const prevApiType = _.get( + prevProps, + 'monitorValues.uri.api_type', + FORMIK_INITIAL_VALUES.uri.api_type + ); + const currApiType = _.get( + this.props, + 'monitorValues.uri.api_type', + FORMIK_INITIAL_VALUES.uri.api_type + ); + if (prevSearchType !== currSearchType || prevApiType !== currApiType) { + switch (currSearchType) { + case SEARCH_TYPE.CLUSTER_METRICS: + _.set(this.state, 'addTriggerButton', this.prepareAddTriggerButton()); + _.set(this.state, 'triggerEmptyPrompt', this.prepareTriggerEmptyPrompt()); + break; + } + } + const prevInputs = prevProps.monitor.inputs[0]; const currInputs = this.props.monitor.inputs[0]; if (!_.isEqual(prevInputs, currInputs)) { - if (this.state.isBucketLevelMonitor) this.onQueryMappings(); + const { isBucketLevelMonitor } = this.state; + if (isBucketLevelMonitor) this.onQueryMappings(); } } + prepareAddTriggerButton = () => { + const { monitorValues, triggerArrayHelpers, triggerValues } = this.props; + const disableAddTriggerButton = + _.get(triggerValues, 'triggerDefinitions', []).length >= MAX_TRIGGERS; + return ( + + ); + }; + + prepareTriggerEmptyPrompt = () => { + const { monitorValues, triggerArrayHelpers } = this.props; + return ( + + ); + }; + onRunExecute = (triggers = []) => { const { httpClient, monitor, notifications } = this.props; const formikValues = monitorToFormik(monitor); @@ -67,6 +136,10 @@ class ConfigureTriggers extends React.Component { const searchRequest = buildSearchRequest(formikValues); _.set(monitorToExecute, 'inputs[0].search', searchRequest); break; + case SEARCH_TYPE.CLUSTER_METRICS: + const clusterMetricsRequest = buildClusterMetricsRequest(formikValues); + _.set(monitorToExecute, 'inputs[0].uri', clusterMetricsRequest); + break; default: console.log(`Unsupported searchType found: ${JSON.stringify(searchType)}`, searchType); } @@ -140,62 +213,58 @@ class ConfigureTriggers extends React.Component { httpClient, notifications, } = this.props; - - const { dataTypes, executeResponse, isBucketLevelMonitor } = this.state; + const { dataTypes, executeResponse, isBucketLevelMonitor, triggerEmptyPrompt } = this.state; const hasTriggers = !_.isEmpty(_.get(triggerValues, 'triggerDefinitions')); - return hasTriggers ? ( - triggerValues.triggerDefinitions.map((trigger, index) => { - return ( -
- {isBucketLevelMonitor ? ( - - ) : ( - - )} - -
- ); - }) - ) : ( - - ); + return hasTriggers + ? triggerValues.triggerDefinitions.map((trigger, index) => { + return ( +
+ {isBucketLevelMonitor ? ( + + ) : ( + + )} + +
+ ); + }) + : triggerEmptyPrompt; }; render() { const { triggerArrayHelpers, triggerValues } = this.props; - const disableAddTriggerButton = - _.get(triggerValues, 'triggerDefinitions', []).length >= MAX_TRIGGERS; + const { addTriggerButton } = this.state; const numOfTriggers = _.get(triggerValues, 'triggerDefinitions', []).length; const displayAddTriggerButton = numOfTriggers > 0; return ( @@ -210,10 +279,7 @@ class ConfigureTriggers extends React.Component { {displayAddTriggerButton ? (
- + {addTriggerButton} {inputLimitText(numOfTriggers, MAX_TRIGGERS, 'trigger', 'triggers')}
diff --git a/public/pages/CreateTrigger/containers/CreateTrigger/CreateTrigger/CreateTrigger.js b/public/pages/CreateTrigger/containers/CreateTrigger/CreateTrigger/CreateTrigger.js index 517ac51a3..d4fa69b1b 100644 --- a/public/pages/CreateTrigger/containers/CreateTrigger/CreateTrigger/CreateTrigger.js +++ b/public/pages/CreateTrigger/containers/CreateTrigger/CreateTrigger/CreateTrigger.js @@ -28,13 +28,14 @@ import monitorToFormik from '../../../../CreateMonitor/containers/CreateMonitor/ import { buildSearchRequest } from '../../../../CreateMonitor/containers/DefineMonitor/utils/searchRequests'; import { formikToTrigger, formikToTriggerUiMetadata } from '../utils/formikToTrigger'; import { triggerToFormik } from '../utils/triggerToFormik'; -import { FORMIK_INITIAL_TRIGGER_VALUES } from '../utils/constants'; +import { FORMIK_INITIAL_TRIGGER_VALUES, TRIGGER_TYPE } from '../utils/constants'; import { SEARCH_TYPE } from '../../../../../utils/constants'; import { SubmitErrorHandler } from '../../../../../utils/SubmitErrorHandler'; import { backendErrorNotification } from '../../../../../utils/helpers'; import DefineBucketLevelTrigger from '../../DefineBucketLevelTrigger'; import { getPathsPerDataType } from '../../../../CreateMonitor/containers/DefineMonitor/utils/mappings'; import { MONITOR_TYPE } from '../../../../../utils/constants'; +import { buildClusterMetricsRequest } from '../../../../CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers'; export const DEFAULT_CLOSED_STATES = { WHEN: false, @@ -99,10 +100,11 @@ export default class CreateTrigger extends Component { onEdit = (trigger, triggerMetadata, { setSubmitting, setErrors }) => { const { monitor, updateMonitor, onCloseTrigger, triggerToEdit } = this.props; const { ui_metadata: uiMetadata = {}, triggers, monitor_type } = monitor; - const { name } = - monitor_type === MONITOR_TYPE.QUERY_LEVEL - ? triggerToEdit.query_level_trigger - : triggerToEdit.bucket_level_trigger; + const triggerType = + monitor_type === MONITOR_TYPE.BUCKET_LEVEL + ? TRIGGER_TYPE.BUCKET_LEVEL + : TRIGGER_TYPE.QUERY_LEVEL; + const { name } = triggerToEdit[triggerType]; const updatedTriggersMetadata = _.cloneDeep(uiMetadata.triggers || {}); delete updatedTriggersMetadata[name]; const updatedUiMetadata = { @@ -110,11 +112,7 @@ export default class CreateTrigger extends Component { triggers: { ...updatedTriggersMetadata, ...triggerMetadata }, }; - const findTriggerName = (element) => { - return monitor_type === MONITOR_TYPE.QUERY_LEVEL - ? name === element.query_level_trigger.name - : name === element.bucket_level_trigger.name; - }; + const findTriggerName = (element) => element[triggerType].name; const indexToUpdate = _.findIndex(triggers, findTriggerName); const updatedTriggers = triggers.slice(); @@ -147,6 +145,10 @@ export default class CreateTrigger extends Component { const searchRequest = buildSearchRequest(formikValues); _.set(monitorToExecute, 'inputs[0].search', searchRequest); break; + case SEARCH_TYPE.CLUSTER_METRICS: + const clusterMetricsRequest = buildClusterMetricsRequest(formikValues); + _.set(monitorToExecute, 'inputs[0].uri', clusterMetricsRequest); + break; default: console.log(`Unsupported searchType found: ${JSON.stringify(searchType)}`, searchType); } @@ -280,7 +282,7 @@ export default class CreateTrigger extends Component { render() { const { monitor, onCloseTrigger, setFlyout, edit, httpClient, notifications } = this.props; const { dataTypes, initialValues, executeResponse } = this.state; - const isQueryLevelMonitor = _.get(monitor, 'monitor_type') === MONITOR_TYPE.QUERY_LEVEL; + const isBucketLevelMonitor = _.get(monitor, 'monitor_type') === MONITOR_TYPE.BUCKET_LEVEL; return (
@@ -292,18 +294,7 @@ export default class CreateTrigger extends Component {

{edit ? 'Edit' : 'Create'} trigger

- {isQueryLevelMonitor ? ( - - ) : ( + {isBucketLevelMonitor ? ( {(arrayHelpers) => ( )} + ) : ( + )} diff --git a/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js b/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js index 631698ea6..845499f18 100644 --- a/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js +++ b/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js @@ -29,11 +29,12 @@ export function formikToTriggerDefinitions(values, monitorUiMetadata) { } export function formikToTriggerDefinition(values, monitorUiMetadata) { - const isQueryLevelMonitor = - _.get(monitorUiMetadata, 'monitor_type', MONITOR_TYPE.QUERY_LEVEL) === MONITOR_TYPE.QUERY_LEVEL; - return isQueryLevelMonitor - ? formikToQueryLevelTrigger(values, monitorUiMetadata) - : formikToBucketLevelTrigger(values, monitorUiMetadata); + const isBucketLevelMonitor = + _.get(monitorUiMetadata, 'monitor_type', MONITOR_TYPE.QUERY_LEVEL) === + MONITOR_TYPE.BUCKET_LEVEL; + return isBucketLevelMonitor + ? formikToBucketLevelTrigger(values, monitorUiMetadata) + : formikToQueryLevelTrigger(values, monitorUiMetadata); } export function formikToQueryLevelTrigger(values, monitorUiMetadata) { @@ -127,6 +128,7 @@ export function formikToBucketLevelTriggerAction(values) { export function formikToTriggerUiMetadata(values, monitorUiMetadata) { switch (monitorUiMetadata.monitor_type) { case MONITOR_TYPE.QUERY_LEVEL: + case MONITOR_TYPE.CLUSTER_METRICS: const searchType = _.get(monitorUiMetadata, 'search.searchType', 'query'); const queryLevelTriggersUiMetadata = {}; _.get(values, 'triggerDefinitions', []).forEach((trigger) => { @@ -170,7 +172,8 @@ export function formikToCondition(values, monitorUiMetadata = {}) { const searchType = _.get(monitorUiMetadata, 'search.searchType', 'query'); const aggregationType = _.get(monitorUiMetadata, 'search.aggregations.0.aggregationType'); - if (searchType === SEARCH_TYPE.QUERY) return { script: values.script }; + if (searchType === SEARCH_TYPE.QUERY || searchType === SEARCH_TYPE.CLUSTER_METRICS) + return { script: values.script }; if (searchType === SEARCH_TYPE.AD) return getADCondition(values); // If no aggregation type defined, default to count of documents situation diff --git a/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.test.js b/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.test.js index 350251469..768bc440a 100644 --- a/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.test.js +++ b/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.test.js @@ -78,6 +78,13 @@ describe('formikToCondition', () => { }); }); + test('can return condition when searchType is clusterMetrics', () => { + const formikValues = _.cloneDeep(FORMIK_INITIAL_TRIGGER_VALUES); + expect(formikToCondition(formikValues, { search: { searchType: 'clusterMetrics' } })).toEqual({ + script: formikValues.script, + }); + }); + test('can return condition when searchType is ad', () => { const formikValues = _.cloneDeep(FORMIK_INITIAL_TRIGGER_VALUES); expect(formikToCondition(formikValues, { search: { searchType: 'ad' } })).toEqual({ diff --git a/public/pages/CreateTrigger/containers/CreateTrigger/utils/triggerToFormik.js b/public/pages/CreateTrigger/containers/CreateTrigger/utils/triggerToFormik.js index 538904839..66c4b60dc 100644 --- a/public/pages/CreateTrigger/containers/CreateTrigger/utils/triggerToFormik.js +++ b/public/pages/CreateTrigger/containers/CreateTrigger/utils/triggerToFormik.js @@ -29,11 +29,11 @@ export function triggerDefinitionsToFormik(triggers, monitor) { } export function triggerDefinitionToFormik(trigger, monitor) { - const isQueryLevelMonitor = - _.get(monitor, 'monitor_type', MONITOR_TYPE.QUERY_LEVEL) === MONITOR_TYPE.QUERY_LEVEL; - return isQueryLevelMonitor - ? queryLevelTriggerToFormik(trigger, monitor) - : bucketLevelTriggerToFormik(trigger, monitor); + const isBucketLevelMonitor = + _.get(monitor, 'monitor_type', MONITOR_TYPE.QUERY_LEVEL) === MONITOR_TYPE.BUCKET_LEVEL; + return isBucketLevelMonitor + ? bucketLevelTriggerToFormik(trigger, monitor) + : queryLevelTriggerToFormik(trigger, monitor); } export function queryLevelTriggerToFormik(trigger, monitor) { diff --git a/public/pages/CreateTrigger/containers/DefineTrigger/DefineTrigger.js b/public/pages/CreateTrigger/containers/DefineTrigger/DefineTrigger.js index 0e2a2bbbd..cf77a8d50 100644 --- a/public/pages/CreateTrigger/containers/DefineTrigger/DefineTrigger.js +++ b/public/pages/CreateTrigger/containers/DefineTrigger/DefineTrigger.js @@ -21,6 +21,10 @@ import ConfigureActions from '../ConfigureActions'; import monitorToFormik from '../../../CreateMonitor/containers/CreateMonitor/utils/monitorToFormik'; import { buildSearchRequest } from '../../../CreateMonitor/containers/DefineMonitor/utils/searchRequests'; import { backendErrorNotification } from '../../../../utils/helpers'; +import { + buildClusterMetricsRequest, + canExecuteClusterMetricsMonitor, +} from '../../../CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers'; const defaultRowProps = { label: 'Trigger name', @@ -83,7 +87,16 @@ class DefineTrigger extends Component { // when this component mount (new trigger added) // see how to subscribe the formik related value change componentDidMount() { - this.onRunExecute(); + const { + monitorValues: { searchType, uri }, + } = this.props; + switch (searchType) { + case SEARCH_TYPE.CLUSTER_METRICS: + if (canExecuteClusterMetricsMonitor(uri)) this.onRunExecute(); + break; + default: + this.onRunExecute(); + } } onRunExecute = (triggers = []) => { @@ -101,6 +114,10 @@ class DefineTrigger extends Component { break; case SEARCH_TYPE.AD: break; + case SEARCH_TYPE.CLUSTER_METRICS: + const clusterMetricsRequest = buildClusterMetricsRequest(formikValues); + _.set(monitorToExecute, 'inputs[0].uri', clusterMetricsRequest); + break; default: console.log(`Unsupported searchType found: ${JSON.stringify(searchType)}`, searchType); } diff --git a/public/pages/Dashboard/components/AcknowledgeAlertsModal/AcknowledgeAlertsModal.js b/public/pages/Dashboard/components/AcknowledgeAlertsModal/AcknowledgeAlertsModal.js index 9ebb84b46..2c9475eb1 100644 --- a/public/pages/Dashboard/components/AcknowledgeAlertsModal/AcknowledgeAlertsModal.js +++ b/public/pages/Dashboard/components/AcknowledgeAlertsModal/AcknowledgeAlertsModal.js @@ -303,6 +303,7 @@ export default class AcknowledgeAlertsModal extends Component { const getItemId = (item) => { switch (monitorType) { case MONITOR_TYPE.QUERY_LEVEL: + case MONITOR_TYPE.CLUSTER_METRICS: return `${item.id}-${item.version}`; case MONITOR_TYPE.BUCKET_LEVEL: return item.id; @@ -328,6 +329,7 @@ export default class AcknowledgeAlertsModal extends Component { let columns = []; switch (monitorType) { case MONITOR_TYPE.QUERY_LEVEL: + case MONITOR_TYPE.CLUSTER_METRICS: columns = queryColumns; break; case MONITOR_TYPE.BUCKET_LEVEL: diff --git a/public/pages/Dashboard/components/AcknowledgeAlertsModal/__snapshots__/AcknowledgeAlertsModal.test.js.snap b/public/pages/Dashboard/components/AcknowledgeAlertsModal/__snapshots__/AcknowledgeAlertsModal.test.js.snap index 1f8fd9c33..c3c08a463 100644 --- a/public/pages/Dashboard/components/AcknowledgeAlertsModal/__snapshots__/AcknowledgeAlertsModal.test.js.snap +++ b/public/pages/Dashboard/components/AcknowledgeAlertsModal/__snapshots__/AcknowledgeAlertsModal.test.js.snap @@ -72,6 +72,12 @@ exports[`AcknowledgeAlertsModal renders 1`] = ` "searchType": "graph", "timeField": "", "timezone": Array [], + "uri": Object { + "api_type": "", + "path": "", + "path_params": "", + "url": "", + }, "weekly": Object { "fri": false, "mon": false, diff --git a/public/pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats.js b/public/pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats.js index 2a7e39fa9..aa0768a5e 100644 --- a/public/pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats.js +++ b/public/pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats.js @@ -14,6 +14,8 @@ import { OPENSEARCH_DASHBOARDS_AD_PLUGIN, SEARCH_TYPE, } from '../../../../../utils/constants'; +import { API_TYPES } from '../../../../CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorConstants'; +import { getApiType } from '../../../../CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers'; // TODO: used in multiple places, move into helper export function getTime(time) { @@ -23,12 +25,17 @@ export function getTime(time) { return DEFAULT_EMPTY_DATA; } -function getMonitorType(searchType) { +function getMonitorType(searchType, monitor) { switch (searchType) { case SEARCH_TYPE.GRAPH: return 'Visual Graph'; case SEARCH_TYPE.AD: return 'Anomaly Detector'; + case SEARCH_TYPE.CLUSTER_METRICS: + const uri = _.get(monitor, 'inputs.0.uri'); + const apiType = getApiType(uri); + const apiTypeLabel = _.get(API_TYPES, `${apiType}.label`); + return apiTypeLabel; default: return 'Extraction Query'; } @@ -40,6 +47,8 @@ function getMonitorLevelType(monitorType) { return 'Per query monitor'; case MONITOR_TYPE.BUCKET_LEVEL: return 'Per bucket monitor'; + case MONITOR_TYPE.CLUSTER_METRICS: + return 'Per cluster metrics monitor'; default: // TODO: May be valuable to implement a toast that displays in this case. console.log('Unexpected monitor type:', monitorType); @@ -55,7 +64,10 @@ export default function getOverviewStats( detector, detectorId ) { - const searchType = _.get(monitor, 'ui_metadata.search.searchType', 'query'); + const searchType = _.has(monitor, 'inputs[0].uri') + ? SEARCH_TYPE.CLUSTER_METRICS + : _.get(monitor, 'ui_metadata.search.searchType', 'query'); + const detectorOverview = detector ? [ { @@ -79,7 +91,7 @@ export default function getOverviewStats( }, { header: 'Monitor definition type', - value: getMonitorType(searchType), + value: getMonitorType(searchType, monitor), }, ...detectorOverview, { diff --git a/public/pages/Monitors/containers/Monitors/Monitors.js b/public/pages/Monitors/containers/Monitors/Monitors.js index 0465bbd70..21f9de5a5 100644 --- a/public/pages/Monitors/containers/Monitors/Monitors.js +++ b/public/pages/Monitors/containers/Monitors/Monitors.js @@ -15,7 +15,7 @@ import MonitorEmptyPrompt from '../../components/MonitorEmptyPrompt'; import { DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_QUERY_PARAMS } from './utils/constants'; import { getURLQueryParams } from './utils/helpers'; import { columns as staticColumns } from './utils/tableUtils'; -import { MONITOR_ACTIONS } from '../../../../utils/constants'; +import { MONITOR_ACTIONS, MONITOR_TYPE } from '../../../../utils/constants'; import { backendErrorNotification } from '../../../../utils/helpers'; import { displayAcknowledgedAlertsToast } from '../../../Dashboard/utils/helpers'; @@ -127,6 +127,27 @@ export default class Monitors extends Component { }; } + // TODO: The getMonitors API is wrapping the 'monitor' field for ClusterMetrics monitors in an additional 'monitor' object. + // This formatGetMonitorsResponse method is a temporary means of resolving that issue until it can be debugged on the backend. + formatGetMonitorsResponse = (monitors) => { + const unwrappedMonitors = []; + monitors.forEach((monitor) => { + const monitorType = _.get(monitor, 'monitor.monitor.monitor_type', 'monitor.monitor_type'); + switch (monitorType) { + case MONITOR_TYPE.CLUSTER_METRICS: + let unwrappedMonitor = monitor.monitor; + _.set(monitor, 'monitor', unwrappedMonitor.monitor); + _.set(monitor, 'name', monitor.monitor.name); + _.set(monitor, 'enabled', monitor.monitor.enabled); + unwrappedMonitors.push(monitor); + break; + default: + unwrappedMonitors.push(monitor); + } + }); + return unwrappedMonitors; + }; + async getMonitors(from, size, search, sortField, sortDirection, state) { this.setState({ loadingMonitors: true }); try { @@ -137,7 +158,7 @@ export default class Monitors extends Component { const response = await httpClient.get('../api/alerting/monitors', { query: params }); if (response.ok) { const { monitors, totalMonitors } = response; - this.setState({ monitors, totalMonitors }); + this.setState({ monitors: this.formatGetMonitorsResponse(monitors), totalMonitors }); } else { console.log('error getting monitors:', response); // TODO: 'response.ok' is 'false' when there is no alerting config index in the cluster, and notification should not be shown to new Alerting users diff --git a/public/utils/constants.js b/public/utils/constants.js index e784b15a7..21900fae4 100644 --- a/public/utils/constants.js +++ b/public/utils/constants.js @@ -22,11 +22,13 @@ export const SEARCH_TYPE = { GRAPH: 'graph', QUERY: 'query', AD: 'ad', + CLUSTER_METRICS: 'clusterMetrics', }; export const MONITOR_TYPE = { QUERY_LEVEL: 'query_level_monitor', BUCKET_LEVEL: 'bucket_level_monitor', + CLUSTER_METRICS: 'cluster_metrics_monitor', }; export const DESTINATION_ACTIONS = { diff --git a/public/utils/validate.js b/public/utils/validate.js index e4b0cb211..6b03a0d09 100644 --- a/public/utils/validate.js +++ b/public/utils/validate.js @@ -23,10 +23,14 @@ export const validateActionName = (monitor, trigger) => (value) => { // TODO: Expand on this validation by passing in triggerValues and comparing the current // action's name with names of other actions in the trigger creation form. let actions; - if (monitor.monitor_type === MONITOR_TYPE.QUERY_LEVEL) { - actions = _.get(trigger, `${TRIGGER_TYPE.QUERY_LEVEL}.actions`, []); - } else if (monitor.monitor_type === MONITOR_TYPE.BUCKET_LEVEL) { - actions = _.get(trigger, `${TRIGGER_TYPE.BUCKET_LEVEL}.actions`, []); + switch (monitor.monitor_type) { + case MONITOR_TYPE.QUERY_LEVEL: + case MONITOR_TYPE.CLUSTER_METRICS: + actions = _.get(trigger, `${TRIGGER_TYPE.QUERY_LEVEL}.actions`, []); + break; + case MONITOR_TYPE.BUCKET_LEVEL: + actions = _.get(trigger, `${TRIGGER_TYPE.BUCKET_LEVEL}.actions`, []); + break; } const matches = actions.filter((action) => action.name === value); if (matches.length > 1) return 'Action name is already used.'; @@ -56,6 +60,11 @@ export const validateRequiredNumber = (value) => { if (value === undefined || typeof value === 'string') return 'Provide a value.'; }; +export const isInvalidApiPath = (name, form) => { + const path = _.get(form, `values.${name}`); + return _.get(form.touched, name, false) && _.isEmpty(path); +}; + export const validateMonitorName = (httpClient, monitorToEdit) => async (value) => { try { if (!value) return 'Required.';