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"