diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..29eddb95e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: 🐛 Bug report +about: Create a report to help us improve +title: '[BUG]' +labels: 'bug, untriaged' +assignees: '' +--- + +**What is the bug?** +A clear and concise description of the bug. + +**How can one reproduce the bug?** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**What is the expected behavior?** +A clear and concise description of what you expected to happen. + +**What is your host/environment?** + - OS: [e.g. iOS] + - Version [e.g. 22] + - Plugins + +**Do you have any screenshots?** +If applicable, add screenshots to help explain your problem. + +**Do you have any additional context?** +Add any other context about the problem. diff --git a/.github/ISSUE_TEMPLATE/bug_template.md b/.github/ISSUE_TEMPLATE/bug_template.md index 8af6ebb52..183ab2c49 100644 --- a/.github/ISSUE_TEMPLATE/bug_template.md +++ b/.github/ISSUE_TEMPLATE/bug_template.md @@ -2,7 +2,7 @@ name: 🐛 Bug report about: Create a report to help us improve title: "[BUG]" -labels: 'bug, untriaged, Beta' +labels: 'bug, untriaged' assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..a8199a104 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,7 @@ +contact_links: + - name: OpenSearch Community Support + url: https://discuss.opendistrocommunity.dev/ + about: Please ask and answer questions here. + - name: AWS/Amazon Security + url: https://aws.amazon.com/security/vulnerability-reporting/ + about: Please report security vulnerabilities here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 2791b8081..6198f3383 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,19 +1,18 @@ --- name: 🎆 Feature request -about: Suggest an idea for this project -title: '' -labels: enhancement +about: Request a feature in this project +title: '[FEATURE]' +labels: 'enhancement, untriaged' assignees: '' --- +**Is your feature request related to a problem?** +A clear and concise description of what the problem is, e.g. _I'm always frustrated when [...]_ -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** +**What solution would you like?** A clear and concise description of what you want to happen. -**Describe alternatives you've considered** +**What alternatives have you considered?** A clear and concise description of any alternative solutions or features you've considered. -**Additional context** +**Do you have any additional context?** Add any other context or screenshots about the feature request here. \ No newline at end of file diff --git a/.github/workflows/cypress-workflow.yml b/.github/workflows/cypress-workflow.yml index 5459f2fbe..55b0a28e9 100644 --- a/.github/workflows/cypress-workflow.yml +++ b/.github/workflows/cypress-workflow.yml @@ -10,7 +10,7 @@ on: - 1.x env: OPENSEARCH_DASHBOARDS_VERSION: 'main' - OPENSEARCH_VERSION: '2.0.0-alpha1-SNAPSHOT' + OPENSEARCH_VERSION: '2.0.0-rc1-SNAPSHOT' jobs: tests: name: Run Cypress E2E tests diff --git a/babel.config.js b/babel.config.js index 54e99d34a..b0b98d6f6 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,7 +1,11 @@ // babelrc doesn't respect NODE_PATH anymore but using require does. // Alternative to install them locally in node_modules module.exports = { - presets: [require('@babel/preset-env'), require('@babel/preset-react')], + presets: [ + require('@babel/preset-env'), + require('@babel/preset-react'), + require('@babel/preset-typescript'), + ], plugins: [ require('@babel/plugin-proposal-class-properties'), require('@babel/plugin-proposal-object-rest-spread'), diff --git a/cypress/fixtures/sample_destination_chime.json b/cypress/fixtures/sample_destination_chime.json deleted file mode 100644 index 147b589cf..000000000 --- a/cypress/fixtures/sample_destination_chime.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "sample_destination_chime", - "type": "chime", - "chime": { - "url": "https://hooks.chime.aws/incomingwebhooks/XXX?token=XXX" - } -} diff --git a/cypress/fixtures/sample_destination_custom_webhook.json b/cypress/fixtures/sample_destination_custom_webhook.json deleted file mode 100644 index 657b60c50..000000000 --- a/cypress/fixtures/sample_destination_custom_webhook.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "sample_destination", - "type": "custom_webhook", - "custom_webhook": { - "header_params": { - "Content-Type": "application/json" - }, - "url": "http://www.sampledestination.com" - } -} diff --git a/cypress/integration/bucket_level_monitor_spec.js b/cypress/integration/bucket_level_monitor_spec.js index 4b52523cb..dc679bdbe 100644 --- a/cypress/integration/bucket_level_monitor_spec.js +++ b/cypress/integration/bucket_level_monitor_spec.js @@ -5,7 +5,6 @@ import { INDEX, PLUGIN_NAME } from '../support/constants'; import sampleAggregationQuery from '../fixtures/sample_aggregation_query'; -import sampleDestination from '../fixtures/sample_destination_custom_webhook'; import sampleExtractionQueryMonitor from '../fixtures/sample_extraction_query_bucket_level_monitor'; import sampleVisualEditorMonitor from '../fixtures/sample_visual_editor_bucket_level_monitor'; @@ -309,7 +308,6 @@ describe('Bucket-Level Monitors', () => { after(() => { // Delete all monitors and destinations cy.deleteAllMonitors(); - cy.deleteAllDestinations(); // Delete sample data cy.deleteIndexByName(`${INDEX.SAMPLE_DATA_ECOMMERCE}`); diff --git a/cypress/integration/cluster_metrics_monitor_spec.js b/cypress/integration/cluster_metrics_monitor_spec.js index 9800d1630..50d05aaf3 100644 --- a/cypress/integration/cluster_metrics_monitor_spec.js +++ b/cypress/integration/cluster_metrics_monitor_spec.js @@ -3,7 +3,6 @@ * 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'; @@ -392,7 +391,6 @@ describe('ClusterMetricsMonitor', () => { after(() => { // Delete all monitors and destinations cy.deleteAllMonitors(); - cy.deleteAllDestinations(); // Delete sample data cy.deleteIndexByName(`${INDEX.SAMPLE_DATA_ECOMMERCE}`); diff --git a/cypress/integration/destination_spec.js b/cypress/integration/destination_spec.js deleted file mode 100644 index a30f9101d..000000000 --- a/cypress/integration/destination_spec.js +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { PLUGIN_NAME } from '../support/constants'; -import sampleDestination from '../fixtures/sample_destination_custom_webhook'; -import sampleDestinationChime from '../fixtures/sample_destination_chime'; - -const SAMPLE_DESTINATION = 'sample_destination'; -const SAMPLE_DESTINATION_WITH_ANOTHER_NAME = 'sample_destination_chime'; -const UPDATED_DESTINATION = 'updated_destination'; -const SAMPLE_URL = 'http://www.sampledestination.com'; - -describe('Destinations', () => { - 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}#/destinations`); - - // Common text to wait for to confirm page loaded, give up to 20 seconds for initial load - cy.contains('Add destination', { timeout: 20000 }); - }); - - describe('can be created', () => { - before(() => { - cy.deleteAllDestinations(); - }); - - it('with a custom webhook', () => { - // Confirm we loaded empty destination list - cy.contains('There are no existing destinations'); - - // Route us to create destination page - cy.contains('Add destination').click({ force: true }); - - // Wait for input to load and then type in the destination name - cy.get('input[name="name"]').type(SAMPLE_DESTINATION, { force: true }); - - // Select the type of destination - cy.get('#type').select('custom_webhook', { force: true }); - - // Wait for input to load and then type in the index name - cy.get('input[name="custom_webhook.url"]').type(SAMPLE_URL, { force: true }); - - // Click the create button - cy.get('button').contains('Create').click({ force: true }); - - // Confirm we can see the created destination in the list - cy.contains(SAMPLE_DESTINATION); - }); - }); - - describe('can be updated', () => { - before(() => { - cy.deleteAllDestinations(); - cy.createDestination(sampleDestination); - }); - - it('by changing the name', () => { - // Confirm we can see the created destination in the list - cy.contains(SAMPLE_DESTINATION); - - // Click the Edit button - cy.get('button').contains('Edit').click({ force: true }); - - // Wait for input to load and then type in the destination name - // should() is used to wait for input loading before clearing - cy.get('input[name="name"]') - .should('have.value', SAMPLE_DESTINATION) - .clear() - .type(UPDATED_DESTINATION, { force: true }); - - // Click the create button - cy.get('button').contains('Update').click({ force: true }); - - // Confirm we can see the updated destination in the list - cy.contains(UPDATED_DESTINATION); - }); - }); - - describe('can be deleted', () => { - before(() => { - cy.deleteAllDestinations(); - cy.createDestination(sampleDestination); - }); - - it('by clicking the button under "Actions"', () => { - // Confirm we can see the created destination in the list - cy.contains(SAMPLE_DESTINATION); - - // Click the Delete button - cy.contains('Delete').click({ force: true }); - - // Click the delete confirmation button in modal - cy.get(`[data-test-subj="confirmModalConfirmButton"]`).click(); - - // Confirm we can see an empty destination list - cy.contains('There are no existing destinations'); - }); - }); - - describe('can be searched', () => { - before(() => { - cy.deleteAllDestinations(); - // Create 21 destinations so that a monitor will not appear in the first page - for (let i = 0; i < 20; i++) { - cy.createDestination(sampleDestination); - } - cy.createDestination(sampleDestinationChime); - }); - - it('by name', () => { - // Sort the table by monitor name in alphabetical order - cy.get('thead > tr > th').contains('Destination name').click({ force: true }); - - // Confirm the monitor with a different name does not exist - cy.contains(SAMPLE_DESTINATION_WITH_ANOTHER_NAME).should('not.exist'); - - // Type in monitor name in search box - cy.get(`input[type="search"]`).focus().type(SAMPLE_DESTINATION_WITH_ANOTHER_NAME); - - // Confirm we filtered down to our one and only destination - cy.get('tbody > tr').should(($tr) => { - expect($tr, '1 row').to.have.length(1); - expect($tr, 'item').to.contain(SAMPLE_DESTINATION_WITH_ANOTHER_NAME); - }); - }); - }); - - after(() => { - // Delete all existing destinations - cy.deleteAllDestinations(); - }); -}); diff --git a/cypress/integration/query_level_monitor_spec.js b/cypress/integration/query_level_monitor_spec.js index 8d797216d..b6cf5e5ea 100644 --- a/cypress/integration/query_level_monitor_spec.js +++ b/cypress/integration/query_level_monitor_spec.js @@ -3,10 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { PLUGIN_NAME } from '../support/constants'; +import _ from 'lodash'; +import { INDEX, PLUGIN_NAME } from '../support/constants'; import sampleQueryLevelMonitor from '../fixtures/sample_query_level_monitor'; import sampleQueryLevelMonitorWithAlwaysTrueTrigger from '../fixtures/sample_query_level_monitor_with_always_true_trigger'; -import sampleDestination from '../fixtures/sample_destination_custom_webhook.json'; import sampleDaysIntervalQueryLevelMonitor from '../fixtures/sample_days_interval_query_level_monitor.json'; import sampleCronExpressionQueryLevelMonitor from '../fixtures/sample_cron_expression_query_level_monitor.json'; @@ -17,6 +17,54 @@ const SAMPLE_DAYS_INTERVAL_MONITOR = 'sample_days_interval_query_level_monitor'; const SAMPLE_CRON_EXPRESSION_MONITOR = 'sample_cron_expression_query_level_monitor'; const SAMPLE_TRIGGER = 'sample_trigger'; const SAMPLE_ACTION = 'sample_action'; +const SAMPLE_DESTINATION = 'sample_destination'; + +const addVisualQueryLevelTrigger = ( + triggerName, + triggerIndex, + isEdit = true, + thresholdEnum, + thresholdValue +) => { + // Click 'Add trigger' button + cy.contains('Add trigger', { timeout: 20000 }).click({ force: true }); + + if (isEdit) { + // 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); + + // Type in the condition thresholdEnum + cy.get( + `[data-test-subj="triggerDefinitions[${triggerIndex}].thresholdEnum_conditionEnumField"]` + ).select(thresholdEnum); + + // Type in the condition thresholdValue + cy.get( + `[data-test-subj="triggerDefinitions[${triggerIndex}].thresholdValue_conditionValueField"]` + ) + .clear() + .type(`${thresholdValue}{enter}`); + + // FIXME: Temporarily removing destination creation to resolve flakiness. It seems deleteAllDestinations() + // is executing mid-testing. Need to further investigate a more ideal solution. Destination creation should + // ideally take place in the before() block, and clearing should occur in the after() block. + // // Type in the action name + // cy.get( + // `input[name="triggerDefinitions[${triggerIndex}].actions.0.name"]` + // ).type(`${triggerName}-${triggerIndex}-action1`, { 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('Query-Level Monitors', () => { beforeEach(() => { @@ -184,6 +232,88 @@ describe('Query-Level Monitors', () => { }); }); + describe('can have triggers', () => { + before(() => { + cy.deleteAllMonitors(); + cy.loadSampleEcommerceData(); + cy.createMonitor(sampleQueryLevelMonitor); + }); + + it('with names that contain periods', () => { + const triggers = _.orderBy( + [ + { name: '.trigger', enum: 'ABOVE' }, + { name: 'trigger.', enum: 'BELOW' }, + { name: '.trigger.', enum: 'EXACTLY' }, + { name: '..trigger', enum: 'ABOVE' }, + { name: 'trigger..', enum: 'BELOW' }, + { name: '.trigger..', enum: 'EXACTLY' }, + { name: '..trigger.', enum: 'ABOVE' }, + { name: '.trigger.name', enum: 'BELOW' }, + { name: 'trigger.name.', enum: 'EXACTLY' }, + { name: '.trigger.name.', enum: 'ABOVE' }, + ], + (trigger) => trigger.name + ); + + // Confirm we can see the created monitor in the list + cy.contains(SAMPLE_MONITOR); + + // Select the existing monitor + cy.get('a').contains(SAMPLE_MONITOR).click(); + + // Click Edit button + cy.contains('Edit').click({ force: true }); + + // Wait for input to load and then type in the new monitor name + cy.get('input[name="name"]').should('have.value', SAMPLE_MONITOR); + + // Select visual editor + cy.get('[data-test-subj="visualEditorRadioCard"]').click(); + + // Wait for input to load and then type in the index name + cy.get('#index').type(`{backspace}${INDEX.SAMPLE_DATA_ECOMMERCE}{enter}`, { force: true }); + + // Enter the time field + cy.get('#timeField').type('order_date{downArrow}{enter}', { force: true }); + + // Add the test triggers + // For simplicity, the 'value' number is used in this test for the thresholdValue, and the trigger index number. + for (let i = 0; i < triggers.length; i++) { + const trigger = triggers[i]; + triggers[i].value = i; + addVisualQueryLevelTrigger(trigger.name, i, true, `IS ${trigger.enum}`, `${i}`); + } + + // Click Update button + cy.get('button').contains('Update').last().click({ force: true }); + + // Confirm we can see the correct number of rows in the trigger list by checking element + cy.contains(`This table contains ${triggers.length} rows`, { timeout: 20000 }); + + // Click Edit button + cy.contains('Edit').click({ force: true }); + + triggers.forEach((trigger) => { + const triggerIndex = trigger.value; + // Click the trigger accordion to expand it + cy.get(`[data-test-subj="triggerDefinitions[${triggerIndex}]._triggerAccordion"]`).click(); + + // Confirm each trigger exists with the expected name and values + cy.get(`input[name="triggerDefinitions[${triggerIndex}].name"]`).should( + 'have.value', + trigger.name + ); + cy.get( + `[data-test-subj="triggerDefinitions[${triggerIndex}].thresholdEnum_conditionEnumField"]` + ).should('have.value', trigger.enum); + cy.get( + `[data-test-subj="triggerDefinitions[${triggerIndex}].thresholdValue_conditionValueField"]` + ).should('have.value', `${trigger.value}`); + }); + }); + }); + describe('schedule component displays as intended', () => { before(() => { cy.deleteAllMonitors(); @@ -244,6 +374,8 @@ describe('Query-Level Monitors', () => { after(() => { // Delete all existing monitors and destinations cy.deleteAllMonitors(); - cy.deleteAllDestinations(); + + // Delete sample data + cy.deleteIndexByName(`${INDEX.SAMPLE_DATA_ECOMMERCE}`); }); }); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 0188b9925..260b1587c 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -74,10 +74,6 @@ Cypress.Commands.add('createMonitor', (monitorJSON) => { cy.request('POST', `${Cypress.env('opensearch')}${API.MONITOR_BASE}`, monitorJSON); }); -Cypress.Commands.add('createDestination', (destinationJSON) => { - cy.request('POST', `${Cypress.env('opensearch')}${API.DESTINATION_BASE}`, destinationJSON); -}); - Cypress.Commands.add('createAndExecuteMonitor', (monitorJSON) => { cy.request('POST', `${Cypress.env('opensearch')}${API.MONITOR_BASE}`, monitorJSON).then( (response) => { @@ -138,25 +134,6 @@ Cypress.Commands.add('deleteAllMonitors', () => { }); }); -Cypress.Commands.add('deleteAllDestinations', () => { - cy.request({ - method: 'GET', - url: `${Cypress.env('opensearch')}${API.DESTINATION_BASE}?size=200`, - failOnStatusCode: false, // In case there is no alerting config index in cluster, where the status code is 404 - }).then((response) => { - if (response.status === 200) { - for (let i = 0; i < response.body.totalDestinations; i++) { - cy.request( - 'DELETE', - `${Cypress.env('opensearch')}${API.DESTINATION_BASE}/${response.body.destinations[i].id}` - ); - } - } else { - cy.log('Failed to get all destinations.', response); - } - }); -}); - Cypress.Commands.add('createIndexByName', (indexName) => { cy.request('PUT', `${Cypress.env('opensearch')}/${indexName}`); }); diff --git a/public/app.js b/public/app.js index 6d5e98abc..3f0049057 100644 --- a/public/app.js +++ b/public/app.js @@ -12,10 +12,14 @@ import 'react-vis/dist/style.css'; // import './less/main.less'; import Main from './pages/Main'; import { CoreContext } from './utils/CoreContext'; +import { ServicesContext, NotificationService } from './services'; export function renderApp(coreStart, params) { const isDarkMode = coreStart.uiSettings.get('theme:darkMode') || false; + const http = coreStart.http; coreStart.chrome.setBreadcrumbs([{ text: 'Alerting' }]); // Set Breadcrumbs for the plugin + const notificationService = new NotificationService(http); + const services = { notificationService }; // Load Chart's dark mode CSS if (isDarkMode) { @@ -27,11 +31,13 @@ export function renderApp(coreStart, params) { // render react to DOM ReactDOM.render( - -
} /> - + + +
} /> + + , params.element ); diff --git a/public/components/Flyout/flyouts/components/AlertsDashboardFlyoutComponent.js b/public/components/Flyout/flyouts/components/AlertsDashboardFlyoutComponent.js index 9d72546d8..730e2b535 100644 --- a/public/components/Flyout/flyouts/components/AlertsDashboardFlyoutComponent.js +++ b/public/components/Flyout/flyouts/components/AlertsDashboardFlyoutComponent.js @@ -14,6 +14,8 @@ import { EuiIcon, EuiLink, EuiSpacer, + EuiTab, + EuiTabs, EuiText, } from '@elastic/eui'; import { getTime } from '../../../../pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats'; @@ -27,7 +29,6 @@ import { SEARCH_TYPE, } from '../../../../utils/constants'; import { TRIGGER_TYPE } from '../../../../pages/CreateTrigger/containers/CreateTrigger/utils/constants'; -import { SEVERITY_OPTIONS } from '../../../../pages/CreateTrigger/containers/DefineTrigger/DefineTrigger'; import { UNITS_OF_TIME } from '../../../../pages/CreateMonitor/components/MonitorExpressions/expressions/utils/constants'; import { DEFAULT_WHERE_EXPRESSION_TEXT } from '../../../../pages/CreateMonitor/components/MonitorExpressions/expressions/utils/whereHelpers'; import { backendErrorNotification } from '../../../../utils/helpers'; @@ -45,14 +46,18 @@ import { queryColumns } from '../../../../pages/Dashboard/utils/tableUtils'; import { DEFAULT_PAGE_SIZE_OPTIONS } from '../../../../pages/Monitors/containers/Monitors/utils/constants'; import queryString from 'query-string'; import { MAX_ALERT_COUNT } from '../../../../pages/Dashboard/utils/constants'; +import { SEVERITY_OPTIONS } from '../../../../pages/CreateTrigger/utils/constants'; +import { TABLE_TAB_IDS } from '../../../../pages/Dashboard/components/FindingsDashboard/utils'; +import FindingsDashboard from '../../../../pages/Dashboard/containers/FindingsDashboard'; export const DEFAULT_NUM_FLYOUT_ROWS = 10; export default class AlertsDashboardFlyoutComponent extends Component { constructor(props) { super(props); - const { location, monitor_id } = this.props; - + const { location, monitors, monitor_id } = this.props; + const monitor = _.get(_.find(monitors, { _id: monitor_id }), '_source'); + const monitorType = _.get(monitor, 'monitor_type', MONITOR_TYPE.QUERY_LEVEL); const { alertState, from, @@ -67,8 +72,9 @@ export default class AlertsDashboardFlyoutComponent extends Component { alerts: [], alertState: alertState, loading: true, - monitors: [], + monitor: monitor, monitorIds: [monitor_id], + monitorType: monitorType, page: Math.floor(from / size), search: search, selectable: true, @@ -77,6 +83,8 @@ export default class AlertsDashboardFlyoutComponent extends Component { size: DEFAULT_NUM_FLYOUT_ROWS, sortDirection: sortDirection, sortField: sortField, + tabContent: undefined, + tabId: TABLE_TAB_IDS.ALERTS.id, totalAlerts: 0, }; } @@ -129,6 +137,12 @@ export default class AlertsDashboardFlyoutComponent extends Component { monitorIds ); } + const { monitorType } = this.state; + if ( + monitorType === MONITOR_TYPE.DOC_LEVEL && + !_.isEqual(prevState.selectedItems, this.state.selectedItems) + ) + this.setState({ tabContent: this.renderAlertsTable() }); } getBucketLevelGraphConditions = (trigger) => { @@ -153,7 +167,7 @@ export default class AlertsDashboardFlyoutComponent extends Component { }; getAlerts = async () => { - this.setState({ ...this.state, loading: true }); + this.setState({ loading: true }); const { from, search, @@ -193,9 +207,9 @@ export default class AlertsDashboardFlyoutComponent extends Component { console.log('error getting alerts:', resp); backendErrorNotification(notifications, 'get', 'alerts', resp.err); } + this.setState({ tabContent: this.renderAlertsTable() }); }); - - this.setState({ ...this.state, loading: false }); + this.setState({ loading: false }); }; acknowledgeAlerts = async () => { @@ -249,7 +263,8 @@ export default class AlertsDashboardFlyoutComponent extends Component { alertState, monitorIds ); - this.setState({ ...this.state, selectedItems: [] }); + this.setState({ selectedItems: [], tabContent: undefined }); + this.setState({ tabContent: this.renderAlertsTable() }); this.props.refreshDashboard(); }; @@ -284,89 +299,24 @@ export default class AlertsDashboardFlyoutComponent extends Component { this.setState({ alerts }); }; - render() { - const { - last_notification_time, - loadingMonitors, - monitors, - monitor_id, - monitor_name, - start_time, - triggerID, - trigger_name, - } = this.props; - const monitor = _.get(_.find(monitors, { _id: monitor_id }), '_source'); - const monitorType = _.get(monitor, 'monitor_type', MONITOR_TYPE.QUERY_LEVEL); - const searchType = _.get(monitor, 'ui_metadata.search.searchType', SEARCH_TYPE.GRAPH); - const detectorId = _.get(monitor, MONITOR_INPUT_DETECTOR_ID); - - const triggerType = - monitorType === MONITOR_TYPE.BUCKET_LEVEL - ? TRIGGER_TYPE.BUCKET_LEVEL - : TRIGGER_TYPE.QUERY_LEVEL; - - let trigger = _.get(monitor, 'triggers', []).find( - (trigger) => trigger[triggerType].id === triggerID - ); - trigger = _.get(trigger, triggerType); + getTriggerType() { + const { monitorType } = this.state; + switch (monitorType) { + case MONITOR_TYPE.BUCKET_LEVEL: + return TRIGGER_TYPE.BUCKET_LEVEL; + case MONITOR_TYPE.DOC_LEVEL: + return TRIGGER_TYPE.DOC_LEVEL; + default: + return TRIGGER_TYPE.QUERY_LEVEL; + } + } - const severity = _.get(trigger, 'severity'); + renderAlertsTable() { + const { trigger_name } = this.props; + const { monitor, monitorType } = this.state; + const detectorId = _.get(monitor, MONITOR_INPUT_DETECTOR_ID); const groupBy = _.get(monitor, MONITOR_GROUP_BY); - const condition = - monitorType === MONITOR_TYPE.BUCKET_LEVEL && searchType === SEARCH_TYPE.GRAPH - ? this.getBucketLevelGraphConditions(trigger) - : _.get(trigger, 'condition.script.source', '-'); - - const filters = - monitorType === MONITOR_TYPE.BUCKET_LEVEL && searchType === SEARCH_TYPE.GRAPH - ? this.getBucketLevelGraphFilter(trigger) - : '-'; - - const bucketValue = _.get(monitor, 'ui_metadata.search.bucketValue'); - let bucketUnitOfTime = _.get(monitor, 'ui_metadata.search.bucketUnitOfTime'); - UNITS_OF_TIME.map((entry) => { - if (entry.value === bucketUnitOfTime) bucketUnitOfTime = entry.text; - }); - const timeRangeForLast = - bucketValue !== undefined && !_.isEmpty(bucketUnitOfTime) - ? `${bucketValue} ${bucketUnitOfTime}` - : '-'; - - const actions = () => { - const { selectedItems } = this.state; - const actions = [ - - Acknowledge - , - ]; - if (!_.isEmpty(detectorId)) { - actions.unshift( - - View detector - - ); - } - return actions; - }; - - 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; - } - }; - const { alerts, alertState, @@ -374,6 +324,7 @@ export default class AlertsDashboardFlyoutComponent extends Component { page, search, selectable, + selectedItems, severityLevel, size, sortDirection, @@ -381,16 +332,26 @@ export default class AlertsDashboardFlyoutComponent extends Component { totalAlerts, } = this.state; - const columnType = () => { - let columns = []; + const getItemId = (item) => { switch (monitorType) { case MONITOR_TYPE.QUERY_LEVEL: case MONITOR_TYPE.CLUSTER_METRICS: - columns = queryColumns; - break; + case MONITOR_TYPE.DOC_LEVEL: + return `${item.id}-${item.version}`; + case MONITOR_TYPE.BUCKET_LEVEL: + return item.id; + } + }; + + const columnType = () => { + let columns; + switch (monitorType) { case MONITOR_TYPE.BUCKET_LEVEL: columns = insertGroupByColumn(groupBy); break; + default: + columns = queryColumns; + break; } return removeColumns(['severity', 'trigger_name'], columns); }; @@ -403,6 +364,7 @@ export default class AlertsDashboardFlyoutComponent extends Component { }; const selection = { + initialSelected: selectedItems, onSelectionChange: this.onSelectionChange, selectable: (item) => item.state === ALERT_STATE.ACTIVE, selectableMessage: (selectable) => @@ -416,8 +378,153 @@ export default class AlertsDashboardFlyoutComponent extends Component { }, }; + const actions = () => { + const actions = [ + + Acknowledge + , + ]; + if (!_.isEmpty(detectorId)) { + actions.unshift( + + View detector + + ); + } + return actions; + }; + const trimmedAlerts = alerts.slice(page * size, page * size + size); + return ( + + + + + + ); + } + renderFindingsTable() { + const { httpClient, history, location, monitor_id, notifications } = this.props; + return ( + + ); + } + + renderTableTabs() { + const { tabId } = this.state; + const tabs = [ + { ...TABLE_TAB_IDS.ALERTS, content: this.renderAlertsTable() }, + { ...TABLE_TAB_IDS.FINDINGS, content: this.renderFindingsTable() }, + ]; + + return tabs.map((tab, index) => ( + { + this.setState({ + tabId: tab.id, + tabContent: tab.content, + }); + }} + > + {tab.name} + + )); + } + + render() { + const { + last_notification_time, + loadingMonitors, + monitor_id, + monitor_name, + start_time, + triggerID, + trigger_name, + } = this.props; + const { loading, monitor, monitorType, tabContent } = this.state; + const searchType = _.get(monitor, 'ui_metadata.search.searchType', SEARCH_TYPE.GRAPH); + const triggerType = this.getTriggerType(monitorType); + + let trigger = _.get(monitor, 'triggers', []).find( + (trigger) => trigger[triggerType].id === triggerID + ); + trigger = _.get(trigger, triggerType); + + const severity = _.get(trigger, 'severity'); + const groupBy = _.get(monitor, MONITOR_GROUP_BY); + + const condition = + (searchType === SEARCH_TYPE.GRAPH && monitorType === MONITOR_TYPE.BUCKET_LEVEL) || + MONITOR_TYPE.DOC_LEVEL + ? this.getBucketLevelGraphConditions(trigger) + : _.get(trigger, 'condition.script.source', '-'); + + const filters = + monitorType === MONITOR_TYPE.BUCKET_LEVEL && searchType === SEARCH_TYPE.GRAPH + ? this.getBucketLevelGraphFilter(trigger) + : '-'; + + const bucketValue = _.get(monitor, 'ui_metadata.search.bucketValue'); + let bucketUnitOfTime = _.get(monitor, 'ui_metadata.search.bucketUnitOfTime'); + UNITS_OF_TIME.map((entry) => { + if (entry.value === bucketUnitOfTime) bucketUnitOfTime = entry.text; + }); + const timeRangeForLast = + bucketValue !== undefined && !_.isEmpty(bucketUnitOfTime) + ? `${bucketValue} ${bucketUnitOfTime}` + : '-'; + + const displayTableTabs = monitorType === MONITOR_TYPE.DOC_LEVEL; return (
@@ -481,82 +588,65 @@ export default class AlertsDashboardFlyoutComponent extends Component {

- - - Time range for the last -

{timeRangeForLast}

-
-
-
- - - - - - Filters -

{loadingMonitors || loading ? 'Loading filters...' : filters}

-
-
- - - Group by -

- {loadingMonitors || loading - ? 'Loading groups...' - : !_.isEmpty(groupBy) - ? _.join(_.orderBy(groupBy), ', ') - : '-'} -

-
-
+ {monitorType !== MONITOR_TYPE.DOC_LEVEL && ( + + + Time range for the last +

{timeRangeForLast}

+
+
+ )}
- + {monitorType !== MONITOR_TYPE.DOC_LEVEL && ( +
+ + + + + + Filters +

{loadingMonitors || loading ? 'Loading filters...' : filters}

+
+
+ + + Group by +

+ {loadingMonitors || loading + ? 'Loading groups...' + : !_.isEmpty(groupBy) + ? _.join(_.orderBy(groupBy), ', ') + : '-'} +

+
+
+
+
+ )} - - - - - - - - - - - + + + + + {displayTableTabs ? ( +
+ {this.renderTableTabs()} + {tabContent} +
+ ) : ( + this.renderAlertsTable() + )}
); diff --git a/public/components/Flyout/flyouts/components/__snapshots__/AlertsDashboardFlyoutComponent.test.js.snap b/public/components/Flyout/flyouts/components/__snapshots__/AlertsDashboardFlyoutComponent.test.js.snap index 9e8cd1f3d..765fb84f6 100644 --- a/public/components/Flyout/flyouts/components/__snapshots__/AlertsDashboardFlyoutComponent.test.js.snap +++ b/public/components/Flyout/flyouts/components/__snapshots__/AlertsDashboardFlyoutComponent.test.js.snap @@ -114,153 +114,158 @@ exports[`AlertsDashboardFlyoutComponent renders 1`] = ` +
+ + + + + + Filters + +

+ Loading filters... +

+
+
+ + + + Group by + +

+ Loading groups... +

+
+
+
+
- - - - - Filters - -

- Loading filters... -

-
-
- - - - Group by - -

- Loading groups... -

-
-
-
- - - - Acknowledge - , - ] - } - bodyStyles={ + + + Acknowledge + , + ] + } + bodyStyles={ + Object { + "padding": "initial", + } + } + title="Alerts" + titleSize="s" + > + + + - - - - - - + } + responsive={true} + selection={ + Object { + "initialSelected": Array [], + "onSelectionChange": [Function], + "selectable": [Function], + "selectableMessage": [Function], + } + } + sorting={ + Object { + "sort": Object { + "direction": "desc", + "field": "start_time", + }, + } + } + tableLayout="fixed" + /> + diff --git a/public/models/interfaces.ts b/public/models/interfaces.ts new file mode 100644 index 000000000..ae48e39a9 --- /dev/null +++ b/public/models/interfaces.ts @@ -0,0 +1,10 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NotificationService } from '../services'; + +export interface BrowserServices { + notificationService: NotificationService; +} diff --git a/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/ConfigureDocumentLevelQueries.js b/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/ConfigureDocumentLevelQueries.js new file mode 100644 index 000000000..499b42fe5 --- /dev/null +++ b/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/ConfigureDocumentLevelQueries.js @@ -0,0 +1,68 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Component } from 'react'; +import _ from 'lodash'; +import { connect, FieldArray } from 'formik'; +import { EuiButton, EuiSpacer } from '@elastic/eui'; +import { inputLimitText } from '../../../../utils/helpers'; +import DocumentLevelQuery, { getInitialQueryValues } from './DocumentLevelQuery'; + +export const MAX_QUERIES = 10; // TODO DRAFT: Placeholder limit + +class ConfigureDocumentLevelQueries extends Component { + constructor(props) { + super(props); + this.state = {}; + } + + renderQueries = (arrayHelpers) => { + const { + dataTypes, + formik: { values }, + } = this.props; + if (_.isEmpty(values.queries)) arrayHelpers.push(_.cloneDeep(getInitialQueryValues())); + const numOfQueries = values.queries.length; + return ( +
+ {values.queries.map((query, index) => { + return ( + + ); + })} + +
+ arrayHelpers.push(_.cloneDeep(getInitialQueryValues(numOfQueries)))} + disabled={numOfQueries >= MAX_QUERIES} + > + {numOfQueries === 0 ? 'Add query' : 'Add another query'} + + + {inputLimitText(numOfQueries, MAX_QUERIES, 'query', 'queries')} +
+
+ ); + }; + + render() { + return ( + + {(arrayHelpers) => this.renderQueries(arrayHelpers)} + + ); + } +} + +export default connect(ConfigureDocumentLevelQueries); diff --git a/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/ConfigureDocumentLevelQueryTags.js b/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/ConfigureDocumentLevelQueryTags.js new file mode 100644 index 000000000..ddca82962 --- /dev/null +++ b/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/ConfigureDocumentLevelQueryTags.js @@ -0,0 +1,67 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Component } from 'react'; +import { connect, FieldArray } from 'formik'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { inputLimitText } from '../../../../utils/helpers'; +import DocumentLevelQueryTag from './DocumentLevelQueryTag'; + +export const MAX_TAGS = 10; // TODO DRAFT: Placeholder limit + +class ConfigureDocumentLevelQueryTags extends Component { + constructor(props) { + super(props); + this.state = {}; + } + + renderTags(arrayHelpers) { + const { + formik: { values }, + formFieldName = '', + query, + queryIndex, + } = this.props; + const numOfTags = query.tags.length; + return ( +
+ {values.queries[queryIndex].tags.map((tag, index) => { + return ( + + + + ); + })} +
+ arrayHelpers.push('')} + disabled={numOfTags >= MAX_TAGS} + style={{ paddingTop: '5px' }} + > + + Add tag + + {inputLimitText(numOfTags, MAX_TAGS, 'tag', 'tags')} +
+
+ ); + } + + render() { + const { formFieldName } = this.props; + return ( + + {(arrayHelpers) => this.renderTags(arrayHelpers)} + + ); + } +} + +export default connect(ConfigureDocumentLevelQueryTags); diff --git a/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/DocumentLevelQuery.js b/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/DocumentLevelQuery.js new file mode 100644 index 000000000..d7516b9fe --- /dev/null +++ b/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/DocumentLevelQuery.js @@ -0,0 +1,156 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Component } from 'react'; +import _ from 'lodash'; +import { connect } from 'formik'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { FormikFieldText, FormikComboBox, FormikSelect } from '../../../../components/FormControls'; +import { hasError, isInvalid, required } from '../../../../utils/validate'; +import { FORMIK_INITIAL_DOCUMENT_LEVEL_QUERY_VALUES } from '../../containers/CreateMonitor/utils/constants'; +import { DOC_LEVEL_TAG_TOOLTIP } from './DocumentLevelQueryTag'; +import IconToolTip from '../../../../components/IconToolTip'; +import ConfigureDocumentLevelQueryTags from './ConfigureDocumentLevelQueryTags'; +import { getIndexFields } from '../MonitorExpressions/expressions/utils/dataTypes'; + +const ALLOWED_DATA_TYPES = ['number', 'text', 'keyword', 'boolean']; + +export const QUERY_OPERATORS = [ + { text: 'is', value: '==' }, + { text: 'is not', value: '!=' }, +]; + +export const getInitialQueryValues = (queryIndexNum = 0) => + _.cloneDeep({ + ...FORMIK_INITIAL_DOCUMENT_LEVEL_QUERY_VALUES, + queryName: `Query ${queryIndexNum + 1}`, + }); + +class DocumentLevelQuery extends Component { + constructor(props) { + super(props); + this.state = {}; + } + + render() { + const { dataTypes, formFieldName = '', query, queryIndex, queriesArrayHelpers } = this.props; + return ( +
+ + + + + + {queryIndex > 0 && ( + + queriesArrayHelpers.remove(queryIndex)}> + Remove query + + + )} + + + + + + + form.setFieldValue(field.name, e[0].label), + onBlur: (e, field, form) => form.setFieldTouched(field.name, true), + singleSelection: { asPlainText: true }, + }} + /> + + + + field.onChange(e), + options: QUERY_OPERATORS, + }} + /> + + + + + + + + + + + Tags + - optional + + + + + {_.isEmpty(query.tags) && ( +
+ No tags defined. +
+ )} + + + + +
+ ); + } +} + +export default connect(DocumentLevelQuery); diff --git a/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/DocumentLevelQueryTag.js b/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/DocumentLevelQueryTag.js new file mode 100644 index 000000000..a2b92ac6c --- /dev/null +++ b/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/DocumentLevelQueryTag.js @@ -0,0 +1,123 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Component } from 'react'; +import _ from 'lodash'; +import { connect } from 'formik'; +import { + EuiBadge, + EuiButtonEmpty, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiPopoverTitle, + EuiSpacer, +} from '@elastic/eui'; +import { FormikFieldText } from '../../../../components/FormControls'; +import { hasError, isInvalid, required } from '../../../../utils/validate'; +import { EXPRESSION_STYLE, POPOVER_STYLE } from '../MonitorExpressions/expressions/utils/constants'; + +export const DOC_LEVEL_TAG_TOOLTIP = 'Tags to associate with your queries.'; // TODO DRAFT: Placeholder wording +export const TAG_PLACEHOLDER_TEXT = 'Enter the search term'; // TODO DRAFT: Placeholder wording + +class DocumentLevelQueryTag extends Component { + constructor(props) { + super(props); + const { tag } = props; + this.state = { + isPopoverOpen: _.isEmpty(tag), + }; + this.closePopover = this.closePopover.bind(this); + this.openPopover = this.openPopover.bind(this); + } + + closePopover() { + const { arrayHelpers, tag, tagIndex } = this.props; + if (_.isEmpty(tag)) arrayHelpers.remove(tagIndex); + this.setState({ isPopoverOpen: false }); + } + + openPopover() { + this.setState({ isPopoverOpen: true }); + } + + renderPopover() { + const { formFieldName } = this.props; + return ( +
+ + + + + Cancel + + + + Save + + + +
+ ); + } + + render() { + const { arrayHelpers, tag = '', tagIndex = 0 } = this.props; + const { isPopoverOpen } = this.state; + return ( + + arrayHelpers.remove(tagIndex)} + iconOnClickAriaLabel={'Remove tag'} + onClick={this.openPopover} + onClickAriaLabel={'Edit tag'} + > + {_.isEmpty(tag) ? TAG_PLACEHOLDER_TEXT : tag} + + + } + isOpen={isPopoverOpen} + closePopover={this.closePopover} + panelPaddingSize={'none'} + ownFocus + withTitle + anchorPosition={'downLeft'} + > + ADD TAG + {this.renderPopover()} + + ); + } +} + +export default connect(DocumentLevelQueryTag); diff --git a/public/pages/CreateMonitor/components/MonitorType/MonitorType.js b/public/pages/CreateMonitor/components/MonitorType/MonitorType.js index 62f42f119..03a8a39d9 100644 --- a/public/pages/CreateMonitor/components/MonitorType/MonitorType.js +++ b/public/pages/CreateMonitor/components/MonitorType/MonitorType.js @@ -5,20 +5,32 @@ import React from 'react'; import _ from 'lodash'; -import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { EuiFlexGrid, EuiFlexItem, EuiText } from '@elastic/eui'; import FormikCheckableCard from '../../../../components/FormControls/FormikCheckableCard'; import { MONITOR_TYPE, SEARCH_TYPE } from '../../../../utils/constants'; import { FORMIK_INITIAL_TRIGGER_VALUES } from '../../../CreateTrigger/containers/CreateTrigger/utils/constants'; +import { + DEFAULT_DOCUMENT_LEVEL_QUERY, + FORMIK_INITIAL_VALUES, +} from '../../containers/CreateMonitor/utils/constants'; -export const MONITOR_TYPE_CARD_WIDTH = 400; +export const MONITOR_TYPE_CARD_WIDTH = 400; // TODO DRAFT: Determine width const onChangeDefinition = (e, form) => { const type = e.target.value; form.setFieldValue('monitor_type', type); - // Clearing trigger definitions when changing monitor types. + // Clearing various form fields when changing monitor types. // TODO: Implement modal that confirms the change before clearing. + form.setFieldValue('index', FORMIK_INITIAL_VALUES.index); form.setFieldValue('triggerDefinitions', FORMIK_INITIAL_TRIGGER_VALUES.triggerConditions); + switch (type) { + case MONITOR_TYPE.DOC_LEVEL: + form.setFieldValue('query', DEFAULT_DOCUMENT_LEVEL_QUERY); + break; + default: + form.setFieldValue('query', FORMIK_INITIAL_VALUES.query); + } }; const queryLevelDescription = ( @@ -41,15 +53,19 @@ const clusterMetricsDescription = ( ); +const documentLevelDescription = ( // TODO DRAFT: confirm wording + + Per document monitors allow you to run queries on new documents as they're indexed. + +); + const MonitorType = ({ values }) => ( - + ( ( ( }} /> - + + { + onChangeDefinition(e, form); + }, + children: documentLevelDescription, + 'data-test-subj': 'docLevelMonitorRadioCard', + }} + /> + + ); export default MonitorType; 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 290af85b3..a655e4c97 100644 --- a/public/pages/CreateMonitor/components/MonitorType/__snapshots__/MonitorType.test.js.snap +++ b/public/pages/CreateMonitor/components/MonitorType/__snapshots__/MonitorType.test.js.snap @@ -2,7 +2,8 @@ exports[`MonitorType renders 1`] = `
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+ Per document monitors allow you to run queries on new documents as they're indexed. +
+
+
+
+
+
+
+
`; 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 83fd74f79..9a5d9e711 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 @@ -10,6 +10,7 @@ exports[`AnomalyDetectors renders 1`] = ` "bucketValue": 1, "cronExpression": "0 */1 * * *", "daily": 0, + "description": "", "detectorId": "", "disabled": false, "fieldName": Array [], @@ -34,6 +35,7 @@ exports[`AnomalyDetectors renders 1`] = ` "interval": 1, "unit": "MINUTES", }, + "queries": Array [], "query": "{ \\"size\\": 0, \\"query\\": { @@ -78,6 +80,7 @@ exports[`AnomalyDetectors renders 1`] = ` "bucketValue": 1, "cronExpression": "0 */1 * * *", "daily": 0, + "description": "", "detectorId": "", "disabled": false, "fieldName": Array [], @@ -102,6 +105,7 @@ exports[`AnomalyDetectors renders 1`] = ` "interval": 1, "unit": "MINUTES", }, + "queries": Array [], "query": "{ \\"size\\": 0, \\"query\\": { @@ -207,6 +211,7 @@ exports[`AnomalyDetectors renders 1`] = ` "bucketValue": 1, "cronExpression": "0 */1 * * *", "daily": 0, + "description": "", "detectorId": "", "disabled": false, "fieldName": Array [], @@ -231,6 +236,7 @@ exports[`AnomalyDetectors renders 1`] = ` "interval": 1, "unit": "MINUTES", }, + "queries": Array [], "query": "{ \\"size\\": 0, \\"query\\": { @@ -294,6 +300,7 @@ exports[`AnomalyDetectors renders 1`] = ` "bucketValue": 1, "cronExpression": "0 */1 * * *", "daily": 0, + "description": "", "detectorId": "", "disabled": false, "fieldName": Array [], @@ -318,6 +325,7 @@ exports[`AnomalyDetectors renders 1`] = ` "interval": 1, "unit": "MINUTES", }, + "queries": Array [], "query": "{ \\"size\\": 0, \\"query\\": { @@ -429,6 +437,7 @@ exports[`AnomalyDetectors renders 1`] = ` "bucketValue": 1, "cronExpression": "0 */1 * * *", "daily": 0, + "description": "", "detectorId": "", "disabled": false, "fieldName": Array [], @@ -453,6 +462,7 @@ exports[`AnomalyDetectors renders 1`] = ` "interval": 1, "unit": "MINUTES", }, + "queries": Array [], "query": "{ \\"size\\": 0, \\"query\\": { @@ -516,6 +526,7 @@ exports[`AnomalyDetectors renders 1`] = ` "bucketValue": 1, "cronExpression": "0 */1 * * *", "daily": 0, + "description": "", "detectorId": "", "disabled": false, "fieldName": Array [], @@ -540,6 +551,7 @@ exports[`AnomalyDetectors renders 1`] = ` "interval": 1, "unit": "MINUTES", }, + "queries": Array [], "query": "{ \\"size\\": 0, \\"query\\": { diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js b/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js index 80f95a3a1..4741501a3 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js @@ -253,8 +253,17 @@ export default class CreateMonitor extends Component { } render() { + const { + edit, + history, + httpClient, + location, + monitorToEdit, + notifications, + isDarkMode, + notificationService, + } = this.props; const { initialValues, plugins } = this.state; - const { edit, httpClient, monitorToEdit, notifications, isDarkMode } = this.props; return (
@@ -269,6 +278,7 @@ export default class CreateMonitor extends Component { )} 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 f62889ba0..e3a90c975 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap +++ b/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap @@ -18,6 +18,7 @@ exports[`CreateMonitor renders 1`] = ` "bucketValue": 1, "cronExpression": "0 */1 * * *", "daily": 0, + "description": "", "detectorId": "", "disabled": false, "fieldName": Array [], @@ -42,6 +43,7 @@ exports[`CreateMonitor renders 1`] = ` "interval": 1, "unit": "MINUTES", }, + "queries": Array [], "query": "{ \\"size\\": 0, \\"query\\": { 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 250e4a6da..a5a54c682 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 @@ -115,11 +115,14 @@ Array [ exports[`formikToInputs can call formikToClusterMetricsUri 1`] = ` Object { - "uri": Object { - "api_type": "", - "path": "", - "path_params": "", - "url": "", + "search": Object { + "indices": Array [], + "query": Object { + "query": Object { + "match_all": Object {}, + }, + "size": 0, + }, }, } `; diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js index c16b6ea42..0f1437ad6 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js @@ -5,6 +5,7 @@ import { OPERATORS_MAP } from '../../../components/MonitorExpressions/expressions/utils/constants'; import { MONITOR_TYPE } from '../../../../../utils/constants'; +import { QUERY_OPERATORS } from '../../../components/DocumentLevelMonitorQueries/DocumentLevelQuery'; export const BUCKET_COUNT = 5; @@ -34,6 +35,8 @@ export const FORMIK_INITIAL_VALUES = { index: [], timeField: '', query: MATCH_ALL_QUERY, + queries: [], + description: '', aggregationType: 'count', fieldName: [], aggregations: [], @@ -59,6 +62,31 @@ export const FORMIK_INITIAL_AGG_VALUES = { fieldName: '', }; +export const FORMIK_INITIAL_DOCUMENT_LEVEL_QUERY_VALUES = { + id: undefined, + queryName: 'Query name', + field: '', + operator: QUERY_OPERATORS[0].value, + query: '', + tags: [], +}; + +// TODO DRAFT: Is this an appropriate default to display when defining as an extraction query? +export const DEFAULT_DOCUMENT_LEVEL_QUERY = JSON.stringify( + { + description: 'DESCRIPTION_TEXT', + queries: [ + { + name: 'QUERY_NAME', + query: { match_all: {} }, + tags: ['TAG_TEXT'], + }, + ], + }, + null, + 4 +); + export const DEFAULT_COMPOSITE_AGG_SIZE = 50; export const METRIC_TOOLTIP_TEXT = 'Extracted statistics such as simple calculations of data.'; diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js index 0c9fc50a5..2b63661a6 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js @@ -17,7 +17,19 @@ import { export function formikToMonitor(values) { const uiSchedule = formikToUiSchedule(values); const schedule = buildSchedule(values.frequency, uiSchedule); - const uiSearch = formikToUiSearch(values); + + const monitorUiMetadata = () => { + switch (values.monitor_type) { + case MONITOR_TYPE.DOC_LEVEL: + return { + doc_level_input: formikToDocLevelQueriesUiMetadata(values), + search: { searchType: values.searchType }, + }; + default: + return { search: formikToUiSearch(values) }; + } + }; + return { name: values.name, type: 'monitor', @@ -28,16 +40,18 @@ export function formikToMonitor(values) { triggers: [], ui_metadata: { schedule: uiSchedule, - search: uiSearch, monitor_type: values.monitor_type, + ...monitorUiMetadata(), }, }; } export function formikToInputs(values) { - switch (values.searchType) { - case SEARCH_TYPE.CLUSTER_METRICS: + switch (values.monitor_type) { + case MONITOR_TYPE.CLUSTER_METRICS: return formikToClusterMetricsInput(values); + case MONITOR_TYPE.DOC_LEVEL: + return formikToDocLevelInput(values); default: return formikToSearch(values); } @@ -169,10 +183,16 @@ export function formikToExtractionQuery(values) { export function formikToGraphQuery(values) { const { bucketValue, bucketUnitOfTime, monitor_type } = values; - const useComposite = monitor_type === MONITOR_TYPE.BUCKET_LEVEL; - const aggregation = useComposite - ? formikToCompositeAggregation(values) - : formikToAggregation(values); + + const aggregation = () => { + switch (monitor_type) { + case MONITOR_TYPE.BUCKET_LEVEL: + return formikToCompositeAggregation(values); + default: + return formikToAggregation(values); + } + }; + const timeField = values.timeField; const filters = [ { @@ -191,7 +211,7 @@ export function formikToGraphQuery(values) { } return { size: 0, - aggregations: aggregation, + aggregations: aggregation(), query: { bool: { filter: filters, @@ -200,6 +220,61 @@ export function formikToGraphQuery(values) { }; } +export function formikToDocLevelInput(values) { + let description = FORMIK_INITIAL_VALUES.description; + let indices = formikToIndices(values); + let queries = _.get(values, 'queries', FORMIK_INITIAL_VALUES.queries); + switch (values.searchType) { + case SEARCH_TYPE.GRAPH: + description = values.description; + queries = queries.map((query) => { + const formikToQuery = + query.operator === '==' + ? `${query.field}:\"${query.query}\"` + : JSON.stringify({ + bool: { must_not: { term: { [query.field]: `\"${query.query}\"` } } }, + }); + return { + // id: query.id, // TODO FIXME: Refactor to this assignment logic once backend generates its own ID value + id: query.queryName, + name: query.queryName, + query: formikToQuery, + tags: query.tags, + }; + }); + break; + case SEARCH_TYPE.QUERY: + let query = _.get(values, 'query', ''); + try { + query = JSON.parse(query); + description = _.get(query, 'description', description); + queries = _.get(query, 'queries', queries); + } catch (e) { + /* Ignore JSON parsing errors as users may just be configuring the query */ + } + break; + default: + console.log( + `Unsupported searchType found for ${MONITOR_TYPE.DOC_LEVEL}: ${JSON.stringify( + values.searchType + )}`, + values.searchType + ); + } + + return { + doc_level_input: { + description: description, + indices: indices, + queries: queries, + }, + }; +} + +export function formikToDocLevelQueriesUiMetadata(values) { + return { queries: _.get(values, 'queries', []) }; +} + export function formikToCompositeAggregation(values) { const { aggregations, groupBy } = values; diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js index c97adf1fe..250ef48a1 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js @@ -4,8 +4,8 @@ */ import _ from 'lodash'; -import { FORMIK_INITIAL_VALUES } from './constants'; -import { SEARCH_TYPE, INPUTS_DETECTOR_ID } from '../../../../../utils/constants'; +import { FORMIK_INITIAL_DOCUMENT_LEVEL_QUERY_VALUES, FORMIK_INITIAL_VALUES } from './constants'; +import { SEARCH_TYPE, INPUTS_DETECTOR_ID, MONITOR_TYPE } from '../../../../../utils/constants'; // Convert Monitor JSON to Formik values used in UI forms export default function monitorToFormik(monitor) { @@ -21,10 +21,26 @@ 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 - let { searchType = 'query', fieldName } = search; - if (_.isEmpty(search) && 'uri' in inputs[0]) searchType = SEARCH_TYPE.CLUSTER_METRICS; + const { searchType = 'query', fieldName } = search; const isAD = searchType === SEARCH_TYPE.AD; - const isClusterMetrics = searchType === SEARCH_TYPE.CLUSTER_METRICS; + + const monitorInputs = () => { + switch (monitor_type) { + case MONITOR_TYPE.CLUSTER_METRICS: + return { + index: FORMIK_INITIAL_VALUES.index, + uri: inputs[0].uri, + }; + case MONITOR_TYPE.DOC_LEVEL: + return docLevelInputToFormik(monitor); + default: + return { + index: indicesToFormik(inputs[0].search.indices), + query: JSON.stringify(inputs[0].search.query, null, 4), + }; + } + }; + return { /* INITIALIZE WITH DEFAULTS */ ...formikValues, @@ -38,17 +54,63 @@ export default function monitorToFormik(monitor) { cronExpression, /* DEFINE MONITOR */ + ...monitorInputs(), monitor_type, ...search, searchType, fieldName: fieldName ? [{ label: fieldName }] : [], timezone: timezone ? [{ label: timezone }] : [], - detectorId: isAD ? _.get(inputs, INPUTS_DETECTOR_ID) : undefined, - 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, + adResultIndex: isAD ? _.get(inputs, '0.search.indices.0') : undefined, + }; +} + +export function docLevelInputToFormik(monitor) { + const input = monitor.inputs[0]['doc_level_input']; + const { description, indices, queries } = input; + return { + description: description, // TODO DRAFT: DocLevelInput 'description' field isn't currently represented in the mocks. Remove it from frontend? + index: indicesToFormik(indices), + query: JSON.stringify(_.omit(input, 'indices'), null, 4), + queries: queriesToFormik(queries), }; } + +export function queriesToFormik(queries) { + return queries.map((query) => { + let querySource = ''; + try { + querySource = JSON.parse(query.query); + } catch (e) { + querySource = query.query; + } + + const parsedQuerySource = {}; + const usesIsNotOperator = _.has(querySource, 'bool'); + const operator = usesIsNotOperator ? '!=' : '=='; + + if (usesIsNotOperator) { + const term = _.get(querySource, 'bool.must_not.term'); + const field = _.keys(term)[0]; + parsedQuerySource['field'] = _.trim(field, '":'); + parsedQuerySource['query'] = _.trim(term[field], '"'); + } else { + const splitQuery = _.split(querySource, '"'); + parsedQuerySource['field'] = _.trim(splitQuery[0], '":'); + parsedQuerySource['query'] = _.trim(splitQuery[1], '"'); + } + + return { + ...FORMIK_INITIAL_DOCUMENT_LEVEL_QUERY_VALUES, + id: query.id, + queryName: query.name, + tags: query.tags, + operator: operator, + ...parsedQuerySource, + }; + }); +} + +export function indicesToFormik(indices) { + return indices.map((index) => ({ label: index })); +} diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.test.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.test.js index d26f84283..8ddf26954 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.test.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.test.js @@ -7,6 +7,7 @@ import _ from 'lodash'; import monitorToFormik from './monitorToFormik'; import { FORMIK_INITIAL_VALUES, MATCH_ALL_QUERY } from './constants'; +import { MONITOR_TYPE, SEARCH_TYPE } from '../../../../../utils/constants'; const exampleMonitor = { name: 'Example Monitor', @@ -156,7 +157,8 @@ describe('monitorToFormik', () => { describe('can build ClusterMetricsMonitor', () => { test('with path params', () => { const clusterMetricsMonitor = _.cloneDeep(exampleMonitor); - clusterMetricsMonitor.ui_metadata.search.searchType = 'clusterMetrics'; + clusterMetricsMonitor.monitor_type = MONITOR_TYPE.CLUSTER_METRICS; + clusterMetricsMonitor.ui_metadata.search.searchType = SEARCH_TYPE.CLUSTER_METRICS; clusterMetricsMonitor.inputs = [ { uri: { @@ -171,7 +173,8 @@ describe('monitorToFormik', () => { }); test('without path params', () => { const clusterMetricsMonitor = _.cloneDeep(exampleMonitor); - clusterMetricsMonitor.ui_metadata.search.searchType = 'clusterMetrics'; + clusterMetricsMonitor.monitor_type = MONITOR_TYPE.CLUSTER_METRICS; + clusterMetricsMonitor.ui_metadata.search.searchType = SEARCH_TYPE.CLUSTER_METRICS; clusterMetricsMonitor.inputs = [ { uri: { diff --git a/public/pages/CreateMonitor/containers/DataSource/DataSource.js b/public/pages/CreateMonitor/containers/DataSource/DataSource.js index 7c0496027..0300d441f 100644 --- a/public/pages/CreateMonitor/containers/DataSource/DataSource.js +++ b/public/pages/CreateMonitor/containers/DataSource/DataSource.js @@ -9,7 +9,7 @@ import { EuiSpacer } from '@elastic/eui'; import MonitorIndex from '../MonitorIndex'; import MonitorTimeField from '../../components/MonitorTimeField'; import ContentPanel from '../../../../components/ContentPanel'; -import { SEARCH_TYPE } from '../../../../utils/constants'; +import { MONITOR_TYPE, SEARCH_TYPE } from '../../../../utils/constants'; const propTypes = { values: PropTypes.object.isRequired, @@ -30,8 +30,9 @@ class DataSource extends Component { } render() { - const { searchType } = this.props.values; - const isGraph = searchType === SEARCH_TYPE.GRAPH; + const { monitor_type, searchType } = this.props.values; + const displayTimeField = + searchType === SEARCH_TYPE.GRAPH && monitor_type !== MONITOR_TYPE.DOC_LEVEL; return ( - + - {isGraph && } + {displayTimeField && } ); } diff --git a/public/pages/CreateMonitor/containers/DataSource/__snapshots__/DataSource.test.js.snap b/public/pages/CreateMonitor/containers/DataSource/__snapshots__/DataSource.test.js.snap index da337a372..3182e6526 100644 --- a/public/pages/CreateMonitor/containers/DataSource/__snapshots__/DataSource.test.js.snap +++ b/public/pages/CreateMonitor/containers/DataSource/__snapshots__/DataSource.test.js.snap @@ -18,6 +18,7 @@ exports[`DataSource renders 1`] = ` > { + switch (values.monitor_type) { + case MONITOR_TYPE.DOC_LEVEL: + return ; + default: + return ; + } + }; + + const previewContent = () => { + switch (values.monitor_type) { + case MONITOR_TYPE.BUCKET_LEVEL: + return this.getBucketMonitorGraphs(aggregations, formikSnapshot, response); + case MONITOR_TYPE.DOC_LEVEL: + const { index, queries } = values; + return _.isEmpty(response) ? ( + renderEmptyMessage('Loading findings...') + ) : ( + + ); + default: + return ; + } + }; return ( - + {monitorExpressions()} - {errors.where ? ( - renderEmptyMessage('Invalid input in data filter. Remove data filter or adjust filter ') - ) : isBucketLevel ? ( - this.getBucketMonitorGraphs(aggregations, formikSnapshot, response) - ) : ( - - )} + {errors.where + ? renderEmptyMessage( + 'Invalid input in data filter. Remove data filter or adjust filter ' + ) + : previewContent()} @@ -238,7 +292,7 @@ class DefineMonitor extends Component { let requests; switch (searchType) { case SEARCH_TYPE.QUERY: - requests = [buildSearchRequest(values)]; + requests = [buildRequest(values)]; break; case SEARCH_TYPE.GRAPH: // TODO: Might need to check if groupBy is defined if monitor_type === Graph, and prevent onRunQuery() if no group by defined to avoid errors. @@ -246,14 +300,15 @@ class DefineMonitor extends Component { // 1. The actual query that will be saved on the monitor, to get accurate query performance stats // 2. The UI generated query that gets [BUCKET_COUNT] times the aggregated buckets to show past history of query // If the query is an extraction query, we can use the same query for results and query performance - requests = [buildSearchRequest(values)]; - requests.push(buildSearchRequest(values, false)); + requests = [buildRequest(values)]; + requests.push(buildRequest(values, false)); break; case SEARCH_TYPE.CLUSTER_METRICS: requests = [buildClusterMetricsRequest(values)]; break; } + const startTime = moment(); try { const promises = requests.map((request) => { // Fill in monitor name in case it's empty (in create workflow) @@ -266,7 +321,7 @@ class DefineMonitor extends Component { switch (searchType) { case SEARCH_TYPE.QUERY: case SEARCH_TYPE.GRAPH: - _.set(monitor, 'inputs[0].search', request); + _.set(monitor, 'inputs[0]', request); break; case SEARCH_TYPE.CLUSTER_METRICS: _.set(monitor, 'inputs[0].uri', request); @@ -283,12 +338,23 @@ class DefineMonitor extends Component { const [queryResponse, optionalResponse] = await Promise.all(promises); if (queryResponse.ok) { + const endTime = moment(); + const duration = moment.duration(endTime.diff(startTime)).milliseconds(); const response = _.get(queryResponse.resp, 'input_results.results[0]'); // If there is an optionalResponse use it's results, otherwise use the original response const performanceResponse = optionalResponse ? _.get(optionalResponse, 'resp.input_results.results[0]', null) : response; - this.setState({ response, formikSnapshot, performanceResponse }); + this.setState({ + response, + formikSnapshot, + // TODO FIXME: Doc level backend monitor run results don't include duration metric. Using this for now. + // This returns a much longer duration than other monitors, though. + performanceResponse: + values.monitor_type === MONITOR_TYPE.DOC_LEVEL + ? { ...performanceResponse, took: duration } + : performanceResponse, + }); } else { console.error('There was an error running the query', queryResponse.resp); backendErrorNotification(notifications, 'run', 'query', queryResponse.resp); @@ -336,11 +402,13 @@ class DefineMonitor extends Component { renderVisualMonitor() { const { values } = this.props; const { index, timeField } = values; - let content = null; + let content; + const supportsTimeField = values.monitor_type !== MONITOR_TYPE.DOC_LEVEL; if (index.length) { - content = timeField - ? this.renderGraph() - : renderEmptyMessage('You must specify a time field.'); + content = + _.isEmpty(timeField) && supportsTimeField + ? renderEmptyMessage('You must specify a time field.') + : this.renderGraph(); } else { content = renderEmptyMessage('You must specify an index.'); } @@ -527,7 +595,7 @@ class DefineMonitor extends Component { ? [ , 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 ca81b4831..294fc0c8b 100644 --- a/public/pages/CreateMonitor/containers/DefineMonitor/__snapshots__/DefineMonitor.test.js.snap +++ b/public/pages/CreateMonitor/containers/DefineMonitor/__snapshots__/DefineMonitor.test.js.snap @@ -15,6 +15,7 @@ exports[`DefineMonitor renders 1`] = ` "bucketValue": 1, "cronExpression": "0 */1 * * *", "daily": 0, + "description": "", "detectorId": "", "disabled": false, "fieldName": Array [], @@ -39,6 +40,7 @@ exports[`DefineMonitor renders 1`] = ` "interval": 1, "unit": "MINUTES", }, + "queries": Array [], "query": "{ \\"size\\": 0, \\"query\\": { @@ -158,7 +160,7 @@ exports[`DefineMonitor should show warning in case of Ad monitor and plugin is n color="warning" iconType="help" size="s" - title="Anomaly detector plugin is not installed on Opensearch, This monitor will not functional properly." + title="Anomaly detector plugin is not installed on OpenSearch, This monitor will not functional properly." /> +export const buildRequest = (values, uiGraphQuery = true) => values.searchType === SEARCH_TYPE.GRAPH ? buildGraphSearchRequest(values, uiGraphQuery) : buildQuerySearchRequest(values); function buildQuerySearchRequest(values) { - const indices = formikToIndices(values); - const query = JSON.parse(values.query); - return { query, indices }; + switch (values.monitor_type) { + case MONITOR_TYPE.DOC_LEVEL: + return formikToDocLevelInput(values); + default: + const indices = formikToIndices(values); + const query = JSON.parse(values.query); + return { search: { query, indices } }; + } } function buildGraphSearchRequest(values, uiGraphQuery) { - const query = uiGraphQuery ? formikToUiGraphQuery(values) : formikToGraphQuery(values); - const indices = formikToIndices(values); - return { query, indices }; + switch (values.monitor_type) { + case MONITOR_TYPE.DOC_LEVEL: + return formikToDocLevelInput(values); + default: + const query = uiGraphQuery ? formikToUiGraphQuery(values) : formikToGraphQuery(values); + const indices = formikToIndices(values); + return { search: { query, indices } }; + } } diff --git a/public/pages/CreateMonitor/containers/MonitorIndex/MonitorIndex.js b/public/pages/CreateMonitor/containers/MonitorIndex/MonitorIndex.js index f7878730c..ac424f2b4 100644 --- a/public/pages/CreateMonitor/containers/MonitorIndex/MonitorIndex.js +++ b/public/pages/CreateMonitor/containers/MonitorIndex/MonitorIndex.js @@ -11,6 +11,7 @@ import { EuiHealth, EuiHighlight } from '@elastic/eui'; import { FormikComboBox } from '../../../../components/FormControls'; import { validateIndex, hasError, isInvalid } from '../../../../utils/validate'; import { canAppendWildcard, createReasonableWait, getMatchedOptions } from './utils/helpers'; +import { MONITOR_TYPE } from '../../../../utils/constants'; const CustomOption = ({ option, searchValue, contentClassName }) => { const { health, label, index } = option; @@ -215,6 +216,8 @@ class MonitorIndex extends React.Component { false //isIncludingSystemIndices ); + const supportMultipleIndices = this.props.monitorType !== MONITOR_TYPE.DOC_LEVEL; + return ( 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 edc02cc3b..cc9024932 100644 --- a/public/pages/CreateMonitor/containers/MonitorIndex/__snapshots__/MonitorIndex.test.js.snap +++ b/public/pages/CreateMonitor/containers/MonitorIndex/__snapshots__/MonitorIndex.test.js.snap @@ -10,6 +10,7 @@ exports[`MonitorIndex renders 1`] = ` "bucketValue": 1, "cronExpression": "0 */1 * * *", "daily": 0, + "description": "", "detectorId": "", "disabled": false, "fieldName": Array [], @@ -34,6 +35,7 @@ exports[`MonitorIndex renders 1`] = ` "interval": 1, "unit": "MINUTES", }, + "queries": Array [], "query": "{ \\"size\\": 0, \\"query\\": { @@ -100,6 +102,7 @@ exports[`MonitorIndex renders 1`] = ` ], "placeholder": "Select indices", "renderOption": [Function], + "singleSelection": false, } } name="index" @@ -150,6 +153,7 @@ exports[`MonitorIndex renders 1`] = ` "bucketValue": 1, "cronExpression": "0 */1 * * *", "daily": 0, + "description": "", "detectorId": "", "disabled": false, "fieldName": Array [], @@ -174,6 +178,7 @@ exports[`MonitorIndex renders 1`] = ` "interval": 1, "unit": "MINUTES", }, + "queries": Array [], "query": "{ \\"size\\": 0, \\"query\\": { @@ -237,6 +242,7 @@ exports[`MonitorIndex renders 1`] = ` "bucketValue": 1, "cronExpression": "0 */1 * * *", "daily": 0, + "description": "", "detectorId": "", "disabled": false, "fieldName": Array [], @@ -261,6 +267,7 @@ exports[`MonitorIndex renders 1`] = ` "interval": 1, "unit": "MINUTES", }, + "queries": Array [], "query": "{ \\"size\\": 0, \\"query\\": { @@ -388,6 +395,7 @@ exports[`MonitorIndex renders 1`] = ` "bucketValue": 1, "cronExpression": "0 */1 * * *", "daily": 0, + "description": "", "detectorId": "", "disabled": false, "fieldName": Array [], @@ -412,6 +420,7 @@ exports[`MonitorIndex renders 1`] = ` "interval": 1, "unit": "MINUTES", }, + "queries": Array [], "query": "{ \\"size\\": 0, \\"query\\": { @@ -475,6 +484,7 @@ exports[`MonitorIndex renders 1`] = ` "bucketValue": 1, "cronExpression": "0 */1 * * *", "daily": 0, + "description": "", "detectorId": "", "disabled": false, "fieldName": Array [], @@ -499,6 +509,7 @@ exports[`MonitorIndex renders 1`] = ` "interval": 1, "unit": "MINUTES", }, + "queries": Array [], "query": "{ \\"size\\": 0, \\"query\\": { @@ -556,6 +567,7 @@ exports[`MonitorIndex renders 1`] = ` ], "placeholder": "Select indices", "renderOption": [Function], + "singleSelection": false, } } name="index" diff --git a/public/pages/CreateTrigger/components/Action/Action.js b/public/pages/CreateTrigger/components/Action/Action.js index f8bf7179c..70d9d3e20 100644 --- a/public/pages/CreateTrigger/components/Action/Action.js +++ b/public/pages/CreateTrigger/components/Action/Action.js @@ -11,32 +11,103 @@ import { EuiHorizontalRule, EuiPanel, EuiSpacer, + EuiFlexGroup, + EuiFlexItem, EuiText, } from '@elastic/eui'; import { FormikFieldText, FormikComboBox } from '../../../../components/FormControls'; import { isInvalid, hasError, validateActionName } from '../../../../utils/validate'; import { ActionsMap } from './utils/constants'; import { validateDestination } from './utils/validate'; -import { DEFAULT_ACTION_TYPE } from '../../utils/constants'; +import { DEFAULT_ACTION_TYPE, MANAGE_CHANNELS_PATH } from '../../utils/constants'; +import NotificationsCallOut from '../NotificationsCallOut'; const Action = ({ action, arrayHelpers, context, destinations, + flattenedDestinations, index, onDelete, sendTestMessage, setFlyout, + httpClient, fieldPath, values, + hasNotificationPlugin, }) => { - const selectedDestination = destinations.filter((item) => item.value === action.destination_id); + const selectedDestination = flattenedDestinations.filter( + (item) => item.value === action.destination_id + ); const type = _.get(selectedDestination, '0.type', DEFAULT_ACTION_TYPE); const { name } = action; const ActionComponent = ActionsMap[type].component; const actionLabel = ActionsMap[type].label; + const manageChannelsUrl = httpClient.basePath.prepend(MANAGE_CHANNELS_PATH); const isFirstAction = index !== undefined && index === 0; + + const renderChannels = () => { + return ( +
+ + + { + // Just a swap correct fields. + arrayHelpers.replace(index, { + ...action, + destination_id: options[0].value, + }); + }, + onBlur: (e, field, form) => { + form.setFieldTouched(`${fieldPath}actions.${index}.destination_id`, true); + }, + singleSelection: { asPlainText: true }, + isClearable: false, + renderOption: (option) => ( + + {option.label} + + {option.description} + + + ), + rowHeight: 45, + }} + /> + + + + window.open(manageChannelsUrl)} + > + Manage channels + + + + + {!hasNotificationPlugin && } +
+ ); + }; + return (
@@ -77,35 +148,8 @@ const Action = ({ isInvalid, }} /> - { - // Just a swap correct fields. - arrayHelpers.replace(index, { - ...action, - destination_id: options[0].value, - }); - }, - onBlur: (e, field, form) => { - form.setFieldTouched(`${fieldPath}actions.${index}.destination_id`, true); - }, - singleSelection: { asPlainText: true }, - isClearable: false, - 'data-test-subj': `${fieldPath}actions.${index}_actionDestination`, - }} - /> + {renderChannels()} { + test('renders with Notifications plugin installed', () => { + const httpClient = { + basePath: { prepend: jest.fn() }, + }; + const context = { ctx: { monitor: {}, trigger: {} } }; + const component = ( + + {}} + sendTestMessage={() => {}} + setFlyout={() => {}} + httpClient={httpClient} + fieldPath="testPath" + values={{}} + hasNotificationPlugin={true} + /> + + ); + + expect(render(component)).toMatchSnapshot(); + }); + + test('renders without Notifications plugin installed', () => { + const httpClient = { + basePath: { prepend: jest.fn() }, + }; + const context = { ctx: { monitor: {}, trigger: {} } }; + const component = ( + + {}} + sendTestMessage={() => {}} + setFlyout={() => {}} + httpClient={httpClient} + fieldPath="testPath" + values={{}} + hasNotificationPlugin={false} + /> + + ); + + expect(render(component)).toMatchSnapshot(); + }); +}); diff --git a/public/pages/CreateTrigger/components/Action/__snapshots__/Action.test.js.snap b/public/pages/CreateTrigger/components/Action/__snapshots__/Action.test.js.snap new file mode 100644 index 000000000..fd01e21ee --- /dev/null +++ b/public/pages/CreateTrigger/components/Action/__snapshots__/Action.test.js.snap @@ -0,0 +1,1108 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Action renders with Notifications plugin installed 1`] = ` +
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+ Names can only contain letters, numbers, and special characters +
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+ +
+
+