From 6dc848e32f82b7398356fbc43a250872f11996aa Mon Sep 17 00:00:00 2001 From: Kaituo Li Date: Thu, 23 Mar 2023 16:57:36 -0700 Subject: [PATCH] Various bug fixes (#502) * Various bug fixes We display the error message "No features have been added to this anomaly detector. A feature is a metric used for anomaly detection. A detector can detect anomalies across one or more features" when the preview API fails to complete the preview request due to one of the following reasons: * The preview API cannot locate sufficient data to show sample anomaly results, as it requires more than 400 data points within the preview date range (from 5 days ago to the present time). * The preview API fails with an error (e.g., 5xx error). * No features have been added to the anomaly detector. * No features have been enabled in the anomaly detector. This pull request (PR) fixes the issue by displaying errors on a case-by-case basis. Additionally, this PR adds Prettier to the repository as it is used by Dashboards and other plugins (https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/DEVELOPER_GUIDE.md#prettier-and-linting). Furthermore, this PR standardizes the preview range to 7 days. Previously, we attempted to use 7 days but had to override it to 5 days due to two startTime fields in the parameters (refer to AnomalyDetectorData.js). This PR also resolves a bug where we show two links to the detector configuration (refer to getOverviewStats.js). This PR addresses a warning when displaying a detector URL on the monitor overview page (OverviewStat/OverviewStat). As the following error trace indicates, we only supported string or number as a value in Overview stats. This PR introduces a React element as another supported type. checkPropTypes.js:20 Warning: Failed prop type: Invalid prop `value` supplied to `OverviewStat`, expected one of type [string, number]. in OverviewStat (created by MonitorOverview) in MonitorOverview (created by MonitorDetails) in div (created by MonitorDetails) in MonitorDetails (created by Context.Consumer) in Route (created by Context.Consumer) in Switch (created by Context.Consumer) in div (created by Context.Consumer) in Main (created by Context.Consumer) in Route in Router (created by HashRouter) in HashRouter This PR also removes the unused file AnomalyHistory.js. Testing done: * Manually tested each preview failure scenario. * Manually tested alerting workflow of both HC and single stream detectors still works. * Added unit tests for new code. Signed-off-by: Kaituo Li * add missing period Signed-off-by: Kaituo Li * Don't block trigger creation when preview fails Creating/editing a monitor without setting a trigger means no alert is ever raised. This commit fixed the issue. Signed-off-by: Kaituo Li --------- Signed-off-by: Kaituo Li --- DEVELOPER_GUIDE.md | 10 + package.json | 3 +- .../EmptyFeaturesMessage.js | 118 +++++--- .../EmptyFeaturesMessage.test.js | 13 +- .../EmptyFeaturesMessage.test.js.snap | 28 +- .../AnomalyDetectors/AnomalyDetectorData.js | 19 +- .../DefineTrigger/AnomalyDetectorTrigger.js | 67 ++++- .../AnomalyDetectorTrigger.test.js | 274 ++++++++++++++++++ .../AnomalyDetectorTrigger.test.js.snap | 88 ++++++ .../MonitorOverview/utils/getOverviewStats.js | 2 +- .../components/OverviewStat/OverviewStat.js | 2 +- .../AnomalyHistory/AnomalyHistory.js | 124 -------- public/utils/constants.js | 9 + yarn.lock | 5 + 14 files changed, 571 insertions(+), 191 deletions(-) create mode 100644 public/pages/CreateTrigger/containers/DefineTrigger/AnomalyDetectorTrigger.test.js create mode 100644 public/pages/CreateTrigger/containers/DefineTrigger/__snapshots__/AnomalyDetectorTrigger.test.js.snap delete mode 100644 public/pages/MonitorDetails/containers/AnomalyHistory/AnomalyHistory.js diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 178fd6f80..fa8b31fc7 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -78,6 +78,16 @@ Example output: `./build/alertingDashboards-1.0.0-rc1.zip` 1. `--no-base-path`: opt out the BasePathProxy. 1. `--no-watch`: make sure your server has not restarted. +### Formatting + +This codebase uses Prettier as our code formatter. All new code that is added has to be reformatted using the Prettier version listed in `package.json`. In order to keep consistent formatting across the project developers should only use the prettier CLI to reformat their code using the following command: + +``` +yarn prettier --write +``` + +> NOTE: There also exists prettier plugins on several editors that allow for automatic reformatting on saving the file. However using this is discouraged as you must ensure that the plugin uses the correct version of prettier (listed in `package.json`) before using such a plugin. + ### Backport - [Link to backport documentation](https://github.com/opensearch-project/opensearch-plugins/blob/main/BACKPORT.md) \ No newline at end of file diff --git a/package.json b/package.json index 9926f1463..2cd743c42 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "formik": "^2.2.6", "lodash": "^4.17.21", "query-string": "^6.13.2", - "react-vis": "^1.8.1" + "react-vis": "^1.8.1", + "prettier": "^2.1.1" }, "resolutions": { "ansi-regex": "^5.0.1", diff --git a/public/pages/CreateMonitor/components/AnomalyDetectors/EmptyFeaturesMessage/EmptyFeaturesMessage.js b/public/pages/CreateMonitor/components/AnomalyDetectors/EmptyFeaturesMessage/EmptyFeaturesMessage.js index 988bf6136..c10c45ad0 100644 --- a/public/pages/CreateMonitor/components/AnomalyDetectors/EmptyFeaturesMessage/EmptyFeaturesMessage.js +++ b/public/pages/CreateMonitor/components/AnomalyDetectors/EmptyFeaturesMessage/EmptyFeaturesMessage.js @@ -6,48 +6,94 @@ import React from 'react'; import PropTypes from 'prop-types'; import { EuiEmptyPrompt, EuiButton, EuiText, EuiLoadingChart } from '@elastic/eui'; -import { OPENSEARCH_DASHBOARDS_AD_PLUGIN } from '../../../../../utils/constants'; +import { + OPENSEARCH_DASHBOARDS_AD_PLUGIN, + PREVIEW_ERROR_TYPE, +} from '../../../../../utils/constants'; -const EmptyFeaturesMessage = (props) => ( -
- {props.isLoading ? ( - } /> - ) : ( - - No features have been added to this anomaly detector. A feature is a metric that is used - for anomaly detection. A detector can discover anomalies across one or more features. - - } - actions={[ - - Add Feature - , - ]} - /> - )} -
-); +/** + * + * @param {string} errorType error type + * @param {*} err error message + * @param {*} isHCDetector whether the detector is HC + * @returns error messages to show on the empty trigger page + */ +function getErrorMsg(errorType, err, isHCDetector) { + switch (errorType) { + case PREVIEW_ERROR_TYPE.NO_FEATURE: + return 'No features have been added to this anomaly detector. A feature is a metric that is used for anomaly detection. A detector can discover anomalies across one or more features.'; + case PREVIEW_ERROR_TYPE.NO_ENABLED_FEATURES: + return 'No features have been enabled in this anomaly detector. A feature is a metric that is used for anomaly detection. A detector can discover anomalies across one or more features.'; + default: + console.log('We only deal with feature related error type in this page: ' + errorType); + return ''; + } +} + +// FunctionComponent +const ActionUI = ({ errorType, detectorId }) => { + switch (errorType) { + case PREVIEW_ERROR_TYPE.NO_FEATURE: + return ( + + Add Feature + + ); + case PREVIEW_ERROR_TYPE.NO_ENABLED_FEATURES: + return ( + + Enable Feature + + ); + default: + console.log('We only deal with feature related error type in this page: ' + errorType); + return ''; + } +}; + +const EmptyFeaturesMessage = (props) => { + const errorMsg = getErrorMsg(props.previewErrorType, props.error, props.isHCDetector); + + return ( +
+ {props.isLoading ? ( + } /> + ) : ( + {errorMsg}} + actions={[]} + /> + )} +
+ ); +}; EmptyFeaturesMessage.propTypes = { detectorId: PropTypes.string, isLoading: PropTypes.bool.isRequired, containerStyle: PropTypes.object, + error: PropTypes.string.isRequired, + isHCDetector: PropTypes.bool.isRequired, + previewErrorType: PropTypes.number.isRequired, }; EmptyFeaturesMessage.defaultProps = { detectorId: '', diff --git a/public/pages/CreateMonitor/components/AnomalyDetectors/EmptyFeaturesMessage/EmptyFeaturesMessage.test.js b/public/pages/CreateMonitor/components/AnomalyDetectors/EmptyFeaturesMessage/EmptyFeaturesMessage.test.js index c030d7210..fa0d52b28 100644 --- a/public/pages/CreateMonitor/components/AnomalyDetectors/EmptyFeaturesMessage/EmptyFeaturesMessage.test.js +++ b/public/pages/CreateMonitor/components/AnomalyDetectors/EmptyFeaturesMessage/EmptyFeaturesMessage.test.js @@ -6,10 +6,21 @@ import React from 'react'; import { render } from 'enzyme'; import { EmptyFeaturesMessage } from './EmptyFeaturesMessage'; +import { PREVIEW_ERROR_TYPE } from '../../../../../utils/constants'; describe('EmptyFeaturesMessage', () => { - test('renders ', () => { + test('renders no feature', () => { const component = ; expect(render(component)).toMatchSnapshot(); }); + test('renders no enabled feature', () => { + const component = ( + + ); + const wrapper = render(component); + expect(wrapper.find('[data-test-subj~="editButton"]').text()).toEqual('Enable Feature'); + }); }); diff --git a/public/pages/CreateMonitor/components/AnomalyDetectors/EmptyFeaturesMessage/__snapshots__/EmptyFeaturesMessage.test.js.snap b/public/pages/CreateMonitor/components/AnomalyDetectors/EmptyFeaturesMessage/__snapshots__/EmptyFeaturesMessage.test.js.snap index 53b6690c9..cde86e472 100644 --- a/public/pages/CreateMonitor/components/AnomalyDetectors/EmptyFeaturesMessage/__snapshots__/EmptyFeaturesMessage.test.js.snap +++ b/public/pages/CreateMonitor/components/AnomalyDetectors/EmptyFeaturesMessage/__snapshots__/EmptyFeaturesMessage.test.js.snap @@ -1,11 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`EmptyFeaturesMessage renders 1`] = ` +exports[`EmptyFeaturesMessage renders no feature 1`] = `
- No features have been added to this anomaly detector. A feature is a metric that is used for anomaly detection. A detector can discover anomalies across one or more features. -
+ />
diff --git a/public/pages/CreateMonitor/containers/AnomalyDetectors/AnomalyDetectorData.js b/public/pages/CreateMonitor/containers/AnomalyDetectors/AnomalyDetectorData.js index 20c9d0aac..59b423f08 100644 --- a/public/pages/CreateMonitor/containers/AnomalyDetectors/AnomalyDetectorData.js +++ b/public/pages/CreateMonitor/containers/AnomalyDetectors/AnomalyDetectorData.js @@ -7,7 +7,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import moment from 'moment'; import { CoreContext } from '../../../../utils/CoreContext'; -import { AD_PREVIEW_DAYS } from '../../../../utils/constants'; +import { AD_PREVIEW_DAYS, DEFAULT_PREVIEW_ERROR_MSG } from '../../../../utils/constants'; import { backendErrorNotification } from '../../../../utils/helpers'; class AnomalyDetectorData extends React.Component { @@ -25,6 +25,7 @@ class AnomalyDetectorData extends React.Component { previewStartTime: 0, previewEndTime: 0, isLoading: false, + error: '', }; this.getPreviewData = this.getPreviewData.bind(this); } @@ -39,6 +40,17 @@ class AnomalyDetectorData extends React.Component { } } + getPreviewErrorMessage(err) { + if (typeof err === 'string') return err; + if (err) { + if (err.msg === 'Bad Request') { + return err.response || DEFAULT_PREVIEW_ERROR_MSG; + } + if (err.msg) return err.msg; + } + return DEFAULT_PREVIEW_ERROR_MSG; + } + async getPreviewData() { const { detectorId, startTime, endTime } = this.props; const { http: httpClient, notifications } = this.context; @@ -47,7 +59,6 @@ class AnomalyDetectorData extends React.Component { }); if (!detectorId) return; const requestParams = { - startTime: moment().subtract(AD_PREVIEW_DAYS, 'd').valueOf(), startTime: startTime, endTime: endTime, preview: this.props.preview, @@ -69,6 +80,7 @@ class AnomalyDetectorData extends React.Component { } else { this.setState({ isLoading: false, + error: getPreviewErrorMessage(response.error), }); backendErrorNotification(notifications, 'get', 'detector results', response.error); } @@ -76,6 +88,7 @@ class AnomalyDetectorData extends React.Component { console.error('Unable to get detectorResults', err); this.setState({ isLoading: false, + error: err, }); } } @@ -91,7 +104,7 @@ AnomalyDetectorData.propTypes = { }; AnomalyDetectorData.defaultProps = { preview: true, - startTime: moment().subtract(5, 'd').valueOf(), + startTime: moment().subtract(AD_PREVIEW_DAYS, 'd').valueOf(), endTime: moment().valueOf(), }; diff --git a/public/pages/CreateTrigger/containers/DefineTrigger/AnomalyDetectorTrigger.js b/public/pages/CreateTrigger/containers/DefineTrigger/AnomalyDetectorTrigger.js index 8f9be5374..ac7d5bafe 100644 --- a/public/pages/CreateTrigger/containers/DefineTrigger/AnomalyDetectorTrigger.js +++ b/public/pages/CreateTrigger/containers/DefineTrigger/AnomalyDetectorTrigger.js @@ -11,6 +11,31 @@ import TriggerExpressions from '../../components/TriggerExpressions'; import { AnomaliesChart } from '../../../CreateMonitor/components/AnomalyDetectors/AnomaliesChart'; import { EmptyFeaturesMessage } from '../../../CreateMonitor/components/AnomalyDetectors/EmptyFeaturesMessage/EmptyFeaturesMessage'; import { EmptyDetectorMessage } from '../../../CreateMonitor/components/AnomalyDetectors/EmptyDetectorMessage/EmptyDetectorMessage'; +import { PREVIEW_ERROR_TYPE } from '../../../../utils/constants'; + +/** + * + * @param {string} err if there is any exception or error running preview API, + err is not empty. + * @param {*} features detector features + * @returns error type + */ +function getPreviewErrorType(err, features) { + if (features === undefined || features.length == 0) { + return PREVIEW_ERROR_TYPE.NO_FEATURE; + } + + const enabledFeatures = features.filter((feature) => feature.featureEnabled); + if (enabledFeatures.length == 0) { + return PREVIEW_ERROR_TYPE.NO_ENABLED_FEATURES; + } + + // if error is a non-empty string, return it. + if (err) return PREVIEW_ERROR_TYPE.PREVIEW_EXCEPTION; + + // sparse data + return PREVIEW_ERROR_TYPE.SPARSE_DATA; +} class AnomalyDetectorTrigger extends React.Component { constructor(props) { @@ -18,11 +43,16 @@ class AnomalyDetectorTrigger extends React.Component { } render() { const { adValues, detectorId, fieldPath } = this.props; + const { httpClient, notifications } = this.context; return (
{ + const features = _.get(anomalyData, 'detector.featureAttributes', []); + const isHCDetector = !_.isEmpty(_.get(anomalyData, 'detector.categoryField', [])); + const previewErrorType = getPreviewErrorType(anomalyData.error, features); + // using lodash.get without worrying about whether an intermediate property is null or undefined. if (_.get(anomalyData, 'anomalyResult.anomalies', []).length > 0) { return ( @@ -66,11 +96,40 @@ class AnomalyDetectorTrigger extends React.Component { /> ); + } else if (_.isEmpty(detectorId)) { + return ; + } else if ( + previewErrorType === PREVIEW_ERROR_TYPE.EXCEPTION || + previewErrorType === PREVIEW_ERROR_TYPE.SPARSE_DATA + ) { + return ( + + + + + + ); } else { - return _.isEmpty(detectorId) ? ( - - ) : ( - + return ( + ); } }} diff --git a/public/pages/CreateTrigger/containers/DefineTrigger/AnomalyDetectorTrigger.test.js b/public/pages/CreateTrigger/containers/DefineTrigger/AnomalyDetectorTrigger.test.js new file mode 100644 index 000000000..99a48096c --- /dev/null +++ b/public/pages/CreateTrigger/containers/DefineTrigger/AnomalyDetectorTrigger.test.js @@ -0,0 +1,274 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render, mount } from 'enzyme'; +import { Formik } from 'formik'; +import { AnomalyDetectorTrigger } from './AnomalyDetectorTrigger'; +import { httpClientMock } from '../../../../../test/mocks'; +import { CoreContext } from '../../../../../public/utils/CoreContext'; + +// enabling waiting until all of the promiseds have cleared: https://tinyurl.com/5hym6n9b +const runAllPromises = () => new Promise(setImmediate); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('AnomalyDetectorTrigger', () => { + const AppContext = React.createContext({ + httpClient: { httpClientMock }, + notifications: undefined, + }); + + test('renders no feature', () => { + const component = ; + expect(render(component)).toMatchSnapshot(); + }); + test('renders no detector id', () => { + const component = ; + expect(render(component)).toMatchSnapshot(); + }); + test('renders preview sparse data', async () => { + // using it since it will render React.Fragment that rendering AnomalyDetectorTrigger returns + const response = { + anomalyResult: { + anomalies: [], + featureData: {}, + }, + detector: { + featureAttributes: [ + { + featureId: 'TV6fFYYB7j86MXY_Bzh2', + featureName: 'time', + featureEnabled: true, + aggregationQuery: { + time: { + max: { + field: 'time', + }, + }, + }, + }, + ], + }, + }; + + // Mock return in get preview function + httpClientMock.get = jest + .fn() + .mockImplementation(() => Promise.resolve({ ok: true, response: response })); + const wrapper = mount( + // put it under Formik to render TriggerExpressions that has Formik fields. + // rendering TriggerExpressions also require adValues to be passed in + + + + + + ); + + expect(httpClientMock.get).toHaveBeenCalledTimes(1); + await runAllPromises(); + + // without update, we will finish mount before the embedded async AnomalyDetectorData finish mounting + wrapper.update(); + + expect(wrapper.update().find('[data-test-subj~="empty-prompt"]').exists()).toBe(false); + expect( + wrapper + .find('[data-test-subj~="anomalyDetector.anomalyGradeThresholdEnum_conditionEnumField"]') + .exists() + ).toBe(true); + expect( + wrapper + .find( + '[data-test-subj~="anomalyDetector.anomalyConfidenceThresholdValue_conditionValueField"]' + ) + .exists() + ).toBe(true); + }); + test('renders no enabled feature', async () => { + const response = { + anomalyResult: { + anomalies: [], + featureData: {}, + }, + detector: { + featureAttributes: [ + { + featureId: 'TV6fFYYB7j86MXY_Bzh2', + featureName: 'time', + featureEnabled: false, + aggregationQuery: { + time: { + max: { + field: 'time', + }, + }, + }, + }, + ], + }, + error: '', + }; + + // Mock return in get preview function + httpClientMock.get = jest + .fn() + .mockImplementation(() => Promise.resolve({ ok: true, response: response })); + const wrapper = mount( + + + + ); + + await runAllPromises(); + + // without update, we will finish mount before the embedded async AnomalyDetectorData finish mounting + expect(wrapper.update().find('[data-test-subj~="empty-prompt"]').exists()).toBe(true); + expect(wrapper.find('.euiButton__text').text()).toEqual('Enable Feature'); + expect(wrapper.update().find('[data-test-subj~="_conditionEnumField"]').exists()).toBe(false); + expect(wrapper.update().find('[data-test-subj~="_conditionValueField"]').exists()).toBe(false); + }); + test('renders error', async () => { + const response = { + anomalyResult: { + anomalies: [], + featureData: {}, + }, + detector: { + featureAttributes: [ + { + featureId: 'TV6fFYYB7j86MXY_Bzh2', + featureName: 'time', + featureEnabled: true, + aggregationQuery: { + time: { + max: { + field: 'time', + }, + }, + }, + }, + ], + }, + error: 'request error', + }; + + // Mock return in get preview function + httpClientMock.get = jest + .fn() + .mockImplementation(() => Promise.resolve({ ok: true, response: response })); + const wrapper = mount( + // put it under Formik to render TriggerExpressions that has Formik fields. + // rendering TriggerExpressions also require adValues to be passed in + + + + + + ); + + expect(httpClientMock.get).toHaveBeenCalledTimes(1); + await runAllPromises(); + + // without update, we will finish mount before the embedded async AnomalyDetectorData finish mounting + wrapper.update(); + + console.log(wrapper.debug()); + expect(wrapper.update().find('[data-test-subj~="empty-prompt"]').exists()).toBe(false); + expect( + wrapper + .find('[data-test-subj~="anomalyDetector.anomalyGradeThresholdEnum_conditionEnumField"]') + .exists() + ).toBe(true); + expect( + wrapper + .find( + '[data-test-subj~="anomalyDetector.anomalyConfidenceThresholdValue_conditionValueField"]' + ) + .exists() + ).toBe(true); + }); + test('feature has priority over preview error', async () => { + const response = { + anomalyResult: { + anomalies: [], + featureData: {}, + }, + detector: { + featureAttributes: [ + { + featureId: 'TV6fFYYB7j86MXY_Bzh2', + featureName: 'time', + featureEnabled: false, + aggregationQuery: { + time: { + max: { + field: 'time', + }, + }, + }, + }, + ], + }, + error: 'request error', + }; + + // Mock return in get preview function + httpClientMock.get = jest + .fn() + .mockImplementation(() => Promise.resolve({ ok: true, response: response })); + const wrapper = mount( + // put it under Formik to render TriggerExpressions that has Formik fields. + // rendering TriggerExpressions also require adValues to be passed in + + + + + + ); + + expect(httpClientMock.get).toHaveBeenCalledTimes(1); + await runAllPromises(); + + // without update, we will finish mount before the embedded async AnomalyDetectorData finish mounting + wrapper.update(); + + // without update, we will finish mount before the embedded async AnomalyDetectorData finish mounting + expect(wrapper.update().find('[data-test-subj~="empty-prompt"]').exists()).toBe(true); + expect(wrapper.find('.euiButton__text').text()).toEqual('Enable Feature'); + expect(wrapper.update().find('[data-test-subj~="_conditionEnumField"]').exists()).toBe(false); + expect(wrapper.update().find('[data-test-subj~="_conditionValueField"]').exists()).toBe(false); + }); +}); diff --git a/public/pages/CreateTrigger/containers/DefineTrigger/__snapshots__/AnomalyDetectorTrigger.test.js.snap b/public/pages/CreateTrigger/containers/DefineTrigger/__snapshots__/AnomalyDetectorTrigger.test.js.snap new file mode 100644 index 000000000..7f598c380 --- /dev/null +++ b/public/pages/CreateTrigger/containers/DefineTrigger/__snapshots__/AnomalyDetectorTrigger.test.js.snap @@ -0,0 +1,88 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AnomalyDetectorTrigger renders no detector id 1`] = ` +
+
+
+ +
+
+ You must specify a detector. +
+
+
+
+
+
+`; + +exports[`AnomalyDetectorTrigger renders no feature 1`] = ` +
+
+
+ +
+
+ No features have been added to this anomaly detector. A feature is a metric that is used for anomaly detection. A detector can discover anomalies across one or more features. +
+
+
+ +
+
+`; diff --git a/public/pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats.js b/public/pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats.js index 3b6d574fc..d9844b262 100644 --- a/public/pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats.js +++ b/public/pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats.js @@ -78,7 +78,7 @@ export default function getOverviewStats( href={`${OPENSEARCH_DASHBOARDS_AD_PLUGIN}#/detectors/${detectorId}`} target="_blank" > - {detector.name} + {detector.name} ), }, diff --git a/public/pages/MonitorDetails/components/OverviewStat/OverviewStat.js b/public/pages/MonitorDetails/components/OverviewStat/OverviewStat.js index b277bf812..0fa6f3c4a 100644 --- a/public/pages/MonitorDetails/components/OverviewStat/OverviewStat.js +++ b/public/pages/MonitorDetails/components/OverviewStat/OverviewStat.js @@ -18,7 +18,7 @@ const OverviewStat = ({ header, value }) => ( OverviewStat.propTypes = { header: PropTypes.string.isRequired, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.element]).isRequired, }; export default OverviewStat; diff --git a/public/pages/MonitorDetails/containers/AnomalyHistory/AnomalyHistory.js b/public/pages/MonitorDetails/containers/AnomalyHistory/AnomalyHistory.js deleted file mode 100644 index eb2af0f15..000000000 --- a/public/pages/MonitorDetails/containers/AnomalyHistory/AnomalyHistory.js +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { Component } from 'react'; -import moment from 'moment'; -import { get } from 'lodash'; -import PropTypes from 'prop-types'; -import { EuiEmptyPrompt, EuiText, EuiSpacer } from '@elastic/eui'; -import { AnomalyDetectorData } from '../../../CreateMonitor/containers/AnomalyDetectors/AnomalyDetectorData'; -import { EmptyFeaturesMessage } from '../../../CreateMonitor/components/AnomalyDetectors/EmptyFeaturesMessage/EmptyFeaturesMessage'; -import ContentPanel from '../../../../components/ContentPanel'; -import { AnomaliesChart } from '../../../CreateMonitor/components/AnomalyDetectors/AnomaliesChart'; -import { FeatureChart } from '../../../CreateMonitor/components/AnomalyDetectors/FeatureChart/FeatureChart'; -import DateRangePicker from '../MonitorHistory/DateRangePicker'; - -const DEFAULT_ANOMALY_TIME_WINDOW_DAYS = 5; -class AnomalyHistory extends Component { - constructor(props) { - super(props); - this.initialStartTime = moment(Date.now()) - .subtract(DEFAULT_ANOMALY_TIME_WINDOW_DAYS, 'days') - .startOf('day'); - this.initialEndTime = moment(Date.now()); - this.state = { - startTime: this.initialStartTime, - endTime: this.initialEndTime, - }; - } - handleRangeChange = (startTime, endTime) => { - this.setState({ startTime, endTime }); - }; - render() { - const { detectorId, monitorLastEnabledTime } = this.props; - return ( - , - ]} - > - { - let featureData = []; - //Skip disabled features showing from Alerting. - featureData = get(anomalyData, 'detector.featureAttributes', []) - .filter((feature) => feature.featureEnabled) - .map((feature, index) => ({ - featureName: feature.featureName, - data: anomalyData.anomalyResult.featureData[feature.featureId] || [], - })); - const annotations = get(anomalyData, 'anomalyResult.anomalies', []) - .filter( - (anomaly) => anomaly.anomalyGrade > 0 && anomaly.startTime >= monitorLastEnabledTime - ) - .map((anomaly) => ({ - coordinates: { - x0: anomaly.startTime, - x1: anomaly.endTime, - }, - details: `There is an anomaly with confidence ${anomaly.confidence}`, - })); - if ( - !anomalyData.isLoading && - get(anomalyData, 'anomalyResult.anomalies', []).length === 0 - ) { - return ( - No anomalies found for this detector.} - /> - ); - } - return ( - - anomaly.startTime > monitorLastEnabledTime - )} - isLoading={anomalyData.isLoading} - title="Anomalies" - displayGrade - displayConfidence - /> - - - - ); - }} - /> - - ); - } -} - -AnomalyHistory.PropTypes = { - detectorId: PropTypes.string.isRequired, -}; -export { AnomalyHistory }; diff --git a/public/utils/constants.js b/public/utils/constants.js index c6aae5e80..9511ae0d2 100644 --- a/public/utils/constants.js +++ b/public/utils/constants.js @@ -86,3 +86,12 @@ export const CHANNEL_TYPE = Object.freeze({ [BACKEND_CHANNEL_TYPE.SES]: 'Amazon SES', [BACKEND_CHANNEL_TYPE.SNS]: 'Amazon SNS', }); + +export const DEFAULT_PREVIEW_ERROR_MSG = 'There was a problem previewing the detector.'; + +export const PREVIEW_ERROR_TYPE = { + EXCEPTION: 0, + NO_FEATURE: 1, + NO_ENABLED_FEATURES: 2, + SPARSE_DATA: 3, +}; diff --git a/yarn.lock b/yarn.lock index a004c785f..d35b800d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3561,6 +3561,11 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg== +prettier@^2.1.1: + version "2.8.4" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.4.tgz#34dd2595629bfbb79d344ac4a91ff948694463c3" + integrity sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw== + pretty-bytes@^5.4.1: version "5.6.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"