diff --git a/cypress/integration/1_detectors.spec.js b/cypress/integration/1_detectors.spec.js
index 96c2fbf29..90c14235d 100644
--- a/cypress/integration/1_detectors.spec.js
+++ b/cypress/integration/1_detectors.spec.js
@@ -18,6 +18,128 @@ const testMappings = {
const cypressDNSRule = dns_rule_data.title;
+const createDetector = (detectorName, dataSource, expectFailure) => {
+ // Locate Create detector button click to start
+ cy.get('.euiButton').filter(':contains("Create detector")').click({ force: true });
+
+ // Check to ensure process started
+ cy.waitForPageLoad('create-detector', {
+ contains: 'Define detector',
+ });
+
+ // Enter a name for the detector in the appropriate input
+ cy.get(`input[placeholder="Enter a name for the detector."]`).focus().realType(detectorName);
+
+ // Select our pre-seeded data source (check cypressIndexDns)
+ cy.get(`[data-test-subj="define-detector-select-data-source"]`)
+ .find('input')
+ .focus()
+ .realType(dataSource);
+
+ cy.intercept({
+ pathname: '/_plugins/_security_analytics/rules/_search',
+ query: {
+ prePackaged: 'true',
+ },
+ }).as('getSigmaRules');
+
+ // Select threat detector type (Windows logs)
+ cy.get(`input[id="dns"]`).click({ force: true });
+
+ cy.wait('@getSigmaRules').then(() => {
+ // Open Detection rules accordion
+ cy.get('[data-test-subj="detection-rules-btn"]').click({ force: true, timeout: 5000 });
+
+ cy.contains('table tr', 'DNS', {
+ timeout: 120000,
+ });
+ });
+
+ // Check that correct page now showing
+ cy.contains('Configure field mapping');
+
+ if (!expectFailure) {
+ // Select appropriate names to map fields to
+ for (let field_name in testMappings.properties) {
+ const mappedTo = testMappings.properties[field_name].path;
+
+ cy.contains('tr', field_name).within(() => {
+ cy.get(`[data-test-subj="detector-field-mappings-select"]`).click().type(mappedTo);
+ });
+ }
+ }
+
+ // Click Next button to continue
+ cy.get('button').contains('Next').click({ force: true });
+
+ // Check that correct page now showing
+ cy.contains('Set up alerts');
+
+ // Type name of new trigger
+ cy.get(`input[placeholder="Enter a name for the alert condition."]`)
+ .focus()
+ .realType('test_trigger');
+
+ // Type in (or select) tags for the alert condition
+ cy.get(`[data-test-subj="alert-tags-combo-box"]`)
+ .find('input')
+ .focus()
+ .realType('attack.defense_evasion')
+ .realPress('Enter');
+
+ // Select applicable severity levels
+ cy.get(`[data-test-subj="security-levels-combo-box"]`).click({ force: true });
+ cy.contains('1 (Highest)').click({ force: true });
+
+ // Continue to next page
+ cy.contains('Next').click({ force: true });
+
+ // Confirm page is reached
+ cy.contains('Review and create');
+
+ // Confirm field mappings registered
+ cy.contains('Field mapping');
+
+ if (!expectFailure) {
+ for (let field in testMappings.properties) {
+ const mappedTo = testMappings.properties[field].path;
+
+ cy.contains(field);
+ cy.contains(mappedTo);
+ }
+ }
+
+ // Confirm entries user has made
+ cy.contains('Detector details');
+ cy.contains(detectorName);
+ cy.contains('dns');
+ cy.contains(dataSource);
+ cy.contains('Alert on test_trigger');
+
+ // Create the detector
+ cy.get('button').contains('Create').click({ force: true });
+ cy.waitForPageLoad('detector-details', {
+ contains: detectorName,
+ });
+
+ cy.contains('Attempting to create the detector.');
+
+ // Confirm detector active
+ cy.contains(detectorName);
+ cy.contains('Active');
+
+ if (!expectFailure) {
+ cy.contains('Actions');
+ }
+
+ cy.contains('Detector configuration');
+ cy.contains('Field mappings');
+ cy.contains('Alert triggers');
+ cy.contains('Detector details');
+ cy.contains('Created at');
+ cy.contains('Last updated time');
+};
+
describe('Detectors', () => {
const cypressIndexDns = 'cypress-index-dns';
const cypressIndexWindows = 'cypress-index-windows';
@@ -86,130 +208,19 @@ describe('Detectors', () => {
cy.get('.euiCallOut')
.should('be.visible')
- .contains('The selected log sources contain different log types');
+ .contains(
+ 'To avoid issues with field mappings, we recommend creating separate detectors for different log types.'
+ );
});
it('...can be created', () => {
- // Locate Create detector button click to start
- cy.get('.euiButton').filter(':contains("Create detector")').click({ force: true });
-
- // Check to ensure process started
- cy.waitForPageLoad('create-detector', {
- contains: 'Define detector',
- });
-
- // Enter a name for the detector in the appropriate input
- cy.get(`input[placeholder="Enter a name for the detector."]`).focus().realType('test detector');
-
- // Select our pre-seeded data source (check cypressIndexDns)
- cy.get(`[data-test-subj="define-detector-select-data-source"]`)
- .find('input')
- .focus()
- .realType(cypressIndexDns);
-
- cy.intercept({
- pathname: '/_plugins/_security_analytics/rules/_search',
- query: {
- prePackaged: 'true',
- },
- }).as('getSigmaRules');
-
- // Select threat detector type (Windows logs)
- cy.get(`input[id="dns"]`).click({ force: true });
-
- cy.wait('@getSigmaRules').then(() => {
- // Open Detection rules accordion
- cy.get('[data-test-subj="detection-rules-btn"]').click({ force: true, timeout: 5000 });
-
- cy.contains('table tr', 'DNS', {
- timeout: 120000,
- });
-
- // find search, type USB
- cy.get(`input[placeholder="Search..."]`).ospSearch(cypressDNSRule);
-
- // Disable all rules
- cy.contains('tr', cypressDNSRule, { timeout: 1000 });
- cy.get('table th').within(() => {
- cy.get('button').first().click({ force: true });
- });
-
- // Enable single rule
- cy.contains('table tr', cypressDNSRule).within(() => {
- cy.get('button').eq(1).click({ force: true, timeout: 2000 });
- });
- });
-
- // Select appropriate names to map fields to
- for (let field_name in testMappings.properties) {
- const mappedTo = testMappings.properties[field_name].path;
-
- cy.contains('tr', field_name).within(() => {
- cy.get(`[data-test-subj="detector-field-mappings-select"]`).click().type(mappedTo);
- });
- }
-
- // Click Next button to continue
- cy.get('button').contains('Next').click({ force: true });
-
- // Check that correct page now showing
- cy.contains('Set up alerts');
-
- // Type name of new trigger
- cy.get(`input[placeholder="Enter a name for the alert condition."]`)
- .focus()
- .realType('test_trigger');
-
- // Type in (or select) tags for the alert condition
- cy.get(`[data-test-subj="alert-tags-combo-box"]`)
- .find('input')
- .focus()
- .realType('attack.defense_evasion')
- .realPress('Enter');
-
- // Select applicable severity levels
- cy.get(`[data-test-subj="security-levels-combo-box"]`).click({ force: true });
- cy.contains('1 (Highest)').click({ force: true });
-
- // Continue to next page
- cy.contains('Next').click({ force: true });
-
- // Confirm page is reached
- cy.contains('Review and create');
-
- // Confirm field mappings registered
- cy.contains('Field mapping');
-
- for (let field in testMappings.properties) {
- const mappedTo = testMappings.properties[field].path;
-
- cy.contains(field);
- cy.contains(mappedTo);
- }
-
- // Confirm entries user has made
- cy.contains('Detector details');
- cy.contains(detectorName);
- cy.contains('dns');
- cy.contains(cypressIndexDns);
- cy.contains('Alert on test_trigger');
-
- // Create the detector
- cy.get('button').contains('Create').click({ force: true });
- cy.waitForPageLoad('detector-details', {
- contains: detectorName,
- });
+ createDetector(detectorName, cypressIndexDns, false);
+ cy.contains('Detector created successfully');
+ });
- // Confirm detector active
- cy.contains(detectorName);
- cy.contains('Active');
- cy.contains('Actions');
- cy.contains('Detector configuration');
- cy.contains('Field mappings');
- cy.contains('Alert triggers');
- cy.contains('Detector details');
- cy.contains('Created at');
- cy.contains('Last updated time');
+ it('...can fail creation', () => {
+ createDetector(`${detectorName}_fail`, '.kibana_1', true);
+ cy.contains('Create detector failed.');
});
it('...basic details can be edited', () => {
@@ -276,7 +287,7 @@ describe('Detectors', () => {
});
// Confirm number of rules before edit
- cy.contains('Active rules (1)');
+ cy.contains('Active rules (13)');
// Click "Edit" button in Detector rules panel
cy.get(`[data-test-subj="edit-detector-rules"]`).click({ force: true });
@@ -300,7 +311,7 @@ describe('Detectors', () => {
cy.get(`[data-test-subj="save-detector-rules-edits"]`).click({ force: true });
// Confirm 1 rule has been removed from detector
- cy.contains('Active rules (0)');
+ cy.contains('Active rules (12)');
// Click "Edit" button in Detector rules panel
cy.get(`[data-test-subj="edit-detector-rules"]`).click({ force: true });
@@ -326,7 +337,7 @@ describe('Detectors', () => {
});
// Confirm 1 rule has been added to detector
- cy.contains('Active rules (1)');
+ cy.contains('Active rules (13)');
});
it('...should update field mappings if data source is changed', () => {
@@ -347,33 +358,10 @@ describe('Detectors', () => {
cy.get('.reviewFieldMappings').should('not.exist');
// Change input source
- cy.get(`[data-test-subj="define-detector-select-data-source"]`)
- .find('input')
- .ospClear()
- .focus()
- .realType(cypressIndexWindows)
- .realPress('Enter');
-
- cy.get('.reviewFieldMappings').should('be.visible');
- cy.get('.reviewFieldMappings').within(($el) => {
- cy.get($el).contains('Automatically mapped fields (0)');
- });
-
- // Change input source
- cy.get(`[data-test-subj="define-detector-select-data-source"]`)
- .find('input')
- .ospClear()
- .focus()
- .realType(cypressIndexDns)
- .realPress('Enter');
-
- cy.get('.reviewFieldMappings').should('be.visible');
- cy.get('.reviewFieldMappings').within(($el) => {
- cy.get($el).contains('Automatically mapped fields (1)');
- });
-
- // Save changes to detector details
- cy.get(`[data-test-subj="save-basic-details-edits"]`).click({ force: true });
+ cy.get('.euiBadge__iconButton > .euiIcon').click({ force: true });
+ cy.get(`[data-test-subj="define-detector-select-data-source"]`).type(
+ `${cypressIndexWindows}{enter}`
+ );
});
it('...should update field mappings if rule selection is changed', () => {
@@ -393,61 +381,14 @@ describe('Detectors', () => {
cy.get('.reviewFieldMappings').should('not.exist');
- // Search for specific rule
- cy.get(`input[placeholder="Search..."]`).ospSearch(cypressDNSRule);
-
cy.intercept('mappings/view').as('getMappingsView');
- // Toggle single search result to unchecked
- cy.contains('table tr', cypressDNSRule).within(() => {
- // Of note, timeout can sometimes work instead of wait here, but is very unreliable from case to case.
- cy.wait(1000);
- cy.get('button').eq(1).click();
+ cy.get('table th').within(() => {
+ cy.get('button').first().click({ force: true });
});
cy.wait('@getMappingsView');
cy.get('.reviewFieldMappings').should('be.visible');
- cy.get('.reviewFieldMappings').within(($el) => {
- cy.get($el).contains('Automatically mapped fields (0)');
- });
-
- //Suspicious DNS Query with B64 Encoded String
- cy.get(`input[placeholder="Search..."]`).ospSearch(cypressDNSRule);
- cy.contains('table tr', cypressDNSRule).within(() => {
- // Of note, timeout can sometimes work instead of wait here, but is very unreliable from case to case.
- cy.wait(1000);
- cy.get('button').eq(1).click();
- });
-
- cy.wait('@getMappingsView');
- cy.get(`input[placeholder="Search..."]`).ospSearch(
- 'Suspicious DNS Query with B64 Encoded String'
- );
- cy.contains('table tr', 'Suspicious DNS Query with B64 Encoded String').within(() => {
- // Of note, timeout can sometimes work instead of wait here, but is very unreliable from case to case.
- cy.wait(1000);
- cy.get('button').eq(1).click();
- });
-
- cy.wait('@getMappingsView');
- cy.get('.reviewFieldMappings').should('be.visible');
- cy.get('.reviewFieldMappings').within(($el) => {
- cy.get($el).contains('Automatically mapped fields (1)');
- });
-
- cy.get(`input[placeholder="Search..."]`).ospSearch('High TXT Records Requests Rate');
- cy.contains('table tr', 'High TXT Records Requests Rate').within(() => {
- // Of note, timeout can sometimes work instead of wait here, but is very unreliable from case to case.
- cy.wait(1000);
- cy.get('button').eq(1).click();
- });
-
- cy.wait('@getMappingsView');
- cy.get('.reviewFieldMappings').should('be.visible');
- cy.get('.reviewFieldMappings').within(($el) => {
- cy.get($el).contains('Automatically mapped fields (1)');
- cy.get($el).contains('1 rule fields may need manual mapping');
- });
});
it('...can be deleted', () => {
diff --git a/public/app.scss b/public/app.scss
index 524fdfc11..37983aa06 100644
--- a/public/app.scss
+++ b/public/app.scss
@@ -13,6 +13,7 @@ $euiTextColor: $euiColorDarkestShade !default;
@import "./components/Charts/ChartContainer.scss";
@import "./pages/Overview/components/Widgets/WidgetContainer.scss";
+@import "./pages/Main/components/Callout.scss";
@import "./pages/Detectors/components/ReviewFieldMappings/ReviewFieldMappings.scss";
.selected-radio-panel {
diff --git a/public/pages/Alerts/containers/Alerts/__snapshots__/Alerts.test.tsx.snap b/public/pages/Alerts/containers/Alerts/__snapshots__/Alerts.test.tsx.snap
index ac1a1d036..ea1294d09 100644
--- a/public/pages/Alerts/containers/Alerts/__snapshots__/Alerts.test.tsx.snap
+++ b/public/pages/Alerts/containers/Alerts/__snapshots__/Alerts.test.tsx.snap
@@ -49,6 +49,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
@@ -138,6 +141,7 @@ exports[` spec renders the component 1`] = `
"location": Object {
"pathname": "",
},
+ "push": [MockFunction],
"replace": [MockFunction],
}
}
@@ -158,6 +162,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
diff --git a/public/pages/CreateDetector/components/ConfigureFieldMapping/components/RequiredFieldMapping/FieldNameSelector.tsx b/public/pages/CreateDetector/components/ConfigureFieldMapping/components/RequiredFieldMapping/FieldNameSelector.tsx
index 3abb78b74..c2c7fb722 100644
--- a/public/pages/CreateDetector/components/ConfigureFieldMapping/components/RequiredFieldMapping/FieldNameSelector.tsx
+++ b/public/pages/CreateDetector/components/ConfigureFieldMapping/components/RequiredFieldMapping/FieldNameSelector.tsx
@@ -39,7 +39,7 @@ export default class FieldNameSelector extends Component spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
diff --git a/public/pages/Detectors/components/UpdateAlertConditions/__snapshots__/UpdateAlertConditions.test.tsx.snap b/public/pages/Detectors/components/UpdateAlertConditions/__snapshots__/UpdateAlertConditions.test.tsx.snap
index 5c1981715..735f7d5f9 100644
--- a/public/pages/Detectors/components/UpdateAlertConditions/__snapshots__/UpdateAlertConditions.test.tsx.snap
+++ b/public/pages/Detectors/components/UpdateAlertConditions/__snapshots__/UpdateAlertConditions.test.tsx.snap
@@ -392,6 +392,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
@@ -999,6 +1002,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
diff --git a/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap b/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap
index 4cc594d1f..bd7cc18c3 100644
--- a/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap
+++ b/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap
@@ -190,6 +190,7 @@ exports[` spec renders the component 1`] = `
"location": Object {
"pathname": "",
},
+ "push": [MockFunction],
"replace": [MockFunction] {
"calls": Array [
Array [
@@ -576,6 +577,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
@@ -905,6 +909,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
diff --git a/public/pages/Detectors/containers/AlertTriggersView/__snapshots__/AlertTriggersView.test.tsx.snap b/public/pages/Detectors/containers/AlertTriggersView/__snapshots__/AlertTriggersView.test.tsx.snap
index 5d4b67e59..57e1239bc 100644
--- a/public/pages/Detectors/containers/AlertTriggersView/__snapshots__/AlertTriggersView.test.tsx.snap
+++ b/public/pages/Detectors/containers/AlertTriggersView/__snapshots__/AlertTriggersView.test.tsx.snap
@@ -183,6 +183,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
diff --git a/public/pages/Detectors/containers/Detector/DetectorDetails.tsx b/public/pages/Detectors/containers/Detector/DetectorDetails.tsx
index 46e90b969..c423817b6 100644
--- a/public/pages/Detectors/containers/Detector/DetectorDetails.tsx
+++ b/public/pages/Detectors/containers/Detector/DetectorDetails.tsx
@@ -16,28 +16,20 @@ import {
EuiTabs,
EuiTitle,
EuiHealth,
- EuiCallOut,
- EuiLoadingSpinner,
- EuiPanel,
} from '@elastic/eui';
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { CoreServicesContext } from '../../../../components/core_services';
-import {
- BREADCRUMBS,
- EMPTY_DEFAULT_DETECTOR_HIT,
- logTypesWithDashboards,
- ROUTES,
-} from '../../../../utils/constants';
-import { CreateMappingsResponse, DetectorHit } from '../../../../../server/models/interfaces';
+import { BREADCRUMBS, EMPTY_DEFAULT_DETECTOR_HIT, ROUTES } from '../../../../utils/constants';
+import { DetectorHit } from '../../../../../server/models/interfaces';
import { DetectorDetailsView } from '../DetectorDetailsView/DetectorDetailsView';
import { FieldMappingsView } from '../../components/FieldMappingsView/FieldMappingsView';
import { AlertTriggersView } from '../AlertTriggersView/AlertTriggersView';
import { RuleItem } from '../../../CreateDetector/components/DefineDetector/components/DetectionRules/types/interfaces';
import { DetectorsService } from '../../../../services';
-import { errorNotificationToast, successNotificationToast } from '../../../../utils/helpers';
+import { errorNotificationToast } from '../../../../utils/helpers';
import { NotificationsStart, SimpleSavedObject } from 'opensearch-dashboards/public';
-import { CreateDetectorResponse, ISavedObjectsService, ServerResponse } from '../../../../../types';
+import { ISavedObjectsService, ServerResponse } from '../../../../../types';
import { PENDING_DETECTOR_ID } from '../../../CreateDetector/utils/constants';
import { DataStore } from '../../../../store/DataStore';
@@ -166,11 +158,11 @@ export class DetectorDetails extends React.Component {
- return this.props.savedObjectsService
- .createSavedObject(detectorName, logType, detectorId, inputIndices)
- .catch((error: any) => {
- console.error(error);
- });
- };
-
getPendingDetector = async () => {
- const pendingState = DataStore.detectors.getPendingState();
- const detector = pendingState?.detectorState?.detector;
- const pendingRequests = pendingState?.pendingRequests;
+ const state = DataStore.detectors.getState();
+ const detector = state?.detectorInput?.detector;
+ const pendingRequests = state?.pendingRequests;
this.getTabs();
if (pendingRequests && detector) {
@@ -226,93 +205,33 @@ export class DetectorDetails extends React.Component,
- ServerResponse
- ];
-
- if (mappingsResponse.ok) {
- if (detectorResponse.ok) {
- let dashboardId;
- const detectorId = detectorResponse.response._id;
- if (logTypesWithDashboards.has(detector.detector_type)) {
- const dashboardResponse = await this.createDashboard(
- detector.name,
- detector.detector_type,
- detectorResponse.response._id,
- detector.inputs[0].detector_input.indices
- );
- if (dashboardResponse && dashboardResponse.ok) {
- dashboardId = dashboardResponse.response.id;
- } else {
- const dashboards = await this.props.savedObjectsService.getDashboards();
- dashboards.some((dashboard) => {
- if (
- dashboard.references.findIndex(
- (reference) => reference.id === this.state.detectorId
- ) > -1
- ) {
- dashboardId = dashboard.id;
- return true;
- }
-
- return false;
- });
- }
- }
+ const pendingResponse = await DataStore.detectors.resolvePendingCreationRequest();
+ if (pendingResponse.ok) {
+ const { detectorId, dashboardId } = pendingResponse;
+
+ detectorId &&
this.setState(
{
detectorId,
dashboardId,
},
() => {
- DataStore.detectors.deletePendingState();
+ DataStore.detectors.deleteState();
this.props.history.push(`${ROUTES.DETECTOR_DETAILS}/${detectorId}`);
this.getDetector();
}
);
-
- successNotificationToast(
- this.props.notifications,
- 'created',
- `detector, "${detector.name}"`
- );
- } else {
- this.setState(
- {
- createFailed: true,
- },
- () =>
- errorNotificationToast(
- this.props.notifications,
- 'create',
- 'detector',
- detectorResponse.error
- )
- );
- }
} else {
- this.setState(
- {
- createFailed: true,
- },
- () =>
- errorNotificationToast(
- this.props.notifications,
- 'create',
- 'detector',
- 'Double check the field mappings and try again.'
- )
- );
+ this.setState({ createFailed: true });
}
}
this.setState({ loading: false });
};
async componentDidMount() {
- const pendingState = DataStore.detectors.getPendingState();
- pendingState ? this.getPendingDetector() : this.getDetector();
+ const state = DataStore.detectors.getState();
+ state ? this.getPendingDetector() : this.getDetector();
}
getDetector = async () => {
@@ -512,17 +431,6 @@ export class DetectorDetails extends React.Component {
- const pendingState = DataStore.detectors.getPendingState();
- const detectorState = pendingState?.detectorState;
- this.props.history.push({
- pathname: `${ROUTES.DETECTORS_CREATE}`,
- // @ts-ignore
- state: { detectorState },
- });
- DataStore.detectors.deletePendingState();
- };
-
render() {
const { _source: detector } = this.detectorHit;
const { selectedTabContent, detectorId, createFailed } = this.state;
@@ -548,34 +456,6 @@ export class DetectorDetails extends React.Component
- {creatingDetector ? (
- <>
-
- {!createFailed && (
-
-
-
- )}
-
- {createFailed
- ? 'Detector creation failed. Please review detector configuration and try again.'
- : 'Attempting to create the detector.'}
-
-
- }
- color={createFailed ? 'danger' : 'primary'}
- >
- {createFailed && (
-
- Review detector configuration
-
- )}
-
-
- >
- ) : null}
diff --git a/public/pages/Detectors/containers/Detector/__snapshots__/DetectorDetails.test.tsx.snap b/public/pages/Detectors/containers/Detector/__snapshots__/DetectorDetails.test.tsx.snap
index 2bb9ec9ab..1e105082d 100644
--- a/public/pages/Detectors/containers/Detector/__snapshots__/DetectorDetails.test.tsx.snap
+++ b/public/pages/Detectors/containers/Detector/__snapshots__/DetectorDetails.test.tsx.snap
@@ -195,6 +195,7 @@ exports[` spec renders the component 1`] = `
"location": Object {
"pathname": "",
},
+ "push": [MockFunction],
"replace": [MockFunction],
}
}
@@ -207,6 +208,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
@@ -887,6 +891,7 @@ exports[` spec renders the component 1`] = `
"location": Object {
"pathname": "",
},
+ "push": [MockFunction],
"replace": [MockFunction],
}
}
@@ -901,6 +906,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
@@ -1293,6 +1301,7 @@ exports[` spec renders the component 1`] = `
"location": Object {
"pathname": "",
},
+ "push": [MockFunction],
"replace": [MockFunction],
}
}
@@ -1307,6 +1316,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
@@ -2642,6 +2654,7 @@ exports[` spec renders the component 1`] = `
"location": Object {
"pathname": "",
},
+ "push": [MockFunction],
"replace": [MockFunction],
}
}
@@ -2656,6 +2669,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
diff --git a/public/pages/Detectors/containers/DetectorDetailsView/__snapshots__/DetectorDetailsView.test.tsx.snap b/public/pages/Detectors/containers/DetectorDetailsView/__snapshots__/DetectorDetailsView.test.tsx.snap
index 2ae733d96..4db4c71a9 100644
--- a/public/pages/Detectors/containers/DetectorDetailsView/__snapshots__/DetectorDetailsView.test.tsx.snap
+++ b/public/pages/Detectors/containers/DetectorDetailsView/__snapshots__/DetectorDetailsView.test.tsx.snap
@@ -186,6 +186,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
@@ -377,6 +380,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
@@ -1511,6 +1517,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
diff --git a/public/pages/Detectors/containers/Detectors/__snapshots__/Detectors.test.tsx.snap b/public/pages/Detectors/containers/Detectors/__snapshots__/Detectors.test.tsx.snap
index 7cf9f7c8f..0a66bbe88 100644
--- a/public/pages/Detectors/containers/Detectors/__snapshots__/Detectors.test.tsx.snap
+++ b/public/pages/Detectors/containers/Detectors/__snapshots__/Detectors.test.tsx.snap
@@ -23,6 +23,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
diff --git a/public/pages/Main/Main.tsx b/public/pages/Main/Main.tsx
index e4e3ef65b..3590d214f 100644
--- a/public/pages/Main/Main.tsx
+++ b/public/pages/Main/Main.tsx
@@ -13,7 +13,9 @@ import {
EuiSideNavItemType,
EuiTitle,
EuiSpacer,
+ EuiGlobalToastList,
} from '@elastic/eui';
+import { Toast } from '@opensearch-project/oui/src/eui_components/toast/global_toast_list';
import { CoreStart } from 'opensearch-dashboards/public';
import { ServicesConsumer } from '../../services';
import { BrowserServices } from '../../models/interfaces';
@@ -35,6 +37,8 @@ import { EditRule } from '../Rules/containers/EditRule/EditRule';
import { ImportRule } from '../Rules/containers/ImportRule/ImportRule';
import { DuplicateRule } from '../Rules/containers/DuplicateRule/DuplicateRule';
import { DateTimeFilter } from '../Overview/models/interfaces';
+import Callout, { ICalloutProps } from './components/Callout';
+import { DataStore } from '../../store/DataStore';
enum Navigation {
SecurityAnalytics = 'Security Analytics',
@@ -68,6 +72,8 @@ interface MainState {
getStartedDismissedOnce: boolean;
selectedNavItemIndex: number;
dateTimeFilter: DateTimeFilter;
+ callout?: ICalloutProps;
+ toasts?: Toast[];
}
const navItemIndexByRoute: { [route: string]: number } = {
@@ -89,8 +95,22 @@ export default class Main extends Component {
endTime: DEFAULT_DATE_RANGE.end,
},
};
+
+ DataStore.detectors.setHandlers(this.showCallout, this.showToast);
}
+ showCallout = (callout?: ICalloutProps) => {
+ this.setState({
+ callout,
+ });
+ };
+
+ showToast = (toasts?: any[]) => {
+ this.setState({
+ toasts,
+ });
+ };
+
componentDidMount(): void {
this.updateSelectedNavItem();
}
@@ -151,6 +171,8 @@ export default class Main extends Component {
location: { pathname },
history,
} = this.props;
+
+ const { callout } = this.state;
const sideNav: EuiSideNavItemType<{ style: any }>[] = [
{
name: Navigation.SecurityAnalytics,
@@ -230,6 +252,7 @@ export default class Main extends Component {
)}
+ {callout ? : null}
{
+
)
}
diff --git a/public/pages/Main/components/Callout.scss b/public/pages/Main/components/Callout.scss
new file mode 100644
index 000000000..832da3e78
--- /dev/null
+++ b/public/pages/Main/components/Callout.scss
@@ -0,0 +1,10 @@
+.mainCallout {
+ .euiCallOutHeader__title {
+ width: 100% !important;
+
+ .mainCalloutCloseButton {
+ flex-wrap: wrap;
+ align-content: end;
+ }
+ }
+}
diff --git a/public/pages/Main/components/Callout.tsx b/public/pages/Main/components/Callout.tsx
new file mode 100644
index 000000000..3255ca411
--- /dev/null
+++ b/public/pages/Main/components/Callout.tsx
@@ -0,0 +1,105 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import {
+ EuiCallOut,
+ EuiSpacer,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonIcon,
+ EuiLoadingSpinner,
+} from '@elastic/eui';
+
+export type TCalloutColor = 'primary' | 'success' | 'warning' | 'danger';
+export type TCalloutIcon = 'iInCircle' | 'check' | 'help' | 'alert';
+
+interface ICalloutType {
+ color: TCalloutColor;
+ iconType: TCalloutIcon;
+}
+
+export interface ICalloutProps {
+ title: string | JSX.Element;
+ message?: string | JSX.Element;
+ type?: ICalloutType | TCalloutColor;
+ closable?: boolean;
+ loading?: boolean;
+ closeHandler?: (callout?: ICalloutProps) => void;
+}
+
+export const toastTypes: {
+ [Key in TCalloutColor]: TCalloutIcon;
+} = {
+ primary: 'iInCircle',
+ success: 'check',
+ warning: 'help',
+ danger: 'alert',
+};
+
+export const resolveType = (type?: ICalloutType | TCalloutColor): ICalloutType => {
+ if (type === undefined) {
+ return {
+ color: 'primary',
+ iconType: 'iInCircle',
+ };
+ } else {
+ if (typeof type === 'string') {
+ return {
+ color: type,
+ iconType: toastTypes[type],
+ };
+ } else {
+ return type;
+ }
+ }
+};
+
+export const CallOut = ({
+ title,
+ message,
+ type,
+ closable = true,
+ loading = false,
+ closeHandler,
+}: ICalloutProps) => {
+ const closeCallout = () => closeHandler && closeHandler(undefined);
+
+ const getTitle = (): JSX.Element => {
+ return (
+
+ {loading && (
+
+
+
+ )}
+ {title}
+ {closable && (
+
+ closeCallout()} iconType="cross" aria-label="Close" />
+
+ )}
+
+ );
+ };
+
+ const { color, iconType } = resolveType(type);
+ return (
+ <>
+
+ {message}
+
+
+
+ >
+ );
+};
+
+export default CallOut;
diff --git a/public/store/DataStore.ts b/public/store/DataStore.ts
index 9a0687cfd..58a6ad538 100644
--- a/public/store/DataStore.ts
+++ b/public/store/DataStore.ts
@@ -14,6 +14,10 @@ export class DataStore {
public static init = (services: BrowserServices, notifications: NotificationsStart) => {
DataStore.rules = new RulesStore(services.ruleService, notifications);
- DataStore.detectors = new DetectorsStore(services.detectorsService, notifications);
+ DataStore.detectors = new DetectorsStore(
+ services.detectorsService,
+ notifications,
+ services.savedObjectsService
+ );
};
}
diff --git a/public/store/DetectorsStore.test.ts b/public/store/DetectorsStore.test.ts
new file mode 100644
index 000000000..af98aa191
--- /dev/null
+++ b/public/store/DetectorsStore.test.ts
@@ -0,0 +1,104 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { DataStore } from './DataStore';
+import notificationsStartMock from '../../test/mocks/services/notifications/NotificationsStart.mock';
+import services from '../../test/mocks/services';
+import { DetectorsStore } from './DetectorsStore';
+import { expect } from '@jest/globals';
+import detectorResponseMock from '../../test/mocks/Detectors/containers/Detectors/DetectorResponse.mock';
+import browserHistoryMock from '../../test/mocks/services/browserHistory.mock';
+import { CreateDetectorState } from '../pages/CreateDetector/containers/CreateDetector';
+import DetectorMock from '../../test/mocks/Detectors/containers/Detectors/Detector.mock';
+
+describe('Detectors store specs', () => {
+ Object.assign(services, {
+ detectorService: {
+ getRules: () => Promise.resolve(detectorResponseMock),
+ deleteRule: () => Promise.resolve(true),
+ },
+ });
+
+ DataStore.init(services, notificationsStartMock);
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('detectors store should be created', () => {
+ expect(DataStore.detectors instanceof DetectorsStore).toBe(true);
+ });
+
+ it('should handle the state', () => {
+ DataStore.detectors.setState(
+ {
+ pendingRequests: [Promise.resolve()],
+ detectorInput: {
+ detector: { detector_type: 'test_detector_type' } as typeof DetectorMock,
+ } as CreateDetectorState,
+ },
+ browserHistoryMock
+ );
+
+ let state = DataStore.detectors.getState();
+ expect(state?.detectorInput?.detector.detector_type).toBe('test_detector_type');
+
+ DataStore.detectors.deleteState();
+ state = DataStore.detectors.getState();
+ expect(state).toBe(undefined);
+ });
+
+ it('should get successful pending state', async () => {
+ DataStore.detectors.setState(
+ {
+ pendingRequests: [
+ Promise.resolve({
+ ok: true,
+ }),
+ Promise.resolve({
+ ok: true,
+ response: {
+ _id: '',
+ detector: {
+ detector_type: '',
+ inputs: [
+ {
+ detector_input: {
+ indices: [],
+ },
+ },
+ ],
+ },
+ },
+ }),
+ ],
+ detectorInput: {
+ detector: { detector_type: 'test_detector_type' } as typeof DetectorMock,
+ } as CreateDetectorState,
+ },
+ browserHistoryMock
+ );
+ const pending = await DataStore.detectors.resolvePendingCreationRequest();
+ expect(pending.ok).toBe(true);
+ });
+
+ it('should get failed pending state', async () => {
+ DataStore.detectors.setState(
+ {
+ pendingRequests: [
+ Promise.resolve({
+ ok: false,
+ }),
+ ],
+ detectorInput: {
+ detector: { detector_type: 'test_detector_type' } as typeof DetectorMock,
+ } as CreateDetectorState,
+ },
+ browserHistoryMock
+ );
+ const pending = await DataStore.detectors.resolvePendingCreationRequest();
+ expect(pending.ok).toBe(false);
+ });
+});
diff --git a/public/store/DetectorsStore.ts b/public/store/DetectorsStore.ts
deleted file mode 100644
index 3a39224dd..000000000
--- a/public/store/DetectorsStore.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright OpenSearch Contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import { DetectorsService } from '../services';
-import { NotificationsStart } from 'opensearch-dashboards/public';
-import { CreateDetectorState } from '../pages/CreateDetector/containers/CreateDetector';
-
-export interface IDetectorsStore {}
-export interface IDetectorsCache {}
-
-export interface IDetectorsState {
- pendingRequests: Promise[];
- detectorState: CreateDetectorState;
-}
-
-/**
- * Class is used to make detector's API calls and cache the detectors.
- * If there is a cache data requests are skipped and result is returned from the cache.
- * If cache is invalidated then the request is made to get a new set of data.
- *
- * @class DetectorsStore
- * @implements IDetectorsStore
- * @param {BrowserServices} services Uses services to make API requests
- */
-export class DetectorsStore implements IDetectorsStore {
- /**
- * Rule service instance
- *
- * @property {DetectorsService} service
- * @readonly
- */
- readonly service: DetectorsService;
-
- /**
- * Notifications
- * @property {NotificationsStart}
- * @readonly
- */
- readonly notifications: NotificationsStart;
-
- /**
- * Keeps detector's data cached
- *
- * @property {IDetectorsCache} cache
- */
- private cache: IDetectorsCache = {};
-
- private state: IDetectorsState | undefined;
-
- constructor(service: DetectorsService, notifications: NotificationsStart) {
- this.service = service;
- this.notifications = notifications;
- }
-
- /**
- * Invalidates all detectors data
- */
- private invalidateCache = () => {
- this.cache = {};
- return this;
- };
-
- public setPendingState = (state: IDetectorsState) => {
- this['state'] = state;
- };
-
- public getPendingState = () => {
- if (!this.state) return undefined;
- return {
- ...this.state,
- };
- };
-
- public deletePendingState = () => {
- delete this.state;
- };
-}
diff --git a/public/store/DetectorsStore.tsx b/public/store/DetectorsStore.tsx
new file mode 100644
index 000000000..9377d06d7
--- /dev/null
+++ b/public/store/DetectorsStore.tsx
@@ -0,0 +1,338 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { DetectorsService } from '../services';
+import { NotificationsStart } from 'opensearch-dashboards/public';
+import { CreateDetectorState } from '../pages/CreateDetector/containers/CreateDetector';
+import { ICalloutProps, resolveType, TCalloutColor } from '../pages/Main/components/Callout';
+import { CreateDetectorResponse, ISavedObjectsService, ServerResponse } from '../../types';
+import { CreateMappingsResponse } from '../../server/models/interfaces';
+import { logTypesWithDashboards, ROUTES } from '../utils/constants';
+import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { Toast } from '@opensearch-project/oui/src/eui_components/toast/global_toast_list';
+import { RouteComponentProps } from 'react-router-dom';
+import { DataStore } from './DataStore';
+import { v4 as uuidv4 } from 'uuid';
+
+export interface IDetectorsStore {
+ readonly service: DetectorsService;
+ readonly notifications: NotificationsStart;
+ readonly savedObjectsService: ISavedObjectsService;
+ readonly history: RouteComponentProps['history'] | undefined;
+ setState: (state: IDetectorsState, history: RouteComponentProps['history']) => void;
+ getState: () => IDetectorsState | undefined;
+ deleteState: () => void;
+ resolvePendingCreationRequest: () => Promise<{
+ detectorId?: string;
+ dashboardId?: string;
+ ok: boolean;
+ }>;
+ setHandlers: (
+ calloutHandler: (callout?: ICalloutProps) => void,
+ toastHandler: (toasts?: Toast[]) => void
+ ) => void;
+}
+
+export interface IDetectorsCache {}
+
+export interface IDetectorsState {
+ pendingRequests: Promise[];
+ detectorInput: CreateDetectorState;
+}
+
+/**
+ * Class is used to make detector's API calls and cache the detectors.
+ * If there is a cache data requests are skipped and result is returned from the cache.
+ * If cache is invalidated then the request is triggered to get a new set of data.
+ *
+ * @class DetectorsStore
+ * @implements IDetectorsStore
+ * @param {BrowserServices} services Uses services to make API requests
+ */
+export class DetectorsStore implements IDetectorsStore {
+ /**
+ * Rule service instance
+ *
+ * @property {DetectorsService} service
+ * @readonly
+ */
+ readonly service: DetectorsService;
+
+ /**
+ * Notifications
+ * @property {NotificationsStart}
+ * @readonly
+ */
+ readonly notifications: NotificationsStart;
+
+ /**
+ * SavedObjectsService
+ * @property {ISavedObjectsService}
+ * @readonly
+ */
+ readonly savedObjectsService: ISavedObjectsService;
+
+ /**
+ * Router history
+ * @property {RouteComponentProps['history']}
+ * @readonly
+ */
+ history: RouteComponentProps['history'] | undefined = undefined;
+
+ /**
+ * Keeps detector's data cached
+ *
+ * @property {IDetectorsCache} cache
+ */
+ private cache: IDetectorsCache = {};
+
+ /**
+ * Store state
+ * @private {IDetectorsState}
+ */
+ private state: IDetectorsState | undefined;
+
+ /**
+ * List of all shown toasts
+ * @private
+ */
+ private toasts: Toast[] = [];
+
+ constructor(
+ service: DetectorsService,
+ notifications: NotificationsStart,
+ savedObjectsService: ISavedObjectsService
+ ) {
+ this.service = service;
+ this.notifications = notifications;
+ this.savedObjectsService = savedObjectsService;
+ }
+
+ /**
+ * Invalidates all detectors data
+ */
+ private invalidateCache = (): DetectorsStore => {
+ this.cache = {};
+ return this;
+ };
+
+ public setState = (state: IDetectorsState, history: RouteComponentProps['history']): void => {
+ this.state = state;
+ this.history = history;
+
+ this.showNotification('Attempting to create the detector.', undefined, 'primary', true);
+ };
+
+ public getState = (): IDetectorsState | undefined => (this.state ? this.state : undefined);
+
+ public deleteState = (): void => {
+ delete this.state;
+ };
+
+ private showNotification = (
+ title: string,
+ message?: string,
+ type?: TCalloutColor,
+ loading?: boolean,
+ btnText?: string,
+ btnHandler?: (e: any) => void
+ ): void => {
+ if (!type) type = 'primary';
+
+ const closeAllToasts = () => {
+ this.toasts = [];
+ this.showToastCallback(this.toasts);
+ };
+
+ const btn = btnText && (
+ {
+ btnHandler && btnHandler(e);
+ this.hideCallout();
+ closeAllToasts();
+ }}
+ size="s"
+ >
+ {btnText}
+
+ );
+
+ const messageBody = (
+
+ {message && {message}}
+ {btn}
+
+ );
+
+ this.showCalloutCallback({
+ type,
+ title,
+ message: messageBody,
+ closeHandler: this.hideCallout,
+ });
+
+ const { color, iconType } = resolveType(type);
+ this.toasts.push({
+ title,
+ color,
+ iconType,
+ id: `toastsKey_${uuidv4()}`,
+ text: messageBody,
+ });
+ this.showToastCallback(this.toasts);
+ };
+
+ private viewDetectorConfiguration = (): void => {
+ const state = DataStore.detectors.getState();
+ const detectorInput = { ...state?.detectorInput };
+ DataStore.detectors.deleteState();
+
+ this.history?.push({
+ pathname: `${ROUTES.DETECTORS_CREATE}`,
+ state: { detectorInput },
+ });
+ };
+
+ public resolvePendingCreationRequest = async (): Promise<{
+ detectorId?: string;
+ dashboardId?: string;
+ ok: boolean;
+ }> => {
+ if (this.state?.pendingRequests) {
+ const [mappingsResponse, detectorResponse] = (await Promise.all(
+ this.state?.pendingRequests
+ )) as [ServerResponse, ServerResponse];
+
+ let title: string = `Create detector failed.`;
+ if (!mappingsResponse.ok) {
+ const message = 'Double check the field mappings and try again.';
+
+ this.showNotification(
+ title,
+ message,
+ 'danger',
+ false,
+ 'Review detector configuration',
+ DataStore.detectors.viewDetectorConfiguration
+ );
+
+ return Promise.resolve({
+ ok: false,
+ error: { title, message },
+ });
+ }
+
+ if (!detectorResponse.ok) {
+ this.showNotification(
+ title,
+ detectorResponse.error,
+ 'danger',
+ false,
+ 'Review detector configuration',
+ DataStore.detectors.viewDetectorConfiguration
+ );
+
+ return Promise.resolve({
+ ok: false,
+ error: {
+ title,
+ message: detectorResponse.error,
+ },
+ });
+ }
+
+ let dashboardId;
+ const detector = detectorResponse.response.detector;
+ const detectorId = detectorResponse.response._id;
+ if (logTypesWithDashboards.has(detector.detector_type)) {
+ const dashboardResponse = await this.createDashboard(
+ detector.name,
+ detector.detector_type,
+ detectorId,
+ detector.inputs[0].detector_input.indices
+ );
+ if (dashboardResponse && dashboardResponse.ok) {
+ dashboardId = dashboardResponse.response.id;
+ } else {
+ const dashboards = await this.savedObjectsService.getDashboards();
+ dashboards.some((dashboard) => {
+ if (dashboard.references.findIndex((reference) => reference.id === detectorId) > -1) {
+ dashboardId = dashboard.id;
+ return true;
+ }
+
+ return false;
+ });
+ }
+ }
+
+ const goToDetectorDetails = (e: any) => {
+ e.preventDefault();
+ DataStore.detectors.deleteState();
+ this.history?.push(`${ROUTES.DETECTOR_DETAILS}/${detectorId}`);
+ };
+
+ title = `Detector created successfully: ${detectorResponse.response.detector.name}`;
+ this.showNotification(
+ title,
+ undefined,
+ 'success',
+ false,
+ 'View detector',
+ goToDetectorDetails
+ );
+
+ return Promise.resolve({
+ detectorId: detectorId,
+ dashboardId: dashboardId,
+ ok: true,
+ });
+ }
+
+ return Promise.resolve({ ok: false });
+ };
+
+ private createDashboard = (
+ detectorName: string,
+ logType: string,
+ detectorId: string,
+ inputIndices: string[]
+ ) => {
+ return this.savedObjectsService
+ .createSavedObject(detectorName, logType, detectorId, inputIndices)
+ .catch((error: any) => {
+ console.error(error);
+ });
+ };
+
+ /**
+ * A handler function that store gets from the Main component to show/hide the callout message
+ * @param {ICalloutProps | undefined} callout
+ */
+ private showCalloutCallback = (callout?: ICalloutProps | undefined): void => {};
+
+ private hideCallout = (): void => this.showCalloutCallback(undefined);
+
+ /**
+ * A handler function that store gets from the Main component to show/hide the toast message
+ * @param {Toast[] | undefined} toasts
+ */
+ private showToastCallback = (toasts?: Toast[] | undefined): void => {};
+
+ public hideToast = (removedToast: any): void => {
+ this.toasts = this.toasts.filter((toast: Toast) => toast.id !== removedToast.id);
+ this.showToastCallback(this.toasts);
+ };
+
+ public setHandlers = (
+ calloutHandler: (callout?: ICalloutProps) => void,
+ toastHandler: (toasts?: Toast[]) => void
+ ): void => {
+ this.showCalloutCallback = calloutHandler;
+ this.showToastCallback = toastHandler;
+ };
+}
diff --git a/test/mocks/services/browserHistory.mock.ts b/test/mocks/services/browserHistory.mock.ts
index a40a1a2ff..9af44b50c 100644
--- a/test/mocks/services/browserHistory.mock.ts
+++ b/test/mocks/services/browserHistory.mock.ts
@@ -9,4 +9,5 @@ export default ({
location: {
pathname: '',
},
+ push: jest.fn(),
} as unknown) as History;
diff --git a/test/mocks/services/notifications/NotificationsStart.mock.ts b/test/mocks/services/notifications/NotificationsStart.mock.ts
index 73f62608c..f1afac33f 100644
--- a/test/mocks/services/notifications/NotificationsStart.mock.ts
+++ b/test/mocks/services/notifications/NotificationsStart.mock.ts
@@ -8,5 +8,8 @@ import { NotificationsStart } from 'opensearch-dashboards/public';
export default ({
toasts: {
addDanger: jest.fn(),
+ addWarning: jest.fn(),
+ addSuccess: jest.fn(),
+ addInfo: jest.fn(),
},
} as unknown) as NotificationsStart;