From 4c56d6d2a9685c0bd0be35036ec3e130d51d7c28 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Thu, 1 Dec 2022 13:06:15 -0800 Subject: [PATCH 01/43] Add branch constants in CI workflow (#345) Signed-off-by: Tyler Ohlsen --- .github/workflows/remote-integ-tests-workflow.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/remote-integ-tests-workflow.yml b/.github/workflows/remote-integ-tests-workflow.yml index bf05f8ec..82458913 100644 --- a/.github/workflows/remote-integ-tests-workflow.yml +++ b/.github/workflows/remote-integ-tests-workflow.yml @@ -12,6 +12,8 @@ on: env: OPENSEARCH_DASHBOARDS_VERSION: 'main' OPENSEARCH_VERSION: '3.0.0-SNAPSHOT' + OPENSEARCH_DASHBOARDS_FTREPO_VERSION: 'main' + ANOMALY_DETECTION_PLUGIN_VERSION: 'main' jobs: test-without-security: name: Run integ tests without security @@ -26,7 +28,7 @@ jobs: with: path: anomaly-detection repository: opensearch-project/anomaly-detection - ref: 'main' + ref: ${{ env.ANOMALY_DETECTION_PLUGIN_VERSION }} - name: Run Opensearch with plugin run: | cd anomaly-detection @@ -72,7 +74,7 @@ jobs: with: path: opensearch-dashboards-functional-test repository: opensearch-project/opensearch-dashboards-functional-test - ref: 'main' # TODO: change to a branch when the branching strategy in that repo has been established + ref: ${{ env.OPENSEARCH_DASHBOARDS_FTREPO_VERSION }} - name: Get Cypress version id: cypress_version run: | From 4928c5035ea6029dc1eae33fa25fdb7191d2bf94 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Fri, 16 Dec 2022 09:59:03 -0800 Subject: [PATCH 02/43] Bump decode-uri-component (#359) Signed-off-by: Tyler Ohlsen --- package.json | 3 ++- yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index d997ca9d..42e3d382 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "**/ansi-regex": "^5.0.1", "**/glob-parent": "^6.0.0", "**/loader-utils": "^2.0.4", - "**/terser": "^4.8.1" + "**/terser": "^4.8.1", + "decode-uri-component": "^0.2.1" } } diff --git a/yarn.lock b/yarn.lock index 0abf4949..6841b7d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1070,10 +1070,10 @@ decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= +decode-uri-component@^0.2.0, decode-uri-component@^0.2.1: + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== dedent@^0.7.0: version "0.7.0" From 45b7e6f0fcb6846509f03eb2debe2e71f2150957 Mon Sep 17 00:00:00 2001 From: Amit Galitzky Date: Thu, 22 Dec 2022 11:25:43 -0800 Subject: [PATCH 03/43] removed duplicate popout icon and ran prettier (#382) Signed-off-by: Amit Galitzky --- global-setup.js | 4 +- .../FormattedFormRow/FormattedFormRow.tsx | 2 +- public/models/interfaces.ts | 36 +- .../components/AnomaliesStat/AnomalyStat.tsx | 2 +- .../__snapshots__/AnomalyStat.test.tsx.snap | 28 +- .../components/FeatureChart/FeatureChart.tsx | 27 +- .../containers/AnomalyDetailsChart.tsx | 111 +-- .../containers/FeatureBreakDown.tsx | 8 +- .../AnomalyCharts/utils/anomalyChartUtils.tsx | 4 +- public/pages/AnomalyCharts/utils/constants.ts | 4 +- .../CategoryField/CategoryField.tsx | 2 +- .../__snapshots__/CategoryField.test.tsx.snap | 28 +- .../components/Features/Features.tsx | 2 +- .../containers/ConfigureModel.tsx | 2 +- .../containers/SampleAnomalies.tsx | 2 +- .../ConfigureModel.test.tsx.snap | 96 +- .../EmptyDashboard/EmptyDashboard.tsx | 3 +- .../EmptyDashboard.test.tsx.snap | 12 +- .../CustomResultIndex/CustomResultIndex.tsx | 2 +- .../components/Settings/Settings.tsx | 2 +- .../__snapshots__/Settings.test.tsx.snap | 24 +- .../DefineDetector.test.tsx.snap | 132 +-- .../MonitorCallout/MonitorCallout.tsx | 2 +- .../MonitorCallout.test.tsx.snap | 11 - .../HistoricalJob/HistoricalJob.tsx | 2 +- .../__snapshots__/HistoricalJob.test.tsx.snap | 12 +- .../components/RealTimeJob/RealTimeJob.tsx | 2 +- .../__snapshots__/RealTimeJob.test.tsx.snap | 12 +- .../__snapshots__/DetectorJobs.test.tsx.snap | 24 +- .../containers/AnomalyResults.tsx | 12 +- .../ConfirmActionModals/utils/helpers.tsx | 14 +- .../EmptyHistoricalDetectorResults.tsx | 3 +- ...ptyHistoricalDetectorResults.test.tsx.snap | 12 +- .../components/CreateWorkflowStepDetails.tsx | 2 +- .../containers/AnomalyDetectionOverview.tsx | 2 +- .../AnomalyDetectionOverview.test.tsx.snap | 220 +---- .../__tests__/anomalyResultUtils.test.ts | 837 ++++++++++++------ public/pages/utils/anomalyResultUtils.ts | 137 +-- server/routes/ad.ts | 29 +- test/jest.config.js | 2 +- 40 files changed, 813 insertions(+), 1055 deletions(-) diff --git a/global-setup.js b/global-setup.js index 3c578000..67ca0db6 100644 --- a/global-setup.js +++ b/global-setup.js @@ -1,3 +1,3 @@ export default () => { - process.env.TZ = 'UTC'; - } \ No newline at end of file + process.env.TZ = 'UTC'; +}; diff --git a/public/components/FormattedFormRow/FormattedFormRow.tsx b/public/components/FormattedFormRow/FormattedFormRow.tsx index 7a125664..20cf8daa 100644 --- a/public/components/FormattedFormRow/FormattedFormRow.tsx +++ b/public/components/FormattedFormRow/FormattedFormRow.tsx @@ -35,7 +35,7 @@ export const FormattedFormRow = (props: FormattedFormRowProps) => { {props.hintLink ? ' ' : null} {props.hintLink ? ( - Learn more + Learn more ) : null} diff --git a/public/models/interfaces.ts b/public/models/interfaces.ts index e2eea176..f8fbc248 100644 --- a/public/models/interfaces.ts +++ b/public/models/interfaces.ts @@ -82,27 +82,27 @@ export type FeatureAttributes = { // all possible valid units accepted by the backend export enum UNITS { - NANOS = "Nanos", - MICROS = "Micros", - MILLIS = "Millis", - SECONDS = "Seconds", + NANOS = 'Nanos', + MICROS = 'Micros', + MILLIS = 'Millis', + SECONDS = 'Seconds', MINUTES = 'Minutes', - HOURS = "Hours", - HALF_DAYS = "HalfDays", - DAYS = "Days", - WEEKS = "Weeks", - MONTHS = "Months", - YEARS = "Years", - DECADES = "Decades", - CENTURIES = "Centuries", - MILLENNIA = "Millennia", - ERAS = "Eras", - FOREVER = "Forever" + HOURS = 'Hours', + HALF_DAYS = 'HalfDays', + DAYS = 'Days', + WEEKS = 'Weeks', + MONTHS = 'Months', + YEARS = 'Years', + DECADES = 'Decades', + CENTURIES = 'Centuries', + MILLENNIA = 'Millennia', + ERAS = 'Eras', + FOREVER = 'Forever', } // cannot create a method in enum, have to write function separately export function toDuration(units: UNITS): Duration { - switch(units) { + switch (units) { case UNITS.NANOS: { // Duration in moment library does not support return moment.duration(0.000000001, 'seconds'); @@ -155,7 +155,7 @@ export function toDuration(units: UNITS): Duration { default: break; } - throw new Error("Unexpected unit: " + units); + throw new Error('Unexpected unit: ' + units); } export type Schedule = { @@ -235,7 +235,7 @@ export type AnomalyData = { entity?: EntityData[]; features?: { [key: string]: FeatureAggregationData }; contributions?: { [key: string]: FeatureContributionData }; - aggInterval?: string; + aggInterval?: string; }; export type FeatureAggregationData = { diff --git a/public/pages/AnomalyCharts/components/AnomaliesStat/AnomalyStat.tsx b/public/pages/AnomalyCharts/components/AnomaliesStat/AnomalyStat.tsx index 56c4db8c..2f82cccf 100644 --- a/public/pages/AnomalyCharts/components/AnomaliesStat/AnomalyStat.tsx +++ b/public/pages/AnomalyCharts/components/AnomaliesStat/AnomalyStat.tsx @@ -91,7 +91,7 @@ export const AlertsStat = (props: { target="_blank" style={{ fontSize: '14px' }} > - View monitor + View monitor ) : null} diff --git a/public/pages/AnomalyCharts/components/AnomaliesStat/__tests__/__snapshots__/AnomalyStat.test.tsx.snap b/public/pages/AnomalyCharts/components/AnomaliesStat/__tests__/__snapshots__/AnomalyStat.test.tsx.snap index fbc59913..4577f90a 100644 --- a/public/pages/AnomalyCharts/components/AnomaliesStat/__tests__/__snapshots__/AnomalyStat.test.tsx.snap +++ b/public/pages/AnomalyCharts/components/AnomaliesStat/__tests__/__snapshots__/AnomalyStat.test.tsx.snap @@ -38,21 +38,7 @@ exports[` spec Alert Stat renders component with monitor and loadi style="font-size: 14px;" target="_blank" > - View monitor - + View monitor spec Alert Stat renders component with monitor and not l style="font-size: 14px;" target="_blank" > - View monitor - + View monitor
{featureName}: {dataString}
- ) - }) + ); + }); } else { for (const [, value] of Object.entries(contributionData)) { featureAttributionList.push(
{value.name}: {value.attribution}
- ) + ); } } return ( @@ -379,35 +382,33 @@ export const AnomalyDetailsChart = React.memo( Feature Contribution: {anomaly ? ( -

-


- {featureAttributionList} -

- ) : null} +

+


+ {featureAttributionList} +

+ ) : null}
); }; - const generateContributionAnomalyAnnotations = ( anomalies: AnomalyData[][] ): any[][] => { let annotations = [] as any[]; anomalies.forEach((anomalyTimeSeries: AnomalyData[]) => { annotations.push( - Array.isArray(anomalyTimeSeries) ? ( - anomalyTimeSeries - .filter((anomaly: AnomalyData) => anomaly.anomalyGrade > 0) - .map((anomaly: AnomalyData) => ( - { - coordinates: { - x0: anomaly.startTime, - x1: anomaly.endTime + (anomaly.endTime - anomaly.startTime), - }, - details: `${JSON.stringify(anomaly)}` - })) - ) : [] + Array.isArray(anomalyTimeSeries) + ? anomalyTimeSeries + .filter((anomaly: AnomalyData) => anomaly.anomalyGrade > 0) + .map((anomaly: AnomalyData) => ({ + coordinates: { + x0: anomaly.startTime, + x1: anomaly.endTime + (anomaly.endTime - anomaly.startTime), + }, + details: `${JSON.stringify(anomaly)}`, + })) + : [] ); }); return annotations; @@ -601,7 +602,9 @@ export const AnomalyDetailsChart = React.memo( /> )} - + /> + {alertAnnotations ? ( - ) + : props.anomalyGradeSeriesName; + return ( + + ); } )} diff --git a/public/pages/AnomalyCharts/containers/FeatureBreakDown.tsx b/public/pages/AnomalyCharts/containers/FeatureBreakDown.tsx index 51dd1318..e939e64d 100644 --- a/public/pages/AnomalyCharts/containers/FeatureBreakDown.tsx +++ b/public/pages/AnomalyCharts/containers/FeatureBreakDown.tsx @@ -176,11 +176,9 @@ export const FeatureBreakDown = React.memo((props: FeatureBreakDownProps) => { detectorEnabledTime={props.detector.enabledTime} entityData={getEntityDataForChart(props.anomalyAndFeatureResults)} isHCDetector={props.isHCDetector} - windowDelay={ - get(props, `detector.windowDelay.period`, { - period: { interval: 0, unit: UNITS.MINUTES }, - }) - } + windowDelay={get(props, `detector.windowDelay.period`, { + period: { interval: 0, unit: UNITS.MINUTES }, + })} /> {index + 1 === get(props, 'detector.featureAttributes', []).length ? null : ( diff --git a/public/pages/AnomalyCharts/utils/anomalyChartUtils.tsx b/public/pages/AnomalyCharts/utils/anomalyChartUtils.tsx index 23c7031d..b2603ada 100644 --- a/public/pages/AnomalyCharts/utils/anomalyChartUtils.tsx +++ b/public/pages/AnomalyCharts/utils/anomalyChartUtils.tsx @@ -640,9 +640,7 @@ export const getFeatureBreakdownWording = ( return isNotSample ? 'Feature breakdown' : 'Sample feature breakdown'; }; -export const getFeatureDataWording = ( - isNotSample: boolean | undefined -) => { +export const getFeatureDataWording = (isNotSample: boolean | undefined) => { return isNotSample ? 'Feature output' : 'Sample feature output'; }; diff --git a/public/pages/AnomalyCharts/utils/constants.ts b/public/pages/AnomalyCharts/utils/constants.ts index 5b699a1b..88afc988 100644 --- a/public/pages/AnomalyCharts/utils/constants.ts +++ b/public/pages/AnomalyCharts/utils/constants.ts @@ -24,7 +24,7 @@ export enum CHART_FIELDS { CONFIDENCE = 'confidence', DATA = 'data', AGG_INTERVAL = 'aggInterval', - EXPECTED_VALUE = 'expectedValue' + EXPECTED_VALUE = 'expectedValue', } export enum CHART_COLORS { @@ -91,7 +91,7 @@ export const DEFAULT_ANOMALY_SUMMARY = { maxAnomalyGrade: 0, minConfidence: 0, maxConfidence: 0, - lastAnomalyOccurrence: '-' + lastAnomalyOccurrence: '-', }; export const HEATMAP_CHART_Y_AXIS_WIDTH = 30; diff --git a/public/pages/ConfigureModel/components/CategoryField/CategoryField.tsx b/public/pages/ConfigureModel/components/CategoryField/CategoryField.tsx index 9afe53a1..130e64d5 100644 --- a/public/pages/ConfigureModel/components/CategoryField/CategoryField.tsx +++ b/public/pages/ConfigureModel/components/CategoryField/CategoryField.tsx @@ -76,7 +76,7 @@ export function CategoryField(props: CategoryFieldProps) { Split a single time series into multiple time series based on categorical fields. You can select up to 2.{' '} - Learn more + Learn more } diff --git a/public/pages/ConfigureModel/components/CategoryField/__tests__/__snapshots__/CategoryField.test.tsx.snap b/public/pages/ConfigureModel/components/CategoryField/__tests__/__snapshots__/CategoryField.test.tsx.snap index a1117607..557d3b8d 100644 --- a/public/pages/ConfigureModel/components/CategoryField/__tests__/__snapshots__/CategoryField.test.tsx.snap +++ b/public/pages/ConfigureModel/components/CategoryField/__tests__/__snapshots__/CategoryField.test.tsx.snap @@ -50,17 +50,7 @@ exports[` spec renders the component when disabled 1`] = ` rel="noopener noreferrer" target="_blank" > - Learn more - + Learn more spec renders the component when enabled 1`] = ` rel="noopener noreferrer" target="_blank" > - Learn more - + Learn more - Learn more + Learn more } diff --git a/public/pages/ConfigureModel/containers/ConfigureModel.tsx b/public/pages/ConfigureModel/containers/ConfigureModel.tsx index 9c589374..b2a21696 100644 --- a/public/pages/ConfigureModel/containers/ConfigureModel.tsx +++ b/public/pages/ConfigureModel/containers/ConfigureModel.tsx @@ -249,7 +249,7 @@ export function ConfigureModel(props: ConfigureModelProps) { and other optional parameters, you can preview your anomalies from a sample feature output.{' '} - Learn more + Learn more diff --git a/public/pages/ConfigureModel/containers/SampleAnomalies.tsx b/public/pages/ConfigureModel/containers/SampleAnomalies.tsx index 7040215c..a6ad30ad 100644 --- a/public/pages/ConfigureModel/containers/SampleAnomalies.tsx +++ b/public/pages/ConfigureModel/containers/SampleAnomalies.tsx @@ -204,7 +204,7 @@ export function SampleAnomalies(props: SampleAnomaliesProps) { ? 'You can preview how your anomalies may look like from sample feature output and adjust the feature settings as needed.' : 'Use the sample data as a reference to fine tune settings. To see the latest preview with your adjustments, click "Refresh preview". Once you are done with your edits, save your changes and run the detector to see real time anomalies for the new data set.'}{' '} - Learn more + Learn more diff --git a/public/pages/ConfigureModel/containers/__tests__/__snapshots__/ConfigureModel.test.tsx.snap b/public/pages/ConfigureModel/containers/__tests__/__snapshots__/ConfigureModel.test.tsx.snap index fdf2f51c..3c171c09 100644 --- a/public/pages/ConfigureModel/containers/__tests__/__snapshots__/ConfigureModel.test.tsx.snap +++ b/public/pages/ConfigureModel/containers/__tests__/__snapshots__/ConfigureModel.test.tsx.snap @@ -35,17 +35,7 @@ exports[` spec creating model configuration renders the compon rel="noopener noreferrer" target="_blank" > - Learn more - + Learn more spec creating model configuration renders the compon rel="noopener noreferrer" target="_blank" > - Learn more - + Learn more spec creating model configuration renders the compon rel="noopener noreferrer" target="_blank" > - Learn more - + Learn more spec creating model configuration renders the compon rel="noopener noreferrer" target="_blank" > - Learn more - + Learn more spec editing model configuration renders the compone rel="noopener noreferrer" target="_blank" > - Learn more - + Learn more spec editing model configuration renders the compone rel="noopener noreferrer" target="_blank" > - Learn more - + Learn more spec editing model configuration renders the compone rel="noopener noreferrer" target="_blank" > - Learn more - + Learn more {

Read about{' '} - Get started with Anomaly detection   - + Get started with Anomaly detection {' '}

diff --git a/public/pages/Dashboard/Components/EmptyDashboard/__tests__/__snapshots__/EmptyDashboard.test.tsx.snap b/public/pages/Dashboard/Components/EmptyDashboard/__tests__/__snapshots__/EmptyDashboard.test.tsx.snap index 57fb8f90..03d27a8e 100644 --- a/public/pages/Dashboard/Components/EmptyDashboard/__tests__/__snapshots__/EmptyDashboard.test.tsx.snap +++ b/public/pages/Dashboard/Components/EmptyDashboard/__tests__/__snapshots__/EmptyDashboard.test.tsx.snap @@ -34,17 +34,7 @@ exports[` spec Empty results renders component with empty messa rel="noopener noreferrer" target="_blank" > - Get started with Anomaly detection   - + Get started with Anomaly detection

- Attempting to initialize the detector with historical data. This - initializing process takes approximately{' '} - {get(detector, 'detectionInterval.period.interval', 10)}{' '} - minutes. + Attempting to initialize the detector with historical data. + This initializing process takes approximately 1 minute if + you have data in each of the last 40 consecutive intervals.

From aec53391f203c0b924995ca239eda3c7ae095150 Mon Sep 17 00:00:00 2001 From: Kaituo Li Date: Mon, 30 Jan 2023 09:36:18 -0800 Subject: [PATCH 11/43] upgrade filter bug (#402) Previously, we didn't actually add filter type when loading old detector. This PR fixed that. Testing done: 1. added a unit tes 2. verified e2et Signed-off-by: Kaituo Li --- .../utils/__tests__/helpers.test.tsx | 60 ++++++++++++++++++- public/pages/DefineDetector/utils/helpers.ts | 8 +-- 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/public/pages/DefineDetector/utils/__tests__/helpers.test.tsx b/public/pages/DefineDetector/utils/__tests__/helpers.test.tsx index e44682f6..b6d9590d 100644 --- a/public/pages/DefineDetector/utils/__tests__/helpers.test.tsx +++ b/public/pages/DefineDetector/utils/__tests__/helpers.test.tsx @@ -10,12 +10,13 @@ */ import { INITIAL_DETECTOR_DEFINITION_VALUES } from '../../utils/constants'; +import { DATA_TYPES } from '../../../../utils/constants'; import { getRandomDetector } from '../../../../redux/reducers/__tests__/utils'; import { detectorDefinitionToFormik, filtersToFormik, } from '../../utils/helpers'; -import { Detector } from '../../../../models/interfaces'; +import { Detector, OPERATORS_MAP, FILTER_TYPES } from '../../../../models/interfaces'; describe('detectorDefinitionToFormik', () => { test('should return initialValues if detector is null', () => { @@ -53,4 +54,61 @@ describe('detectorDefinitionToFormik', () => { windowDelay: randomDetector.windowDelay.period.interval, }); }); + test('upgrade old detector\'s filters to include filter type', () => { + const randomDetector = getRandomDetector(); + randomDetector.uiMetadata = { + features: {}, + filters : [ + { + fieldInfo : [ + { + label : 'service', + type : DATA_TYPES.KEYWORD + } + ], + fieldValue : "app_3", + operator : OPERATORS_MAP.IS + }, + { + fieldInfo : [ + { + label : "host", + type : DATA_TYPES.KEYWORD + } + ], + fieldValue : "server_2", + operator : OPERATORS_MAP.IS + } + ], + filterType : FILTER_TYPES.SIMPLE + }; + const adFormikValues = filtersToFormik(randomDetector); + expect(adFormikValues).toEqual( + [ + { + fieldInfo : [ + { + label : 'service', + type : DATA_TYPES.KEYWORD + } + ], + fieldValue : "app_3", + operator : OPERATORS_MAP.IS, + filterType : FILTER_TYPES.SIMPLE + }, + { + fieldInfo : [ + { + label : "host", + type : DATA_TYPES.KEYWORD + } + ], + fieldValue : "server_2", + operator : OPERATORS_MAP.IS, + filterType : FILTER_TYPES.SIMPLE + } + ] + ); + }); + }); diff --git a/public/pages/DefineDetector/utils/helpers.ts b/public/pages/DefineDetector/utils/helpers.ts index 579a500f..fd427913 100644 --- a/public/pages/DefineDetector/utils/helpers.ts +++ b/public/pages/DefineDetector/utils/helpers.ts @@ -89,12 +89,8 @@ export function filtersToFormik(detector: Detector): UIFilter[] { }, ]; } else { - curFilters.forEach((filter: UIFilter) => { - return { - ...filter, - filterType: curFilterType, - }; - }); + curFilters.forEach((filter: UIFilter) => + filter.filterType = curFilterType); } } return curFilters; From 4847d129835c419de68777822247cafebc93b260 Mon Sep 17 00:00:00 2001 From: Kaituo Li Date: Thu, 9 Feb 2023 13:38:56 -0800 Subject: [PATCH 12/43] Changed required minimum intervals in cold start message (#411) Required minimum intervals to finish cold start is related to shingle size. This PR adds the shingle size in the computation and puts the result in the cold start message. Testing done: 1. verified changing shingle size will change the message. Signed-off-by: Kaituo Li --- public/pages/DetectorResults/containers/AnomalyResults.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/pages/DetectorResults/containers/AnomalyResults.tsx b/public/pages/DetectorResults/containers/AnomalyResults.tsx index 9759b121..2e769d08 100644 --- a/public/pages/DetectorResults/containers/AnomalyResults.tsx +++ b/public/pages/DetectorResults/containers/AnomalyResults.tsx @@ -66,6 +66,7 @@ import { detectorIsSample } from '../../Overview/utils/helpers'; import { SampleIndexDetailsCallout } from '../../Overview/components/SampleIndexDetailsCallout/SampleIndexDetailsCallout'; import { CoreStart } from '../../../../../../src/core/public'; import { CoreServicesContext } from '../../../components/CoreServices/CoreServices'; +import { DEFAULT_SHINGLE_SIZE } from '../../../utils/constants'; interface AnomalyResultsProps extends RouteComponentProps { detectorId: string; @@ -313,7 +314,8 @@ export function AnomalyResults(props: AnomalyResultsProps) {

Attempting to initialize the detector with historical data. This initializing process takes approximately 1 minute if - you have data in each of the last 40 consecutive intervals. + you have data in each of the last{' '} + {32+get(detector, 'shingleSize', DEFAULT_SHINGLE_SIZE)}{' '} consecutive intervals.

From ed47e942449bb3d3b1ee349c706494aeaa33b2fe Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Mon, 13 Feb 2023 16:58:15 -0800 Subject: [PATCH 13/43] Bump @sideway/formula to 3.0.1 (#418) Signed-off-by: Tyler Ohlsen --- package.json | 3 ++- yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 1307bc17..a6875522 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "**/loader-utils": "^2.0.4", "**/terser": "^4.8.1", "decode-uri-component": "^0.2.1", - "json5": "^2.2.3" + "json5": "^2.2.3", + "@sideway/formula": "^3.0.1" } } diff --git a/yarn.lock b/yarn.lock index e3015264..b838e0eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -60,10 +60,10 @@ dependencies: "@hapi/hoek" "^9.0.0" -"@sideway/formula@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c" - integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg== +"@sideway/formula@^3.0.0", "@sideway/formula@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" + integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== "@sideway/pinpoint@^2.0.0": version "2.0.0" From ec57b1d978eeabd509dc25db5e5c27b154d4684e Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Wed, 15 Feb 2023 10:34:38 -0800 Subject: [PATCH 14/43] Remove auto_expand_replicas override in sample data indices (#423) Signed-off-by: Tyler Ohlsen --- public/pages/Overview/utils/constants.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/pages/Overview/utils/constants.tsx b/public/pages/Overview/utils/constants.tsx index 4b2536c3..0e62c05c 100644 --- a/public/pages/Overview/utils/constants.tsx +++ b/public/pages/Overview/utils/constants.tsx @@ -14,11 +14,10 @@ import moment from 'moment'; import React from 'react'; import { EuiIcon } from '@elastic/eui'; -// same as default OpenSearch Dashboards sample data +// We don't need to specify auto_expand_replicas - use cluster defaults instead export const indexSettings = { index: { number_of_shards: 1, - auto_expand_replicas: '0-1', }, }; From 071f836e5b0aa9b9132c8c5599955d2e4294c4e9 Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Mon, 20 Feb 2023 18:18:28 -0500 Subject: [PATCH 15/43] Created untriaged issue workflow. (#422) Signed-off-by: dblock --- .github/workflows/add-untriaged.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/add-untriaged.yml diff --git a/.github/workflows/add-untriaged.yml b/.github/workflows/add-untriaged.yml new file mode 100644 index 00000000..9dcc7020 --- /dev/null +++ b/.github/workflows/add-untriaged.yml @@ -0,0 +1,19 @@ +name: Apply 'untriaged' label during issue lifecycle + +on: + issues: + types: [opened, reopened, transferred] + +jobs: + apply-label: + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v6 + with: + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['untriaged'] + }) From e2f1238e90c528e5b21de39ca2aee0dacd17b4b2 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Mon, 20 Feb 2023 15:22:22 -0800 Subject: [PATCH 16/43] Add 2.6 release notes (#429) Signed-off-by: Tyler Ohlsen --- ...etection-dashboards.release-notes-2.6.0.0.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 release-notes/opensearch-anomaly-detection-dashboards.release-notes-2.6.0.0.md diff --git a/release-notes/opensearch-anomaly-detection-dashboards.release-notes-2.6.0.0.md b/release-notes/opensearch-anomaly-detection-dashboards.release-notes-2.6.0.0.md new file mode 100644 index 00000000..2e217ea4 --- /dev/null +++ b/release-notes/opensearch-anomaly-detection-dashboards.release-notes-2.6.0.0.md @@ -0,0 +1,17 @@ +## Version 2.6.0.0 Release Notes + +Compatible with OpenSearch Dashboards 2.6.0 + +### Enhancements + +* Update cold start message ([#398](https://github.com/opensearch-project/anomaly-detection-dashboards-plugin/pull/398)) +* Changed required minimum intervals in cold start message ([#411](https://github.com/opensearch-project/anomaly-detection-dashboards-plugin/pull/411)) +* Remove `auto_expand_replicas` override in sample data indices ([#423](https://github.com/opensearch-project/anomaly-detection-dashboards-plugin/pull/423)) + +### Bug Fixes + +* upgrade filter bug ([#402](https://github.com/opensearch-project/anomaly-detection-dashboards-plugin/pull/402)) + +### Infrastructure + +* Bump @sideway/formula to 3.0.1 ([#418](https://github.com/opensearch-project/anomaly-detection-dashboards-plugin/pull/418)) From 553cbdd494000fe288c129f2052a8f424c5e8a0e Mon Sep 17 00:00:00 2001 From: Miki Date: Wed, 22 Feb 2023 10:40:28 -0800 Subject: [PATCH 17/43] Fix Node.js and Yarn installation in CI (#433) Signed-off-by: Miki --- .../workflows/remote-integ-tests-workflow.yml | 25 +++++++++---------- .github/workflows/unit-tests-workflow.yml | 23 ++++++++--------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/.github/workflows/remote-integ-tests-workflow.yml b/.github/workflows/remote-integ-tests-workflow.yml index d57e3b67..55991557 100644 --- a/.github/workflows/remote-integ-tests-workflow.yml +++ b/.github/workflows/remote-integ-tests-workflow.yml @@ -64,23 +64,22 @@ jobs: with: path: OpenSearch-Dashboards/plugins/anomaly-detection-dashboards-plugin - - name: Get node and yarn versions - id: versions_step - run: | - echo "::set-output name=node_version::$(node -p "(require('./OpenSearch-Dashboards/package.json').engines.node).match(/[.0-9]+/)[0]")" - echo "::set-output name=yarn_version::$(node -p "(require('./OpenSearch-Dashboards/package.json').engines.yarn).match(/[.0-9]+/)[0]")" - - - name: Setup node - uses: actions/setup-node@v1 + - name: Setup Node + uses: actions/setup-node@v3 with: - node-version: ${{ steps.versions_step.outputs.node_version }} + node-version-file: './OpenSearch-Dashboards/.nvmrc' registry-url: 'https://registry.npmjs.org' - - name: Install correct yarn version for OpenSearch Dashboards + - name: Install Yarn + # Need to use bash to avoid having a windows/linux specific step + shell: bash run: | - npm uninstall -g yarn - echo "Installing yarn ${{ steps.versions_step.outputs.yarn_version }}" - npm i -g yarn@${{ steps.versions_step.outputs.yarn_version }} + YARN_VERSION=$(node -p "require('./OpenSearch-Dashboards/package.json').engines.yarn") + echo "Installing yarn@$YARN_VERSION" + npm i -g yarn@$YARN_VERSION + + - run: node -v + - run: yarn -v - name: Bootstrap the plugin run: | diff --git a/.github/workflows/unit-tests-workflow.yml b/.github/workflows/unit-tests-workflow.yml index 838d9ae2..abe1743c 100644 --- a/.github/workflows/unit-tests-workflow.yml +++ b/.github/workflows/unit-tests-workflow.yml @@ -26,21 +26,20 @@ jobs: repository: opensearch-project/OpenSearch-Dashboards ref: ${{ env.OPENSEARCH_DASHBOARDS_VERSION }} path: OpenSearch-Dashboards - - name: Get node and yarn versions - id: versions_step - run: | - echo "::set-output name=node_version::$(node -p "(require('./OpenSearch-Dashboards/package.json').engines.node).match(/[.0-9]+/)[0]")" - echo "::set-output name=yarn_version::$(node -p "(require('./OpenSearch-Dashboards/package.json').engines.yarn).match(/[.0-9]+/)[0]")" - - name: Setup node - uses: actions/setup-node@v1 + - name: Setup Node + uses: actions/setup-node@v3 with: - node-version: ${{ steps.versions_step.outputs.node_version }} + node-version-file: './OpenSearch-Dashboards/.nvmrc' registry-url: 'https://registry.npmjs.org' - - name: Install correct yarn version for OpenSearch Dashboards + - name: Install Yarn + # Need to use bash to avoid having a windows/linux specific step + shell: bash run: | - npm uninstall -g yarn - echo "Installing yarn ${{ steps.versions_step.outputs.yarn_version }}" - npm i -g yarn@${{ steps.versions_step.outputs.yarn_version }} + YARN_VERSION=$(node -p "require('./OpenSearch-Dashboards/package.json').engines.yarn") + echo "Installing yarn@$YARN_VERSION" + npm i -g yarn@$YARN_VERSION + - run: node -v + - run: yarn -v - name: Checkout Anomaly Detection OpenSearch Dashboards plugin uses: actions/checkout@v2 with: From e9f8eac636507ac93f09e77a6bf0425e4a51ea5f Mon Sep 17 00:00:00 2001 From: Jackie Han Date: Mon, 27 Mar 2023 15:29:18 -0700 Subject: [PATCH 18/43] run prettier command against all files Signed-off-by: Jackie Han --- .../containers/AnomaliesChart.tsx | 9 +- .../containers/AnomalyDetailsChart.tsx | 16 ++-- .../utils/__tests__/helpers.test.tsx | 89 ++++++++++--------- public/pages/DefineDetector/utils/helpers.ts | 5 +- .../containers/AnomalyResults.tsx | 9 +- 5 files changed, 66 insertions(+), 62 deletions(-) diff --git a/public/pages/AnomalyCharts/containers/AnomaliesChart.tsx b/public/pages/AnomalyCharts/containers/AnomaliesChart.tsx index a380f806..0a9a4e03 100644 --- a/public/pages/AnomalyCharts/containers/AnomaliesChart.tsx +++ b/public/pages/AnomalyCharts/containers/AnomaliesChart.tsx @@ -110,13 +110,10 @@ export const AnomaliesChart = React.memo((props: AnomaliesChartProps) => { const handleDateRangeChange = (startDate: number, endDate: number) => { props.onDateRangeChange(startDate, endDate); props.onZoomRangeChange(startDate, endDate); - if ( - !props.isHistorical && - endDate < get(props, 'detector.enabledTime') - ) { - setShowOutOfRangeCallOut(true) + if (!props.isHistorical && endDate < get(props, 'detector.enabledTime')) { + setShowOutOfRangeCallOut(true); } else { - setShowOutOfRangeCallOut(false) + setShowOutOfRangeCallOut(false); } }; diff --git a/public/pages/AnomalyCharts/containers/AnomalyDetailsChart.tsx b/public/pages/AnomalyCharts/containers/AnomalyDetailsChart.tsx index 0743f46c..e2116e30 100644 --- a/public/pages/AnomalyCharts/containers/AnomalyDetailsChart.tsx +++ b/public/pages/AnomalyCharts/containers/AnomalyDetailsChart.tsx @@ -427,15 +427,19 @@ export const AnomalyDetailsChart = React.memo( return ( {props.openOutOfRangeCallOut ? ( - - {`Your selected dates are not in the range from when the detector + + {`Your selected dates are not in the range from when the detector last started streaming data - (${moment(get(props, 'detector.enabledTime')).format('MM/DD/YYYY hh:mm A')}).`} + (${moment(get(props, 'detector.enabledTime')).format( + 'MM/DD/YYYY hh:mm A' + )}).`} ) : null} - + { test('should return initialValues if detector is null', () => { @@ -54,61 +58,58 @@ describe('detectorDefinitionToFormik', () => { windowDelay: randomDetector.windowDelay.period.interval, }); }); - test('upgrade old detector\'s filters to include filter type', () => { + test("upgrade old detector's filters to include filter type", () => { const randomDetector = getRandomDetector(); randomDetector.uiMetadata = { features: {}, - filters : [ + filters: [ { - fieldInfo : [ + fieldInfo: [ { - label : 'service', - type : DATA_TYPES.KEYWORD - } + label: 'service', + type: DATA_TYPES.KEYWORD, + }, ], - fieldValue : "app_3", - operator : OPERATORS_MAP.IS + fieldValue: 'app_3', + operator: OPERATORS_MAP.IS, }, { - fieldInfo : [ + fieldInfo: [ { - label : "host", - type : DATA_TYPES.KEYWORD - } + label: 'host', + type: DATA_TYPES.KEYWORD, + }, ], - fieldValue : "server_2", - operator : OPERATORS_MAP.IS - } + fieldValue: 'server_2', + operator: OPERATORS_MAP.IS, + }, ], - filterType : FILTER_TYPES.SIMPLE + filterType: FILTER_TYPES.SIMPLE, }; const adFormikValues = filtersToFormik(randomDetector); - expect(adFormikValues).toEqual( - [ - { - fieldInfo : [ - { - label : 'service', - type : DATA_TYPES.KEYWORD - } - ], - fieldValue : "app_3", - operator : OPERATORS_MAP.IS, - filterType : FILTER_TYPES.SIMPLE - }, - { - fieldInfo : [ - { - label : "host", - type : DATA_TYPES.KEYWORD - } - ], - fieldValue : "server_2", - operator : OPERATORS_MAP.IS, - filterType : FILTER_TYPES.SIMPLE - } - ] - ); + expect(adFormikValues).toEqual([ + { + fieldInfo: [ + { + label: 'service', + type: DATA_TYPES.KEYWORD, + }, + ], + fieldValue: 'app_3', + operator: OPERATORS_MAP.IS, + filterType: FILTER_TYPES.SIMPLE, + }, + { + fieldInfo: [ + { + label: 'host', + type: DATA_TYPES.KEYWORD, + }, + ], + fieldValue: 'server_2', + operator: OPERATORS_MAP.IS, + filterType: FILTER_TYPES.SIMPLE, + }, + ]); }); - }); diff --git a/public/pages/DefineDetector/utils/helpers.ts b/public/pages/DefineDetector/utils/helpers.ts index fd427913..f9e9ae45 100644 --- a/public/pages/DefineDetector/utils/helpers.ts +++ b/public/pages/DefineDetector/utils/helpers.ts @@ -89,8 +89,9 @@ export function filtersToFormik(detector: Detector): UIFilter[] { }, ]; } else { - curFilters.forEach((filter: UIFilter) => - filter.filterType = curFilterType); + curFilters.forEach( + (filter: UIFilter) => (filter.filterType = curFilterType) + ); } } return curFilters; diff --git a/public/pages/DetectorResults/containers/AnomalyResults.tsx b/public/pages/DetectorResults/containers/AnomalyResults.tsx index 2e769d08..827570ce 100644 --- a/public/pages/DetectorResults/containers/AnomalyResults.tsx +++ b/public/pages/DetectorResults/containers/AnomalyResults.tsx @@ -312,10 +312,11 @@ export function AnomalyResults(props: AnomalyResultsProps) { />

- Attempting to initialize the detector with historical data. - This initializing process takes approximately 1 minute if - you have data in each of the last{' '} - {32+get(detector, 'shingleSize', DEFAULT_SHINGLE_SIZE)}{' '} consecutive intervals. + Attempting to initialize the detector with historical data. This + initializing process takes approximately 1 minute if you have + data in each of the last{' '} + {32 + get(detector, 'shingleSize', DEFAULT_SHINGLE_SIZE)}{' '} + consecutive intervals.

From 23c81420b0a71da4d55d19fc64c3661eea12a8b9 Mon Sep 17 00:00:00 2001 From: Amit Galitzky Date: Thu, 4 May 2023 12:26:59 -0700 Subject: [PATCH 19/43] Adding expression function to retrieve anomalies (#448) * adding an AD expression function Signed-off-by: Amit Galitzky * making expression fn changes only to plugin.ts Signed-off-by: Amit Galitzky * addressing comments, changed a few constants Signed-off-by: Amit Galitzky * moving getDetectorName into try catch Signed-off-by: Amit Galitzky --------- Signed-off-by: Amit Galitzky --- opensearch_dashboards.json | 8 +- public/expressions/constants.ts | 11 ++ public/expressions/overlay_anomalies.ts | 215 ++++++++++++++++++++++++ public/plugin.ts | 12 +- public/services.ts | 14 ++ 5 files changed, 257 insertions(+), 3 deletions(-) create mode 100644 public/expressions/constants.ts create mode 100644 public/expressions/overlay_anomalies.ts create mode 100644 public/services.ts diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index 6868b4ed..8fdba21c 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -3,8 +3,12 @@ "version": "3.0.0.0", "opensearchDashboardsVersion": "3.0.0", "configPath": ["anomaly_detection_dashboards"], - "requiredPlugins": ["navigation"], - "optionalPlugins": [], + "requiredPlugins": [ + "opensearchDashboardsUtils", + "expressions", + "data", + "visAugmenter" + ], "server": true, "ui": true } diff --git a/public/expressions/constants.ts b/public/expressions/constants.ts new file mode 100644 index 00000000..032d544c --- /dev/null +++ b/public/expressions/constants.ts @@ -0,0 +1,11 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const ORIGIN_PLUGIN_VIS_LAYER = 'anomalyDetectionDashboards'; + +// Defines the header used when categorizing and grouping the VisLayers on the view event flyout in OSD. +export const VIS_LAYER_PLUGIN_TYPE = 'Anomaly Detectors'; + +export const TYPE_OF_EXPR_VIS_LAYERS = 'vis_layers'; diff --git a/public/expressions/overlay_anomalies.ts b/public/expressions/overlay_anomalies.ts new file mode 100644 index 00000000..ddbc96cf --- /dev/null +++ b/public/expressions/overlay_anomalies.ts @@ -0,0 +1,215 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { get, isEmpty } from 'lodash'; +import { i18n } from '@osd/i18n'; +import { ExpressionFunctionDefinition } from '../../../../src/plugins/expressions/public'; +import { + VisLayerTypes, + VisLayers, + ExprVisLayers, + PluginResource, +} from '../../../../src/plugins/vis_augmenter/public'; +import { + TimeRange, + calculateBounds, +} from '../../../../src/plugins/data/common'; +import { + getAnomalySummaryQuery, + parsePureAnomalies, +} from '../pages/utils/anomalyResultUtils'; +import { AD_NODE_API } from '../../utils/constants'; +import { AnomalyData } from '../models/interfaces'; +import { getClient } from '../services'; +import { + PointInTimeEventsVisLayer, + VisLayerError, + VisLayerErrorTypes, +} from '../../../../src/plugins/vis_augmenter/public'; +import { PLUGIN_NAME } from '../utils/constants'; +import { NO_PERMISSIONS_KEY_WORD } from '../../server/utils/helpers'; +import { + ORIGIN_PLUGIN_VIS_LAYER, + TYPE_OF_EXPR_VIS_LAYERS, + VIS_LAYER_PLUGIN_TYPE, +} from './constants'; + +type Input = ExprVisLayers; +type Output = Promise; + +interface Arguments { + detectorId: string; +} + +const name = 'overlay_anomalies'; + +export type OverlayAnomaliesExpressionFunctionDefinition = + ExpressionFunctionDefinition<'overlay_anomalies', Input, Arguments, Output>; + +// This gets all the needed anomalies for the given detector ID and time range +const getAnomalies = async ( + detectorId: string, + startTime: number, + endTime: number +): Promise => { + const anomalySummaryQuery = getAnomalySummaryQuery( + startTime, + endTime, + detectorId, + undefined, + false + ); + + const anomalySummaryResponse = await getClient().post( + `..${AD_NODE_API.DETECTOR}/results/_search`, + { + body: JSON.stringify(anomalySummaryQuery), + } + ); + + return parsePureAnomalies(anomalySummaryResponse); +}; + +const getDetectorName = async (detectorId: string) => { + const resp = await getClient().get(`..${AD_NODE_API.DETECTOR}/${detectorId}`); + return get(resp.response, 'name', ''); +}; + +// This takes anomalies and returns them as vis layer of type PointInTimeEvents +const convertAnomaliesToPointInTimeEventsVisLayer = ( + anomalies: AnomalyData[], + ADPluginResource: PluginResource +): PointInTimeEventsVisLayer => { + const events = anomalies.map((anomaly: AnomalyData) => { + return { + timestamp: anomaly.startTime + (anomaly.endTime - anomaly.startTime) / 2, + metadata: {}, + }; + }); + return { + originPlugin: ORIGIN_PLUGIN_VIS_LAYER, + type: VisLayerTypes.PointInTimeEvents, + pluginResource: ADPluginResource, + events: events, + } as PointInTimeEventsVisLayer; +}; + +/* + * This function defines the Anomaly Detection expression function of type vis_layers. + * The expression-fn defined takes an argument of detectorId and an array of VisLayers as input, + * it then returns back the VisLayers array with an additional vislayer composed of anomalies. + * + * The purpose of this function is to allow us on the visualization rendering to gather additional + * overlays from an associated plugin resource such as an anomaly detector in this occasion. The VisLayers will + * now have anomaly data as one of its VisLayers. + * + * To create the new added VisLayer the function uses the detectorId and daterange from the search context + * to fetch anomalies. Next, the anomalies are mapped into events based on timestamps in order to convert them to a + * PointInTimeEventsVisLayer. + * + * If there are any errors fetching the anomalies the function will return a VisLayerError in the + * VisLayer detailing the error type. + */ +export const overlayAnomaliesFunction = + (): OverlayAnomaliesExpressionFunctionDefinition => ({ + name, + type: TYPE_OF_EXPR_VIS_LAYERS, + inputTypes: [TYPE_OF_EXPR_VIS_LAYERS], + help: i18n.translate('data.functions.overlay_anomalies.help', { + defaultMessage: 'Add an anomaly vis layer', + }), + args: { + detectorId: { + types: ['string'], + default: '""', + help: '', + }, + }, + + async fn(input, args, context): Promise { + // Parsing all of the args & input + const detectorId = get(args, 'detectorId', ''); + const timeRange = get( + context, + 'searchContext.timeRange', + '' + ) as TimeRange; + const origVisLayers = get(input, 'layers', [] as VisLayers) as VisLayers; + const parsedTimeRange = timeRange ? calculateBounds(timeRange) : null; + const startTimeInMillis = parsedTimeRange?.min?.unix() + ? parsedTimeRange?.min?.unix() * 1000 + : undefined; + const endTimeInMillis = parsedTimeRange?.max?.unix() + ? parsedTimeRange?.max?.unix() * 1000 + : undefined; + var ADPluginResource = { + type: VIS_LAYER_PLUGIN_TYPE, + id: detectorId, + name: '', + urlPath: `${PLUGIN_NAME}#/detectors/${detectorId}/results`, //details page for detector in AD plugin + }; + try { + const detectorName = await getDetectorName(detectorId); + if (detectorName === '') { + throw new Error('Anomaly Detector - Unable to get detector'); + } + ADPluginResource.name = detectorName; + + if (startTimeInMillis === undefined || endTimeInMillis === undefined) { + throw new RangeError('start or end time invalid'); + } + const anomalies = await getAnomalies( + detectorId, + startTimeInMillis, + endTimeInMillis + ); + const anomalyLayer = convertAnomaliesToPointInTimeEventsVisLayer( + anomalies, + ADPluginResource + ); + return { + type: TYPE_OF_EXPR_VIS_LAYERS, + layers: origVisLayers + ? origVisLayers.concat(anomalyLayer) + : ([anomalyLayer] as VisLayers), + }; + } catch (error) { + console.log('Anomaly Detector - Unable to get anomalies: ', error); + let visLayerError: VisLayerError = {} as VisLayerError; + if ( + typeof error === 'string' && + error.includes(NO_PERMISSIONS_KEY_WORD) + ) { + visLayerError = { + type: VisLayerErrorTypes.PERMISSIONS_FAILURE, + message: error, //TODO: might just change this to a generic message like rest of AD plugin + }; + } else { + visLayerError = { + type: VisLayerErrorTypes.FETCH_FAILURE, + message: + error === 'string' + ? error + : error instanceof Error + ? error.message + : '', + }; + } + const anomalyErrorLayer = { + type: VisLayerTypes.PointInTimeEvents, + originPlugin: PLUGIN_NAME, + pluginResource: ADPluginResource, + events: [], + error: visLayerError, + } as PointInTimeEventsVisLayer; + return { + type: TYPE_OF_EXPR_VIS_LAYERS, + layers: origVisLayers + ? origVisLayers.concat(anomalyErrorLayer) + : ([anomalyErrorLayer] as VisLayers), + }; + } + }, + }); diff --git a/public/plugin.ts b/public/plugin.ts index 7ee985bf..2f55a028 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -20,6 +20,8 @@ import { AnomalyDetectionOpenSearchDashboardsPluginSetup, AnomalyDetectionOpenSearchDashboardsPluginStart, } from '.'; +import { overlayAnomaliesFunction } from './expressions/overlay_anomalies'; +import { setClient } from './services'; export class AnomalyDetectionOpenSearchDashboardsPlugin implements @@ -33,7 +35,8 @@ export class AnomalyDetectionOpenSearchDashboardsPlugin } public setup( - core: CoreSetup + core: CoreSetup, + plugins ): AnomalyDetectionOpenSearchDashboardsPluginSetup { core.application.register({ id: 'anomaly-detection-dashboards', @@ -50,6 +53,13 @@ export class AnomalyDetectionOpenSearchDashboardsPlugin return renderApp(coreStart, params); }, }); + + // Set the HTTP client so it can be pulled into expression fns to make + // direct server-side calls + setClient(core.http); + + // registers the expression function used to render anomalies on an Augmented Visualization + plugins.expressions.registerFunction(overlayAnomaliesFunction); return {}; } diff --git a/public/services.ts b/public/services.ts new file mode 100644 index 00000000..d9161693 --- /dev/null +++ b/public/services.ts @@ -0,0 +1,14 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CoreStart } from '../../../src/core/public'; +import { createGetterSetter } from '../../../src/plugins/opensearch_dashboards_utils/public'; +import { SavedObjectLoader } from '../../../src/plugins/saved_objects/public'; + +export const [getSavedFeatureAnywhereLoader, setSavedFeatureAnywhereLoader] = + createGetterSetter('savedFeatureAnywhereLoader'); + +export const [getClient, setClient] = + createGetterSetter('http'); From efef2e268c25cffb7edb67e4c33547d0de8a944d Mon Sep 17 00:00:00 2001 From: Jackie Han Date: Fri, 19 May 2023 15:03:55 -0700 Subject: [PATCH 20/43] Register AD as dashboard context menu option (#482) * Register AD as dashboard context menu option Signed-off-by: Jackie Han * addressing comments Signed-off-by: Jackie Han * add getActions props Signed-off-by: Jackie Han * add EmbeddableStart Signed-off-by: Jackie Han * remove spread operator Signed-off-by: Jackie Han * clenaup Signed-off-by: Jackie Han * add overlay getter and setter Signed-off-by: Jackie Han --------- Signed-off-by: Jackie Han --- opensearch_dashboards.json | 9 +- public/action/ad_dashboard_action.tsx | 78 +++++++++++++ .../AnywhereParentFlyout.tsx | 37 ++++++ .../AnywhereParentFlyout/index.tsx | 8 ++ .../containers/DocumentationTitle.tsx | 28 +++++ .../DocumentationTitle/index.tsx | 8 ++ public/plugin.ts | 109 ++++++++++-------- public/services.ts | 9 +- public/utils/constants.ts | 4 + public/utils/contextMenu/getActions.tsx | 84 ++++++++++++++ 10 files changed, 325 insertions(+), 49 deletions(-) create mode 100644 public/action/ad_dashboard_action.tsx create mode 100644 public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/AnywhereParentFlyout.tsx create mode 100644 public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/index.tsx create mode 100644 public/components/FeatureAnywhereContextMenu/DocumentationTitle/containers/DocumentationTitle.tsx create mode 100644 public/components/FeatureAnywhereContextMenu/DocumentationTitle/index.tsx create mode 100644 public/utils/contextMenu/getActions.tsx diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index 8fdba21c..21cd2fbb 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -7,7 +7,14 @@ "opensearchDashboardsUtils", "expressions", "data", - "visAugmenter" + "visAugmenter", + "uiActions", + "dashboard", + "embeddable", + "opensearchDashboardsReact", + "savedObjects", + "visAugmenter", + "opensearchDashboardsUtils" ], "server": true, "ui": true diff --git a/public/action/ad_dashboard_action.tsx b/public/action/ad_dashboard_action.tsx new file mode 100644 index 00000000..0be356ed --- /dev/null +++ b/public/action/ad_dashboard_action.tsx @@ -0,0 +1,78 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { IEmbeddable } from '../../../../src/plugins/dashboard/public/embeddable_plugin'; +import { + DASHBOARD_CONTAINER_TYPE, + DashboardContainer, +} from '../../../../src/plugins/dashboard/public'; +import { + IncompatibleActionError, + createAction, + Action, +} from '../../../../src/plugins/ui_actions/public'; +import { isReferenceOrValueEmbeddable } from '../../../../src/plugins/embeddable/public'; +import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; + +export const ACTION_AD = 'ad'; + +function isDashboard( + embeddable: IEmbeddable +): embeddable is DashboardContainer { + return embeddable.type === DASHBOARD_CONTAINER_TYPE; +} + +export interface ActionContext { + embeddable: IEmbeddable; +} + +export interface CreateOptions { + grouping: Action['grouping']; + title: string; + icon: EuiIconType; + id: string; + order: number; + onClick: Function; +} + +export const createADAction = ({ + grouping, + title, + icon, + id, + order, + onClick, +}: CreateOptions) => + createAction({ + id, + order, + getDisplayName: ({ embeddable }: ActionContext) => { + if (!embeddable.parent || !isDashboard(embeddable.parent)) { + throw new IncompatibleActionError(); + } + return title; + }, + getIconType: () => icon, + type: ACTION_AD, + grouping, + isCompatible: async ({ embeddable }: ActionContext) => { + const paramsType = embeddable.vis?.params?.type; + const seriesParams = embeddable.vis?.params?.seriesParams || []; + const series = embeddable.vis?.params?.series || []; + const isLineGraph = + seriesParams.find((item) => item.type === 'line') || + series.find((item) => item.chart_type === 'line'); + const isValidVis = isLineGraph && paramsType !== 'table'; + return Boolean( + embeddable.parent && isDashboard(embeddable.parent) && isValidVis + ); + }, + execute: async ({ embeddable }: ActionContext) => { + if (!isReferenceOrValueEmbeddable(embeddable)) { + throw new IncompatibleActionError(); + } + + onClick({ embeddable }); + }, + }); \ No newline at end of file diff --git a/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/AnywhereParentFlyout.tsx b/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/AnywhereParentFlyout.tsx new file mode 100644 index 00000000..2a54a169 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/AnywhereParentFlyout.tsx @@ -0,0 +1,37 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import React, { useState } from 'react'; +import { get } from 'lodash'; +import AddAnomalyDetector from '../CreateAnomalyDetector'; +import { getEmbeddable } from '../../../../public/services'; + +const AnywhereParentFlyout = ({ startingFlyout, ...props }) => { + const embeddable = getEmbeddable().getEmbeddableFactory; + const indices: { label: string }[] = [ + { label: get(embeddable, 'vis.data.indexPattern.title', '') }, + ]; + + const [mode, setMode] = useState(startingFlyout); + const [selectedDetectorId, setSelectedDetectorId] = useState(); + + const AnywhereFlyout = { + create: AddAnomalyDetector, + }[mode]; + + return ( + + ); +}; + +export default AnywhereParentFlyout; \ No newline at end of file diff --git a/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/index.tsx b/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/index.tsx new file mode 100644 index 00000000..cca0078b --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/index.tsx @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import AnywhereParentFlyout from './AnywhereParentFlyout'; + +export default AnywhereParentFlyout; \ No newline at end of file diff --git a/public/components/FeatureAnywhereContextMenu/DocumentationTitle/containers/DocumentationTitle.tsx b/public/components/FeatureAnywhereContextMenu/DocumentationTitle/containers/DocumentationTitle.tsx new file mode 100644 index 00000000..22d2ac3c --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/DocumentationTitle/containers/DocumentationTitle.tsx @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiIcon, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; + +const DocumentationTitle = () => ( + + + + {i18n.translate( + 'dashboard.actions.adMenuItem.documentation.displayName', + { + defaultMessage: 'Documentation', + } + )} + + + + + + +); + +export default DocumentationTitle; \ No newline at end of file diff --git a/public/components/FeatureAnywhereContextMenu/DocumentationTitle/index.tsx b/public/components/FeatureAnywhereContextMenu/DocumentationTitle/index.tsx new file mode 100644 index 00000000..03b2fb80 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/DocumentationTitle/index.tsx @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import DocumentationTitle from './containers/DocumentationTitle'; + +export default DocumentationTitle; \ No newline at end of file diff --git a/public/plugin.ts b/public/plugin.ts index 2f55a028..36e15cc3 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -14,58 +14,73 @@ import { CoreSetup, CoreStart, Plugin, - PluginInitializerContext, } from '../../../src/core/public'; -import { - AnomalyDetectionOpenSearchDashboardsPluginSetup, - AnomalyDetectionOpenSearchDashboardsPluginStart, -} from '.'; +import { CONTEXT_MENU_TRIGGER, EmbeddableSetup, EmbeddableStart } from '../../../src/plugins/embeddable/public'; +import { ACTION_AD } from './action/ad_dashboard_action'; +import { PLUGIN_NAME } from './utils/constants'; +import { getActions } from './utils/contextMenu/getActions'; import { overlayAnomaliesFunction } from './expressions/overlay_anomalies'; -import { setClient } from './services'; +import { setClient, setEmbeddable, setOverlays } from './services'; +import { AnomalyDetectionOpenSearchDashboardsPluginStart } from 'public'; +import { createStartServicesGetter } from '../../../src/plugins/opensearch_dashboards_utils/public'; -export class AnomalyDetectionOpenSearchDashboardsPlugin - implements - Plugin< - AnomalyDetectionOpenSearchDashboardsPluginSetup, - AnomalyDetectionOpenSearchDashboardsPluginStart - > -{ - constructor(private readonly initializerContext: PluginInitializerContext) { - // can retrieve config from initializerContext +declare module '../../../src/plugins/ui_actions/public' { + export interface ActionContextMapping { + [ACTION_AD]: {}; } +} - public setup( - core: CoreSetup, - plugins - ): AnomalyDetectionOpenSearchDashboardsPluginSetup { - core.application.register({ - id: 'anomaly-detection-dashboards', - title: 'Anomaly Detection', - category: { - id: 'opensearch', - label: 'OpenSearch Plugins', - order: 2000, - }, - order: 5000, - mount: async (params: AppMountParameters) => { - const { renderApp } = await import('./anomaly_detection_app'); - const [coreStart, depsStart] = await core.getStartServices(); - return renderApp(coreStart, params); - }, - }); +export interface AnomalyDetectionSetupDeps { + embeddable: EmbeddableSetup; +} - // Set the HTTP client so it can be pulled into expression fns to make - // direct server-side calls - setClient(core.http); +export interface AnomalyDetectionStartDeps { + embeddable: EmbeddableStart; +} - // registers the expression function used to render anomalies on an Augmented Visualization - plugins.expressions.registerFunction(overlayAnomaliesFunction); - return {}; - } +export class AnomalyDetectionOpenSearchDashboardsPlugin implements + Plugin { + + public setup(core: CoreSetup, plugins: any) { + core.application.register({ + id: PLUGIN_NAME, + title: 'Anomaly Detection', + category: { + id: 'opensearch', + label: 'OpenSearch Plugins', + order: 2000, + }, + order: 5000, + mount: async (params: AppMountParameters) => { + const { renderApp } = await import('./anomaly_detection_app'); + const [coreStart] = await core.getStartServices(); + return renderApp(coreStart, params); + }, + }); - public start( - core: CoreStart - ): AnomalyDetectionOpenSearchDashboardsPluginStart { - return {}; - } -} + // Set the HTTP client so it can be pulled into expression fns to make + // direct server-side calls + setClient(core.http); + + // Create context menu actions. Pass core, to access service for flyouts. + const actions = getActions(); + + // Add actions to uiActions + actions.forEach((action) => { + plugins.uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, action); + }); + + // registers the expression function used to render anomalies on an Augmented Visualization + plugins.expressions.registerFunction(overlayAnomaliesFunction); + return {}; + } + + public start( + core: CoreStart, + {embeddable }: AnomalyDetectionStartDeps + ): AnomalyDetectionOpenSearchDashboardsPluginStart { + setEmbeddable(embeddable); + setOverlays(core.overlays); + return {}; + } +} \ No newline at end of file diff --git a/public/services.ts b/public/services.ts index d9161693..3857a95f 100644 --- a/public/services.ts +++ b/public/services.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CoreStart } from '../../../src/core/public'; +import { CoreStart, OverlayStart } from '../../../src/core/public'; +import { EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { createGetterSetter } from '../../../src/plugins/opensearch_dashboards_utils/public'; import { SavedObjectLoader } from '../../../src/plugins/saved_objects/public'; @@ -12,3 +13,9 @@ export const [getSavedFeatureAnywhereLoader, setSavedFeatureAnywhereLoader] = export const [getClient, setClient] = createGetterSetter('http'); + +export const [getEmbeddable, setEmbeddable] = + createGetterSetter('Embeddable'); + +export const [getOverlays, setOverlays] = + createGetterSetter('Overlays'); diff --git a/public/utils/constants.ts b/public/utils/constants.ts index 23354742..099e6a7e 100644 --- a/public/utils/constants.ts +++ b/public/utils/constants.ts @@ -53,6 +53,8 @@ export const ANOMALY_RESULT_INDEX = '.opendistro-anomaly-results'; export const BASE_DOCS_LINK = 'https://opensearch.org/docs/monitoring-plugins'; +export const AD_DOCS_LINK = 'https://opensearch.org/docs/latest/observing-your-data/ad/index/'; + export const MAX_DETECTORS = 1000; export const MAX_ANOMALIES = 10000; @@ -87,3 +89,5 @@ export enum MISSING_FEATURE_DATA_SEVERITY { } export const SPACE_STR = ' '; + +export const APM_TRACE = 'apmTrace'; diff --git a/public/utils/contextMenu/getActions.tsx b/public/utils/contextMenu/getActions.tsx new file mode 100644 index 00000000..4dcb05f6 --- /dev/null +++ b/public/utils/contextMenu/getActions.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { i18n } from '@osd/i18n'; +import { EuiIconType } from '@elastic/eui'; +import { toMountPoint } from '../../../../../src/plugins/opensearch_dashboards_react/public'; +import { Action } from '../../../../../src/plugins/ui_actions/public'; +import { createADAction } from '../../action/ad_dashboard_action'; +import AnywhereParentFlyout from '../../components/FeatureAnywhereContextMenu/AnywhereParentFlyout'; +import { Provider } from 'react-redux'; +import configureStore from '../../redux/configureStore'; +import DocumentationTitle from '../../components/FeatureAnywhereContextMenu/DocumentationTitle/containers/DocumentationTitle'; +import { AD_DOCS_LINK, APM_TRACE } from '../constants'; +import { getClient, getOverlays } from '../../../public/services'; + +// This is used to create all actions in the same context menu +const grouping: Action['grouping'] = [ + { + id: 'ad-dashboard-context-menu', + getDisplayName: () => 'Anomaly Detector', + getIconType: () => APM_TRACE, + }, +]; + +export const getActions = () => { + const getOnClick = + (startingFlyout) => + async ({ embeddable }) => { + const overlayService = getOverlays(); + const openFlyout = overlayService.openFlyout; + const store = configureStore(getClient()); + const overlay = openFlyout( + toMountPoint( + + overlay.close()} + /> + + ), + { size: 'm', className: 'context-menu__flyout' } + ); + }; + + return [ + { + grouping, + id: 'createAnomalyDetector', + title: i18n.translate( + 'dashboard.actions.adMenuItem.createAnomalyDetector.displayName', + { + defaultMessage: 'Create anomaly detector', + } + ), + icon: 'plusInCircle' as EuiIconType, + order: 100, + onClick: getOnClick('create'), + }, + { + grouping, + id: 'associatedAnomalyDetector', + title: i18n.translate( + 'dashboard.actions.adMenuItem.associatedAnomalyDetector.displayName', + { + defaultMessage: 'Associated anomaly detector', + } + ), + icon: 'gear' as EuiIconType, + order: 99, + onClick: getOnClick('associated'), + }, + { + id: 'documentationAnomalyDetector', + title: , + icon: 'documentation' as EuiIconType, + order: 98, + onClick: () => { + window.open( + AD_DOCS_LINK, + '_blank' + ); + }, + }, + ].map((options) => createADAction({ ...options, grouping })); +}; \ No newline at end of file From 172f947917821c5e7fef25e0588a96f354030e82 Mon Sep 17 00:00:00 2001 From: Amit Galitzky Date: Mon, 22 May 2023 14:59:12 -0700 Subject: [PATCH 21/43] Associated detectors flyout through UI actions (#449) * working js manage detectors Signed-off-by: Amit Galitzky * adding associated detectors page Signed-off-by: Amit Galitzky * adding unlink modal confirmation Signed-off-by: Amit Galitzky * prettier formating and merge conflicts Signed-off-by: Amit Galitzky * add unlinking capability Signed-off-by: Amit Galitzky * adding message for no search results Signed-off-by: Amit Galitzky * clean up files Signed-off-by: Amit Galitzky * more cleanup Signed-off-by: Amit Galitzky * making changes based on new upper container Signed-off-by: Amit Galitzky * ran prettier Signed-off-by: Amit Galitzky * fix notification and clean up associated detectors Signed-off-by: Amit Galitzky * addressing comments Signed-off-by: Amit Galitzky * renaming some files and adding index.ts Signed-off-by: Amit Galitzky * Added license to new files Signed-off-by: Amit Galitzky * clean up after rebase Signed-off-by: Amit Galitzky * addressed more comments Signed-off-by: Amit Galitzky * added notifications service as a getter-setter Signed-off-by: Amit Galitzky --------- Signed-off-by: Amit Galitzky --- public/action/ad_dashboard_action.tsx | 8 +- .../AnywhereParentFlyout.tsx | 14 +- .../AnywhereParentFlyout/index.tsx | 2 +- .../ConfirmUnlinkDetectorModal.tsx | 78 ++++ .../EmptyAssociatedDetectorMessage.tsx | 32 ++ .../AssociatedDetectors/components/index.ts | 7 + .../containers/AssociatedDetectors.tsx | 344 ++++++++++++++++++ .../AssociatedDetectors/index.ts | 6 + .../AssociatedDetectors/styles.scss | 19 + .../AssociatedDetectors/utils/constants.tsx | 8 + .../AssociatedDetectors/utils/helpers.tsx | 75 ++++ .../containers/DocumentationTitle.tsx | 2 +- .../DocumentationTitle/index.tsx | 2 +- public/plugin.ts | 101 ++--- public/services.ts | 13 +- public/utils/constants.ts | 3 +- public/utils/contextMenu/getActions.tsx | 20 +- 17 files changed, 663 insertions(+), 71 deletions(-) create mode 100644 public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/ConfirmUnlinkDetectorModal/ConfirmUnlinkDetectorModal.tsx create mode 100644 public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/EmptyAssociatedDetectorMessage/EmptyAssociatedDetectorMessage.tsx create mode 100644 public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/index.ts create mode 100644 public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx create mode 100644 public/components/FeatureAnywhereContextMenu/AssociatedDetectors/index.ts create mode 100644 public/components/FeatureAnywhereContextMenu/AssociatedDetectors/styles.scss create mode 100644 public/components/FeatureAnywhereContextMenu/AssociatedDetectors/utils/constants.tsx create mode 100644 public/components/FeatureAnywhereContextMenu/AssociatedDetectors/utils/helpers.tsx diff --git a/public/action/ad_dashboard_action.tsx b/public/action/ad_dashboard_action.tsx index 0be356ed..a845351a 100644 --- a/public/action/ad_dashboard_action.tsx +++ b/public/action/ad_dashboard_action.tsx @@ -1,6 +1,6 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 */ import { IEmbeddable } from '../../../../src/plugins/dashboard/public/embeddable_plugin'; import { @@ -75,4 +75,4 @@ export const createADAction = ({ onClick({ embeddable }); }, - }); \ No newline at end of file + }); diff --git a/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/AnywhereParentFlyout.tsx b/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/AnywhereParentFlyout.tsx index 2a54a169..70c27e68 100644 --- a/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/AnywhereParentFlyout.tsx +++ b/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/AnywhereParentFlyout.tsx @@ -1,10 +1,10 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 */ import React, { useState } from 'react'; import { get } from 'lodash'; -import AddAnomalyDetector from '../CreateAnomalyDetector'; +import AssociatedDetectors from '../AssociatedDetectors/containers/AssociatedDetectors'; import { getEmbeddable } from '../../../../public/services'; const AnywhereParentFlyout = ({ startingFlyout, ...props }) => { @@ -17,7 +17,7 @@ const AnywhereParentFlyout = ({ startingFlyout, ...props }) => { const [selectedDetectorId, setSelectedDetectorId] = useState(); const AnywhereFlyout = { - create: AddAnomalyDetector, + associated: AssociatedDetectors, }[mode]; return ( @@ -30,8 +30,8 @@ const AnywhereParentFlyout = ({ startingFlyout, ...props }) => { selectedDetectorId, setSelectedDetectorId, }} - /> + /> ); }; -export default AnywhereParentFlyout; \ No newline at end of file +export default AnywhereParentFlyout; diff --git a/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/index.tsx b/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/index.tsx index cca0078b..591d4b6d 100644 --- a/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/index.tsx +++ b/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/index.tsx @@ -5,4 +5,4 @@ import AnywhereParentFlyout from './AnywhereParentFlyout'; -export default AnywhereParentFlyout; \ No newline at end of file +export default AnywhereParentFlyout; diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/ConfirmUnlinkDetectorModal/ConfirmUnlinkDetectorModal.tsx b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/ConfirmUnlinkDetectorModal/ConfirmUnlinkDetectorModal.tsx new file mode 100644 index 00000000..25687ed5 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/ConfirmUnlinkDetectorModal/ConfirmUnlinkDetectorModal.tsx @@ -0,0 +1,78 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { + EuiText, + EuiOverlayMask, + EuiButton, + EuiButtonEmpty, + EuiModal, + EuiModalHeader, + EuiModalFooter, + EuiModalBody, + EuiModalHeaderTitle, +} from '@elastic/eui'; +import { DetectorListItem } from '../../../../../models/interfaces'; +import { EuiSpacer } from '@elastic/eui'; + +interface ConfirmUnlinkDetectorModalProps { + detector: DetectorListItem; + onUnlinkDetector(): void; + onHide(): void; + onConfirm(): void; + isListLoading: boolean; +} + +export const ConfirmUnlinkDetectorModal = ( + props: ConfirmUnlinkDetectorModalProps +) => { + const [isModalLoading, setIsModalLoading] = useState(false); + const isLoading = isModalLoading || props.isListLoading; + return ( + + + + {'Remove association?'} + + + + Removing association unlinks {props.detector.name} detector from the + visualization but does not delete it. The detector association can + be restored. + + + + + {isLoading ? null : ( + + Cancel + + )} + { + setIsModalLoading(true); + props.onUnlinkDetector(); + props.onConfirm(); + }} + > + {'Remove association'} + + + + + ); +}; diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/EmptyAssociatedDetectorMessage/EmptyAssociatedDetectorMessage.tsx b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/EmptyAssociatedDetectorMessage/EmptyAssociatedDetectorMessage.tsx new file mode 100644 index 00000000..d005e087 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/EmptyAssociatedDetectorMessage/EmptyAssociatedDetectorMessage.tsx @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiEmptyPrompt, EuiText } from '@elastic/eui'; +import React from 'react'; + +const FILTER_TEXT = 'There are no detectors matching your search'; + +interface EmptyDetectorProps { + isFilterApplied: boolean; + embeddableTitle: string; +} + +export const EmptyAssociatedDetectorMessage = (props: EmptyDetectorProps) => ( + No anomaly detectors to display} + titleSize="s" + data-test-subj="emptyAssociatedDetectorFlyoutMessage" + style={{ maxWidth: '45em' }} + body={ + +

+ {props.isFilterApplied + ? FILTER_TEXT + : `There are no anomaly detectors associated with ${props.embeddableTitle} visualization.`} +

+
+ } + /> +); diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/index.ts b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/index.ts new file mode 100644 index 00000000..92d619eb --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { ConfirmUnlinkDetectorModal } from './ConfirmUnlinkDetectorModal/ConfirmUnlinkDetectorModal'; +export { EmptyAssociatedDetectorMessage } from './EmptyAssociatedDetectorMessage/EmptyAssociatedDetectorMessage'; diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx new file mode 100644 index 00000000..c0b4f64f --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx @@ -0,0 +1,344 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useMemo, useEffect, useState } from 'react'; +import { + EuiFlyoutHeader, + EuiTitle, + EuiSpacer, + EuiInMemoryTable, + EuiFlyoutBody, + EuiButton, + EuiFlyout, + EuiFlexItem, + EuiFlexGroup, +} from '@elastic/eui'; +import { get, isEmpty } from 'lodash'; +import '../styles.scss'; +import { getColumns } from '../utils/helpers'; +import { useDispatch, useSelector } from 'react-redux'; +import { AppState } from '../../../../redux/reducers'; +import { DetectorListItem } from '../../../../models/interfaces'; +import { + getSavedFeatureAnywhereLoader, + getNotifications, +} from '../../../../services'; +import { + GET_ALL_DETECTORS_QUERY_PARAMS, + SINGLE_DETECTOR_NOT_FOUND_MSG, +} from '../../../../pages/utils/constants'; +import { getDetectorList } from '../../../../redux/reducers/ad'; +import { + prettifyErrorMessage, + NO_PERMISSIONS_KEY_WORD, +} from '../../../../../server/utils/helpers'; +import { SavedObjectLoader } from '../../../../../../../src/plugins/saved_objects/public'; +import { + EmptyAssociatedDetectorMessage, + ConfirmUnlinkDetectorModal, +} from '../components'; +import { ISavedAugmentVis } from '../../../../../../../src/plugins/vis_augmenter/public'; +import { ASSOCIATED_DETECTOR_ACTION } from '../utils/constants'; + +interface ConfirmModalState { + isOpen: boolean; + action: ASSOCIATED_DETECTOR_ACTION; + isListLoading: boolean; + isRequestingToClose: boolean; + affectedDetector: DetectorListItem; +} + +function AssociatedDetectors({ embeddable, closeFlyout, setMode }) { + const dispatch = useDispatch(); + const allDetectors = useSelector((state: AppState) => state.ad.detectorList); + const isRequestingFromES = useSelector( + (state: AppState) => state.ad.requesting + ); + const [isLoadingFinalDetectors, setIsLoadingFinalDetectors] = + useState(true); + const isLoading = isRequestingFromES || isLoadingFinalDetectors; + const errorGettingDetectors = useSelector( + (state: AppState) => state.ad.errorMessage + ); + const embeddableTitle = embeddable.getTitle(); + const [selectedDetectors, setSelectedDetectors] = useState( + [] as DetectorListItem[] + ); + + const [detectorToUnlink, setDetectorToUnlink] = useState( + {} as DetectorListItem + ); + const [confirmModalState, setConfirmModalState] = useState( + { + isOpen: false, + //@ts-ignore + action: null, + isListLoading: false, + isRequestingToClose: false, + affectedDetector: {} as DetectorListItem, + } + ); + + // Establish savedObjectLoader for all operations on vis_augment saved objects + const savedObjectLoader: SavedObjectLoader = getSavedFeatureAnywhereLoader(); + + const notifications = getNotifications(); + + useEffect(() => { + if ( + errorGettingDetectors && + !errorGettingDetectors.includes(SINGLE_DETECTOR_NOT_FOUND_MSG) + ) { + console.error(errorGettingDetectors); + notifications.toasts.addDanger( + typeof errorGettingDetectors === 'string' && + errorGettingDetectors.includes(NO_PERMISSIONS_KEY_WORD) + ? prettifyErrorMessage(errorGettingDetectors) + : 'Unable to get all detectors' + ); + setIsLoadingFinalDetectors(false); + } + }, [errorGettingDetectors]); + + // Update modal state if user decides to close modal + useEffect(() => { + if (confirmModalState.isRequestingToClose) { + if (isLoading) { + setConfirmModalState({ + ...confirmModalState, + isListLoading: true, + }); + } else { + setConfirmModalState({ + ...confirmModalState, + isOpen: false, + isListLoading: false, + isRequestingToClose: false, + }); + } + } + }, [confirmModalState.isRequestingToClose, isLoading]); + + useEffect(() => { + getDetectors(); + }, []); + + // Handles all changes in the assoicated detectors such as unlinking or new detectors associated + useEffect(() => { + // Gets all augmented saved objects + savedObjectLoader + .findAll() + .then((resp: any) => { + if (resp != undefined) { + const savedAugmentObjectsArr: ISavedAugmentVis[] = get( + resp, + 'hits', + [] + ); + const curSelectedDetectors = getAssociatedDetectors( + Object.values(allDetectors), + savedAugmentObjectsArr + ); + setSelectedDetectors(curSelectedDetectors); + setIsLoadingFinalDetectors(false); + } + }) + .catch((error) => { + notifications.toasts.addDanger( + prettifyErrorMessage(`Unable to fetch associated detectors: ${error}`) + ); + }); + }, [allDetectors]); + + // cross checks all the detectors that exist with all the savedAugment Objects to only display ones + // that are associated to the current visualization + const getAssociatedDetectors = ( + detectors: DetectorListItem[], + savedAugmentObjects: ISavedAugmentVis[] + ) => { + // Filter all savedAugmentObjects that aren't linked to the specific visualization + const savedAugmentForThisVisualization: ISavedAugmentVis[] = + savedAugmentObjects.filter( + (savedObj) => get(savedObj, 'visId', '') === embeddable.vis.id + ); + + // Map all detector IDs for all the found augmented vis objects + const savedAugmentDetectorsSet = new Set( + savedAugmentForThisVisualization.map((savedObject) => + get(savedObject, 'pluginResource.id', '') + ) + ); + + // filter out any detectors that aren't on the set of detectors IDs from the augmented vis objects. + const detectorsToDisplay = detectors.filter((detector) => + savedAugmentDetectorsSet.has(detector.id) + ); + return detectorsToDisplay; + }; + + const onUnlinkDetector = async () => { + setIsLoadingFinalDetectors(true); + await savedObjectLoader.findAll().then(async (resp: any) => { + if (resp != undefined) { + // gets all the saved object for this visualization + const savedAugmentForThisVisualization: ISavedAugmentVis[] = get( + resp, + 'hits', + [] as ISavedAugmentVis[] + ).filter( + (savedObj: ISavedAugmentVis[]) => + get(savedObj, 'visId', '') === embeddable.vis.id + ); + + // find saved augment object matching detector we want to unlink + // There should only be one detector and vis pairing + const savedAugmentToUnlink = savedAugmentForThisVisualization.find( + (savedObject) => + get(savedObject, 'pluginResource.id', '') === detectorToUnlink.id + ); + await savedObjectLoader + .delete(get(savedAugmentToUnlink, 'id', '')) + .then(async (resp: any) => { + notifications.toasts.addSuccess({ + title: `Association removed between the ${detectorToUnlink.name} + and the ${embeddableTitle} visualization`, + text: "The detector's anomalies do not appear on the visualization. Refresh your dashboard to update the visualization", + }); + }) + .catch((error) => { + notifications.toasts.addDanger( + prettifyErrorMessage( + `Error unlinking selected detector: ${error}` + ) + ); + }) + .finally(() => { + getDetectors(); + }); + } + }); + }; + + const handleHideModal = () => { + setConfirmModalState({ + ...confirmModalState, + isOpen: false, + }); + }; + + const handleConfirmModal = () => { + setConfirmModalState({ + ...confirmModalState, + isRequestingToClose: true, + }); + }; + + const getDetectors = async () => { + dispatch(getDetectorList(GET_ALL_DETECTORS_QUERY_PARAMS)); + }; + + // TODO: this part is incomplete because it is pending on a different PR that will have all the associate existing changes + const openAssociateDetectorFlyout = async () => { + console.log('inside create anomaly detector'); + }; + + const handleUnlinkDetectorAction = (detector: DetectorListItem) => { + setDetectorToUnlink(detector); + setConfirmModalState({ + isOpen: true, + action: ASSOCIATED_DETECTOR_ACTION.UNLINK, + isListLoading: false, + isRequestingToClose: false, + affectedDetector: detector, + }); + }; + + const columns = useMemo( + () => getColumns({ handleUnlinkDetectorAction }), + [handleUnlinkDetectorAction] + ); + + const renderEmptyMessage = () => { + if (isLoading) { + return 'Loading detectors...'; + } else if (!isEmpty(selectedDetectors)) { + return ( + + ); + } else { + return ( + + ); + } + }; + + const tableProps = { + items: selectedDetectors, + columns, + search: { + box: { + disabled: selectedDetectors.length === 0, + incremental: true, + schema: true, + }, + }, + hasActions: true, + pagination: true, + sorting: true, + message: renderEmptyMessage(), + }; + return ( +
+ + + +

+ Associated anomaly detectors +

+
+
+ + {confirmModalState.isOpen ? ( + + ) : null} + + + +

{embeddableTitle}

+
+
+ + { + openAssociateDetectorFlyout(); + }} + > + Associate a detector + + +
+ + +
+
+
+ ); +} + +export default AssociatedDetectors; diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/index.ts b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/index.ts new file mode 100644 index 00000000..39483649 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { AssociatedDetectors } from './containers/AssociatedDetectors'; diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/styles.scss b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/styles.scss new file mode 100644 index 00000000..6598f00e --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/styles.scss @@ -0,0 +1,19 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +.associated-detectors { + height: 100%; + display: flex; + flex-direction: column; + + .euiFlyoutBody__overflowContent { + height: 100%; + padding-bottom: 0; + } + + &__flex-group { + height: 100%; + } +} diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/utils/constants.tsx b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/utils/constants.tsx new file mode 100644 index 00000000..37236349 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/utils/constants.tsx @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export enum ASSOCIATED_DETECTOR_ACTION { + UNLINK, +} diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/utils/helpers.tsx b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/utils/helpers.tsx new file mode 100644 index 00000000..c6125537 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/utils/helpers.tsx @@ -0,0 +1,75 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiBasicTableColumn, EuiHealth, EuiLink } from '@elastic/eui'; +import { DETECTOR_STATE } from 'server/utils/constants'; +import { stateToColorMap } from '../../../../pages/utils/constants'; +import { PLUGIN_NAME } from '../../../../utils/constants'; +import { Detector } from '../../../../models/interfaces'; + +export const renderState = (state: DETECTOR_STATE) => { + return ( + //@ts-ignore + {state} + ); +}; + +export const getColumns = ({ handleUnlinkDetectorAction }) => + [ + { + field: 'name', + name: 'Detector', + sortable: true, + truncateText: true, + width: '30%', + align: 'left', + render: (name: string, detector: Detector) => ( + + {name} + + ), + }, + { + field: 'curState', + name: 'Real-time state', + sortable: true, + align: 'left', + width: '30%', + truncateText: true, + render: renderState, + }, + { + field: 'totalAnomalies', + name: 'Anomalies/24hr', + sortable: true, + dataType: 'number', + align: 'left', + truncateText: true, + width: '30%', + }, + { + name: 'Actions', + align: 'left', + truncateText: true, + width: '10%', + actions: [ + { + type: 'icon', + name: 'Remove association', + description: 'Remove association', + icon: 'unlink', + onClick: handleUnlinkDetectorAction, + }, + ], + }, + ] as EuiBasicTableColumn[]; + +export const search = { + box: { + incremental: true, + schema: true, + }, +}; diff --git a/public/components/FeatureAnywhereContextMenu/DocumentationTitle/containers/DocumentationTitle.tsx b/public/components/FeatureAnywhereContextMenu/DocumentationTitle/containers/DocumentationTitle.tsx index 22d2ac3c..3ee81e65 100644 --- a/public/components/FeatureAnywhereContextMenu/DocumentationTitle/containers/DocumentationTitle.tsx +++ b/public/components/FeatureAnywhereContextMenu/DocumentationTitle/containers/DocumentationTitle.tsx @@ -25,4 +25,4 @@ const DocumentationTitle = () => ( ); -export default DocumentationTitle; \ No newline at end of file +export default DocumentationTitle; diff --git a/public/components/FeatureAnywhereContextMenu/DocumentationTitle/index.tsx b/public/components/FeatureAnywhereContextMenu/DocumentationTitle/index.tsx index 03b2fb80..e9f1bd89 100644 --- a/public/components/FeatureAnywhereContextMenu/DocumentationTitle/index.tsx +++ b/public/components/FeatureAnywhereContextMenu/DocumentationTitle/index.tsx @@ -5,4 +5,4 @@ import DocumentationTitle from './containers/DocumentationTitle'; -export default DocumentationTitle; \ No newline at end of file +export default DocumentationTitle; diff --git a/public/plugin.ts b/public/plugin.ts index 36e15cc3..f3f4669e 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -15,14 +15,24 @@ import { CoreStart, Plugin, } from '../../../src/core/public'; -import { CONTEXT_MENU_TRIGGER, EmbeddableSetup, EmbeddableStart } from '../../../src/plugins/embeddable/public'; +import { + CONTEXT_MENU_TRIGGER, + EmbeddableSetup, + EmbeddableStart, +} from '../../../src/plugins/embeddable/public'; import { ACTION_AD } from './action/ad_dashboard_action'; import { PLUGIN_NAME } from './utils/constants'; import { getActions } from './utils/contextMenu/getActions'; import { overlayAnomaliesFunction } from './expressions/overlay_anomalies'; -import { setClient, setEmbeddable, setOverlays } from './services'; +import { + setClient, + setEmbeddable, + setNotifications, + setOverlays, + setSavedFeatureAnywhereLoader, +} from './services'; import { AnomalyDetectionOpenSearchDashboardsPluginStart } from 'public'; -import { createStartServicesGetter } from '../../../src/plugins/opensearch_dashboards_utils/public'; +import { VisAugmenterStart } from '../../../src/plugins/vis_augmenter/public'; declare module '../../../src/plugins/ui_actions/public' { export interface ActionContextMapping { @@ -36,51 +46,54 @@ export interface AnomalyDetectionSetupDeps { export interface AnomalyDetectionStartDeps { embeddable: EmbeddableStart; + visAugmenter: VisAugmenterStart; } -export class AnomalyDetectionOpenSearchDashboardsPlugin implements - Plugin { - - public setup(core: CoreSetup, plugins: any) { - core.application.register({ - id: PLUGIN_NAME, - title: 'Anomaly Detection', - category: { - id: 'opensearch', - label: 'OpenSearch Plugins', - order: 2000, - }, - order: 5000, - mount: async (params: AppMountParameters) => { - const { renderApp } = await import('./anomaly_detection_app'); - const [coreStart] = await core.getStartServices(); - return renderApp(coreStart, params); - }, - }); +export class AnomalyDetectionOpenSearchDashboardsPlugin + implements Plugin +{ + public setup(core: CoreSetup, plugins: any) { + core.application.register({ + id: PLUGIN_NAME, + title: 'Anomaly Detection', + category: { + id: 'opensearch', + label: 'OpenSearch Plugins', + order: 2000, + }, + order: 5000, + mount: async (params: AppMountParameters) => { + const { renderApp } = await import('./anomaly_detection_app'); + const [coreStart] = await core.getStartServices(); + return renderApp(coreStart, params); + }, + }); - // Set the HTTP client so it can be pulled into expression fns to make - // direct server-side calls - setClient(core.http); + // Set the HTTP client so it can be pulled into expression fns to make + // direct server-side calls + setClient(core.http); - // Create context menu actions. Pass core, to access service for flyouts. - const actions = getActions(); + // Create context menu actions. Pass core, to access service for flyouts. + const actions = getActions(); - // Add actions to uiActions - actions.forEach((action) => { - plugins.uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, action); - }); + // Add actions to uiActions + actions.forEach((action) => { + plugins.uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, action); + }); - // registers the expression function used to render anomalies on an Augmented Visualization - plugins.expressions.registerFunction(overlayAnomaliesFunction); - return {}; - } + // registers the expression function used to render anomalies on an Augmented Visualization + plugins.expressions.registerFunction(overlayAnomaliesFunction); + return {}; + } - public start( - core: CoreStart, - {embeddable }: AnomalyDetectionStartDeps - ): AnomalyDetectionOpenSearchDashboardsPluginStart { - setEmbeddable(embeddable); - setOverlays(core.overlays); - return {}; - } -} \ No newline at end of file + public start( + core: CoreStart, + { embeddable, visAugmenter }: AnomalyDetectionStartDeps + ): AnomalyDetectionOpenSearchDashboardsPluginStart { + setEmbeddable(embeddable); + setOverlays(core.overlays); + setSavedFeatureAnywhereLoader(visAugmenter.savedAugmentVisLoader); + setNotifications(core.notifications); + return {}; + } +} diff --git a/public/services.ts b/public/services.ts index 3857a95f..1908f443 100644 --- a/public/services.ts +++ b/public/services.ts @@ -3,7 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CoreStart, OverlayStart } from '../../../src/core/public'; +import { + CoreStart, + NotificationsStart, + OverlayStart, +} from '../../../src/core/public'; import { EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { createGetterSetter } from '../../../src/plugins/opensearch_dashboards_utils/public'; import { SavedObjectLoader } from '../../../src/plugins/saved_objects/public'; @@ -14,8 +18,11 @@ export const [getSavedFeatureAnywhereLoader, setSavedFeatureAnywhereLoader] = export const [getClient, setClient] = createGetterSetter('http'); -export const [getEmbeddable, setEmbeddable] = +export const [getEmbeddable, setEmbeddable] = createGetterSetter('Embeddable'); -export const [getOverlays, setOverlays] = +export const [getOverlays, setOverlays] = createGetterSetter('Overlays'); + +export const [getNotifications, setNotifications] = + createGetterSetter('Notifications'); diff --git a/public/utils/constants.ts b/public/utils/constants.ts index 099e6a7e..17a8b86a 100644 --- a/public/utils/constants.ts +++ b/public/utils/constants.ts @@ -53,7 +53,8 @@ export const ANOMALY_RESULT_INDEX = '.opendistro-anomaly-results'; export const BASE_DOCS_LINK = 'https://opensearch.org/docs/monitoring-plugins'; -export const AD_DOCS_LINK = 'https://opensearch.org/docs/latest/observing-your-data/ad/index/'; +export const AD_DOCS_LINK = + 'https://opensearch.org/docs/latest/observing-your-data/ad/index/'; export const MAX_DETECTORS = 1000; diff --git a/public/utils/contextMenu/getActions.tsx b/public/utils/contextMenu/getActions.tsx index 4dcb05f6..0c1302e4 100644 --- a/public/utils/contextMenu/getActions.tsx +++ b/public/utils/contextMenu/getActions.tsx @@ -1,3 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + import React from 'react'; import { i18n } from '@osd/i18n'; import { EuiIconType } from '@elastic/eui'; @@ -31,10 +36,10 @@ export const getActions = () => { toMountPoint( overlay.close()} - /> + startingFlyout={startingFlyout} + embeddable={embeddable} + closeFlyout={() => overlay.close()} + /> ), { size: 'm', className: 'context-menu__flyout' } @@ -74,11 +79,8 @@ export const getActions = () => { icon: 'documentation' as EuiIconType, order: 98, onClick: () => { - window.open( - AD_DOCS_LINK, - '_blank' - ); + window.open(AD_DOCS_LINK, '_blank'); }, }, ].map((options) => createADAction({ ...options, grouping })); -}; \ No newline at end of file +}; From cb349ce9d2352769c388851ed8ad7955da707094 Mon Sep 17 00:00:00 2001 From: Jackie Han Date: Wed, 24 May 2023 15:04:15 -0700 Subject: [PATCH 22/43] Feature anywhere create detector flyout page (#487) * Add create detector flyout page Signed-off-by: Jackie Han * cleanup Signed-off-by: Jackie Han * run prettier Signed-off-by: Jackie Han * remove enzyme usage Signed-off-by: Jackie Han * reuse existing helper function when creating detector Signed-off-by: Jackie Han * cleanup Signed-off-by: Jackie Han * rebase with associated detector change Signed-off-by: Jackie Han * move helper functions to helper file Signed-off-by: Jackie Han * use VisLayerTypes for VisLayerExpressionFn Signed-off-by: Jackie Han * address comments Signed-off-by: Jackie Han * use OVERLAY_ANOMALIES constant Signed-off-by: Jackie Han --------- Signed-off-by: Jackie Han Signed-off-by: Jackie Han --- .../AnywhereParentFlyout.tsx | 2 + .../AddAnomalyDetector.tsx | 718 ++++++++++++++++++ .../CreateAnomalyDetector/helpers.tsx | 61 ++ .../CreateAnomalyDetector/index.tsx | 8 + .../CreateAnomalyDetector/styles.scss | 58 ++ .../EnhancedAccordion/EnhancedAccordion.tsx | 87 +++ .../EnhancedAccordion/index.tsx | 8 + .../EnhancedAccordion/styles.scss | 32 + .../MinimalAccordion/MinimalAccordion.tsx | 64 ++ .../MinimalAccordion/index.tsx | 8 + .../MinimalAccordion/styles.scss | 28 + public/expressions/constants.ts | 2 + public/expressions/overlay_anomalies.ts | 9 +- .../FeatureAccordion/FeatureAccordion.tsx | 26 +- public/plugin.ts | 7 +- public/utils/constants.ts | 3 + 16 files changed, 1110 insertions(+), 11 deletions(-) create mode 100644 public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx create mode 100644 public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/helpers.tsx create mode 100644 public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/index.tsx create mode 100644 public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/styles.scss create mode 100644 public/components/FeatureAnywhereContextMenu/EnhancedAccordion/EnhancedAccordion.tsx create mode 100644 public/components/FeatureAnywhereContextMenu/EnhancedAccordion/index.tsx create mode 100644 public/components/FeatureAnywhereContextMenu/EnhancedAccordion/styles.scss create mode 100644 public/components/FeatureAnywhereContextMenu/MinimalAccordion/MinimalAccordion.tsx create mode 100644 public/components/FeatureAnywhereContextMenu/MinimalAccordion/index.tsx create mode 100644 public/components/FeatureAnywhereContextMenu/MinimalAccordion/styles.scss diff --git a/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/AnywhereParentFlyout.tsx b/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/AnywhereParentFlyout.tsx index 70c27e68..d2e624ca 100644 --- a/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/AnywhereParentFlyout.tsx +++ b/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/AnywhereParentFlyout.tsx @@ -6,6 +6,7 @@ import React, { useState } from 'react'; import { get } from 'lodash'; import AssociatedDetectors from '../AssociatedDetectors/containers/AssociatedDetectors'; import { getEmbeddable } from '../../../../public/services'; +import AddAnomalyDetector from '../CreateAnomalyDetector/AddAnomalyDetector'; const AnywhereParentFlyout = ({ startingFlyout, ...props }) => { const embeddable = getEmbeddable().getEmbeddableFactory; @@ -17,6 +18,7 @@ const AnywhereParentFlyout = ({ startingFlyout, ...props }) => { const [selectedDetectorId, setSelectedDetectorId] = useState(); const AnywhereFlyout = { + create: AddAnomalyDetector, associated: AssociatedDetectors, }[mode]; diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx new file mode 100644 index 00000000..ead4014b --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx @@ -0,0 +1,718 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useEffect, Fragment } from 'react'; +import { + EuiFlyoutHeader, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiTitle, + EuiButton, + EuiFormFieldset, + EuiCheckableCard, + EuiSpacer, + EuiIcon, + EuiText, + EuiSwitch, + EuiFormRow, + EuiFieldText, + EuiCheckbox, + EuiFlexItem, + EuiFlexGroup, + EuiFieldNumber, + EuiCallOut, + EuiButtonEmpty, + EuiPanel, +} from '@elastic/eui'; +import './styles.scss'; +import { + createAugmentVisSavedObject, + ISavedAugmentVis, + ISavedPluginResource, + VisLayerExpressionFn, + VisLayerTypes, +} from '../../../../../../src/plugins/vis_augmenter/public'; +import { useDispatch } from 'react-redux'; +import { isEmpty, get } from 'lodash'; +import { + Field, + FieldArray, + FieldArrayRenderProps, + FieldProps, + Formik, +} from 'formik'; +import { + createDetector, + getDetectorCount, + matchDetector, + startDetector, +} from '../../../../public/redux/reducers/ad'; +import { EmbeddableRenderer } from '../../../../../../src/plugins/embeddable/public'; +import './styles.scss'; +import EnhancedAccordion from '../EnhancedAccordion'; +import MinimalAccordion from '../MinimalAccordion'; +import { DataFilterList } from '../../../../public/pages/DefineDetector/components/DataFilterList/DataFilterList'; +import { + getError, + getErrorMessage, + isInvalid, + validateDetectorName, + validateNonNegativeInteger, + validatePositiveInteger, +} from '../../../../public/utils/utils'; +import { + CUSTOM_AD_RESULT_INDEX_PREFIX, + MAX_DETECTORS, +} from '../../../../server/utils/constants'; +import { + focusOnFirstWrongFeature, + initialFeatureValue, +} from '../../../../public/pages/ConfigureModel/utils/helpers'; +import { + getIndices, + getMappings, +} from '../../../../public/redux/reducers/opensearch'; +import { formikToDetector } from '../../../../public/pages/ReviewAndCreate/utils/helpers'; +import { FormattedFormRow } from '../../../../public/components/FormattedFormRow/FormattedFormRow'; +import { FeatureAccordion } from '../../../../public/pages/ConfigureModel/components/FeatureAccordion'; +import { + AD_DOCS_LINK, + AD_HIGH_CARDINALITY_LINK, + MAX_FEATURE_NUM, +} from '../../../../public/utils/constants'; +import { getNotifications } from '../../../../public/services'; +import { prettifyErrorMessage } from '../../../../server/utils/helpers'; +import { + ORIGIN_PLUGIN_VIS_LAYER, + OVERLAY_ANOMALIES, + VIS_LAYER_PLUGIN_TYPE, +} from '../../../../public/expressions/constants'; +import { formikToDetectorName, visFeatureListToFormik } from './helpers'; + +function AddAnomalyDetector({ embeddable, closeFlyout, mode, setMode }) { + const dispatch = useDispatch(); + const [queryText, setQueryText] = useState(''); + useEffect(() => { + const getInitialIndices = async () => { + await dispatch(getIndices(queryText)); + }; + getInitialIndices(); + dispatch(getMappings(embeddable.vis.data.aggs.indexPattern.title)); + }, []); + + const [isShowVis, setIsShowVis] = useState(false); + const [accordionsOpen, setAccordionsOpen] = useState({ modelFeatures: true }); + const [detectorNameFromVis, setDetectorNameFromVis] = useState( + formikToDetectorName(embeddable.vis.title) + ); + const [intervalValue, setIntervalalue] = useState(10); + const [delayValue, setDelayValue] = useState(1); + const [enabled, setEnabled] = useState(false); + + const title = embeddable.getTitle(); + const onAccordionToggle = (key) => { + const newAccordionsOpen = { ...accordionsOpen }; + newAccordionsOpen[key] = !accordionsOpen[key]; + setAccordionsOpen(newAccordionsOpen); + }; + const onDetectorNameChange = (e, field) => { + field.onChange(e); + setDetectorNameFromVis(e.target.value); + }; + const onIntervalChange = (e, field) => { + field.onChange(e); + setIntervalalue(e.target.value); + }; + const onDelayChange = (e, field) => { + field.onChange(e); + setDelayValue(e.target.value); + }; + const aggList = embeddable.vis.data.aggs.aggs.filter( + (feature) => feature.schema == 'metric' + ); + const featureList = aggList.filter( + (feature, index) => + index < + (aggList.length < MAX_FEATURE_NUM ? aggList.length : MAX_FEATURE_NUM) + ); + + const notifications = getNotifications(); + const handleValidationAndSubmit = (formikProps) => { + if (!isEmpty(formikProps.errors)) { + focusOnFirstWrongFeature(formikProps.errors, formikProps.setFieldTouched); + notifications.toasts.addDanger('One or more input fields is invalid'); + } else { + handleSubmit(formikProps); + } + }; + + const handleSubmit = (formikProps) => { + formikProps.setSubmitting(true); + try { + const detectorToCreate = formikToDetector(formikProps.values); + dispatch(createDetector(detectorToCreate)) + .then(async (response) => { + notifications.toasts.addSuccess( + `Detector created: ${formikProps.values.name}` + ); + dispatch(startDetector(response.response.id)) + .then((startDetectorResponse) => { + notifications.toasts.addSuccess( + `Successfully started the real-time detector` + ); + }) + .catch((err: any) => { + notifications.toasts.addDanger( + prettifyErrorMessage( + getErrorMessage( + err, + 'There was a problem starting the real-time detector' + ) + ) + ); + }); + + const fn = { + type: VisLayerTypes.PointInTimeEvents, + name: OVERLAY_ANOMALIES, + args: { + detectorId: response.response.id, + }, + } as VisLayerExpressionFn; + + const pluginResource = { + type: VIS_LAYER_PLUGIN_TYPE, + id: response.response.id, + } as ISavedPluginResource; + + const savedObjectToCreate = { + title: embeddable.vis.title, + originPlugin: ORIGIN_PLUGIN_VIS_LAYER, + pluginResource: pluginResource, + visId: embeddable.vis.id, + savedObjectType: 'visualization', + visLayerExpressionFn: fn, + } as ISavedAugmentVis; + + // TODO: catch saved object failure + const savedObject = await createAugmentVisSavedObject( + savedObjectToCreate + ); + + const saveObjectResponse = await savedObject.save({}); + }) + .catch((err: any) => { + dispatch(getDetectorCount()).then((response: any) => { + const totalDetectors = get(response, 'response.count', 0); + if (totalDetectors === MAX_DETECTORS) { + notifications.toasts.addDanger( + 'Cannot create detector - limit of ' + + MAX_DETECTORS + + ' detectors reached' + ); + } else { + notifications.toasts.addDanger( + prettifyErrorMessage( + getErrorMessage( + err, + 'There was a problem creating the detector' + ) + ) + ); + } + }); + }); + closeFlyout(); + } catch (e) { + } finally { + formikProps.setSubmitting(false); + } + }; + + const validateVisDetectorName = async (detectorName: string) => { + if (isEmpty(detectorName)) { + return 'Detector name cannot be empty'; + } else { + const error = validateDetectorName(detectorName); + if (error) { + return error; + } + const resp = await dispatch(matchDetector(detectorName)); + const match = get(resp, 'response.match', false); + if (!match) { + return undefined; + } + //If more than one detectors found, duplicate exists. + if (match) { + return 'Duplicate detector name'; + } + } + }; + + const initialDetectorValue = { + name: detectorNameFromVis, + index: [{ label: embeddable.vis.data.aggs.indexPattern.title }], + timeField: embeddable.vis.data.indexPattern.timeFieldName, + interval: intervalValue, + windowDelay: delayValue, + shingleSize: 8, + filterQuery: { match_all: {} }, + description: '', + resultIndex: undefined, + filters: [], + featureList: visFeatureListToFormik( + featureList, + embeddable.vis.params.seriesParams + ), + categoryFieldEnabled: false, + realTime: true, + historical: false, + }; + + return ( +
+ + {(formikProps) => ( + <> + + +

Add anomaly detector

+
+
+ +
+ + + Options to create a new detector or associate an + existing detector + + + ), + }} + className="add-anomaly-detector__modes" + > + {[ + { + id: 'add-anomaly-detector__create', + label: 'Create new detector', + value: 'create', + }, + { + id: 'add-anomaly-detector__existing', + label: 'Associate existing detector', + value: 'existing', + }, + ].map((option) => ( + setMode(option.value), + }} + /> + ))} + + + {mode === 'create' && ( +
+ +

+ Create and configure an anomaly detector to + automatically detect anomalies in your data and to view + real-time results on the visualization.{' '} + + Learn more + +

+
+ +
+ +

+ + {title} +

+
+ setIsShowVis(!isShowVis)} + /> +
+
+ + +
+ + +

Detector details

+
+ + + onAccordionToggle('detectorDetails')} + subTitle={ + +

+ Detector interval: {intervalValue} minutes; Window + delay: {delayValue} minutes +

+
+ } + > + + {({ field, form }: FieldProps) => ( + + onDetectorNameChange(e, field)} + /> + + )} + + + + + {({ field, form }: FieldProps) => ( + + + + + + + onIntervalChange(e, field) + } + /> + + + +

minutes

+
+
+
+
+
+
+ )} +
+ + + + {({ field, form }: FieldProps) => ( + + + + onDelayChange(e, field)} + /> + + + +

minutes

+
+
+
+
+ )} +
+
+ + + + + onAccordionToggle('advancedConfiguration') + } + initialIsOpen={false} + > + + + + +

+ Source:{' '} + {embeddable.vis.data.aggs.indexPattern.title} +

+
+ + +
+ + + + + {({ field, form }: FieldProps) => ( + + + + + + + +

intervals

+
+
+
+
+ )} +
+
+ + + + {({ field, form }: FieldProps) => ( + + + { + if (enabled) { + form.setFieldValue('resultIndex', ''); + } + setEnabled(!enabled); + }} + /> + + + {enabled ? ( + + + + ) : null} + + {enabled ? ( + + + + + + ) : null} + + )} + + + + + +

+ The dashboard does not support high-cardinality + detectors. + + Learn more + +

+
+
+
+ + + +

Model Features

+
+ + + onAccordionToggle('modelFeatures')} + > + + + + {({ + push, + remove, + form: { values }, + }: FieldArrayRenderProps) => { + return ( + + {values.featureList.map( + (feature: any, index: number) => ( + { + remove(index); + }} + index={index} + feature={feature} + handleChange={formikProps.handleChange} + displayMode="flyout" + /> + ) + )} + + = MAX_FEATURE_NUM + } + onClick={() => { + push(initialFeatureValue()); + }} + > + Add another feature + + + + +

+ You can add up to{' '} + {Math.max( + MAX_FEATURE_NUM - values.featureList.length, + 0 + )}{' '} + more features. +

+
+
+ ); + }} +
+
+
+ )} +
+
+ + + + Cancel + + + { + handleValidationAndSubmit(formikProps); + }} + > + Create Detector + + + + + + )} +
+
+ ); +} + +export default AddAnomalyDetector; diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/helpers.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/helpers.tsx new file mode 100644 index 00000000..5c69015c --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/helpers.tsx @@ -0,0 +1,61 @@ +import { dispatch } from 'd3'; +import { matchDetector } from 'public/redux/reducers/ad'; +import { validateDetectorName } from 'public/utils/utils'; +import { FEATURE_TYPE } from '../../../../public/models/interfaces'; +import { FeaturesFormikValues } from '../../../../public/pages/ConfigureModel/models/interfaces'; +import { find, get, isEmpty, snakeCase } from 'lodash'; + +export function visFeatureListToFormik( + featureList, + seriesParams +): FeaturesFormikValues[] { + return featureList.map((feature) => { + return { + featureId: feature.id, + featureName: getFeatureNameFromVisParams(feature.id, seriesParams), + featureEnabled: true, + featureType: FEATURE_TYPE.SIMPLE, + importance: 1, + newFeature: false, + aggregationBy: 'sum', + aggregationOf: visAggregationToFormik(feature), + aggregationQuery: JSON.stringify( + visAggregationQueryToFormik(feature, seriesParams) + ), + }; + }); +} + +export function formikToDetectorName(title) { + const detectorName = + title + '_anomaly_detector_' + Math.floor(100000 + Math.random() * 900000); + detectorName.replace(/[^a-zA-Z0-9-_]/g, '_'); + return detectorName; +} + +const getFeatureNameFromVisParams = (id, seriesParams) => { + let name = find(seriesParams, function (param) { + if (param.data.id === id) { + return true; + } + }); + + return name.data.label.replace(/[^a-zA-Z0-9-_]/g, '_'); +}; + +function visAggregationToFormik(value) { + return [ + { + label: value.params.field.name, + type: 'number', + }, + ]; +} + +function visAggregationQueryToFormik(value, seriesParams) { + return { + [snakeCase(getFeatureNameFromVisParams(value.id, seriesParams))]: { + sum: { field: value.params.field.name }, + }, + }; +} diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/index.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/index.tsx new file mode 100644 index 00000000..cacc501e --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/index.tsx @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import AddAnomalyDetector from './AddAnomalyDetector'; + +export default AddAnomalyDetector; diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/styles.scss b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/styles.scss new file mode 100644 index 00000000..bf457fc5 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/styles.scss @@ -0,0 +1,58 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +.add-anomaly-detector { + height: 100%; + display: flex; + flex-direction: column; + + .euiFlyoutBody__overflowContent { + height: 100%; + padding-bottom: 0; + } + + .euiFlexItem.add-anomaly-detector__scroll { + overflow-y: auto; + } + + &__flex-group { + height: 100%; + } + + &__modes { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + } +} + +.create-new { + &__vis { + height: 400px; + + &--hidden { + display: none; + } + } + + &__title-and-toggle { + display: flex; + justify-content: space-between; + } + + &__title-icon { + margin-right: 10px; + vertical-align: middle; + } + + .visualization { + padding: 0; + } +} + +.featureButton { + width: 100%; + height: 100%; + min-height: 40px; +} diff --git a/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/EnhancedAccordion.tsx b/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/EnhancedAccordion.tsx new file mode 100644 index 00000000..17075e27 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/EnhancedAccordion.tsx @@ -0,0 +1,87 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import React from 'react'; +import { + EuiTitle, + EuiSpacer, + EuiButtonIcon, + EuiButtonEmpty, + EuiAccordion, + EuiPanel, +} from '@elastic/eui'; +import './styles.scss'; + +const EnhancedAccordion = ({ + id, + title, + subTitle, + isOpen, + onToggle, + children, + isButton, + iconType, + extraAction, + initialIsOpen, +}) => ( +
+
+ +
+
+ {!isButton && ( + {extraAction}
+ } + forceState={isOpen ? 'open' : 'closed'} + onToggle={onToggle} + initialIsOpen={initialIsOpen} + buttonContent={ +
+ +

{title}

+
+ + {subTitle && ( + <> + + {subTitle} + + )} +
+ } + > + + {children} + + + )} + {isButton && ( + + )} +
+ +); + +export default EnhancedAccordion; diff --git a/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/index.tsx b/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/index.tsx new file mode 100644 index 00000000..0b994f5f --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/index.tsx @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import EnhancedAccordion from './EnhancedAccordion'; + +export default EnhancedAccordion; diff --git a/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/styles.scss b/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/styles.scss new file mode 100644 index 00000000..4615733d --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/styles.scss @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +.enhanced-accordion { + &__arrow { + transition: rotate 0.3s; + rotate: 0deg; + + &--open { + rotate: 90deg; + } + + &--hidden { + visibility: hidden; + } + } + + &__title { + padding: 12px 16px; + } + + &__extra { + padding-right: 16px; + } + + &__button { + width: 100%; + height: 100%; + min-height: 50px; + } +} diff --git a/public/components/FeatureAnywhereContextMenu/MinimalAccordion/MinimalAccordion.tsx b/public/components/FeatureAnywhereContextMenu/MinimalAccordion/MinimalAccordion.tsx new file mode 100644 index 00000000..ec290cd2 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/MinimalAccordion/MinimalAccordion.tsx @@ -0,0 +1,64 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import React from 'react'; +import { + EuiHorizontalRule, + EuiTitle, + EuiAccordion, + EuiSpacer, + EuiPanel, + EuiTextColor, + EuiText, +} from '@elastic/eui'; +import './styles.scss'; + +function MinimalAccordion({ + id, + title, + subTitle, + children, + isUsingDivider, + extraAction, +}) { + return ( +
+ {isUsingDivider && ( + <> + + + + )} + + +
{title}
+
+ {subTitle && ( + + {subTitle} + + )} + + } + extraAction={ +
{extraAction}
+ } + > + + {children} + +
+
+ ); +} + +export default MinimalAccordion; diff --git a/public/components/FeatureAnywhereContextMenu/MinimalAccordion/index.tsx b/public/components/FeatureAnywhereContextMenu/MinimalAccordion/index.tsx new file mode 100644 index 00000000..7f222f69 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/MinimalAccordion/index.tsx @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import MinimalAccordion from './MinimalAccordion'; + +export default MinimalAccordion; diff --git a/public/components/FeatureAnywhereContextMenu/MinimalAccordion/styles.scss b/public/components/FeatureAnywhereContextMenu/MinimalAccordion/styles.scss new file mode 100644 index 00000000..3b64d5ee --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/MinimalAccordion/styles.scss @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +.minimal-accordion { + .euiAccordion__button { + align-items: flex-start; + + &:hover, + &:focus { + text-decoration: none; + + .minimal-accordion__title { + text-decoration: underline; + } + } + } + + &__title { + margin-top: -5px; + font-weight: 400; + } + + &__panel { + padding-left: 28px; + padding-bottom: 0; + } +} diff --git a/public/expressions/constants.ts b/public/expressions/constants.ts index 032d544c..2082e349 100644 --- a/public/expressions/constants.ts +++ b/public/expressions/constants.ts @@ -9,3 +9,5 @@ export const ORIGIN_PLUGIN_VIS_LAYER = 'anomalyDetectionDashboards'; export const VIS_LAYER_PLUGIN_TYPE = 'Anomaly Detectors'; export const TYPE_OF_EXPR_VIS_LAYERS = 'vis_layers'; + +export const OVERLAY_ANOMALIES = 'overlay_anomalies'; diff --git a/public/expressions/overlay_anomalies.ts b/public/expressions/overlay_anomalies.ts index ddbc96cf..5d639297 100644 --- a/public/expressions/overlay_anomalies.ts +++ b/public/expressions/overlay_anomalies.ts @@ -32,21 +32,21 @@ import { PLUGIN_NAME } from '../utils/constants'; import { NO_PERMISSIONS_KEY_WORD } from '../../server/utils/helpers'; import { ORIGIN_PLUGIN_VIS_LAYER, + OVERLAY_ANOMALIES, TYPE_OF_EXPR_VIS_LAYERS, VIS_LAYER_PLUGIN_TYPE, } from './constants'; type Input = ExprVisLayers; type Output = Promise; +type Name = typeof OVERLAY_ANOMALIES; interface Arguments { detectorId: string; } -const name = 'overlay_anomalies'; - export type OverlayAnomaliesExpressionFunctionDefinition = - ExpressionFunctionDefinition<'overlay_anomalies', Input, Arguments, Output>; + ExpressionFunctionDefinition; // This gets all the needed anomalies for the given detector ID and time range const getAnomalies = async ( @@ -112,9 +112,10 @@ const convertAnomaliesToPointInTimeEventsVisLayer = ( * If there are any errors fetching the anomalies the function will return a VisLayerError in the * VisLayer detailing the error type. */ + export const overlayAnomaliesFunction = (): OverlayAnomaliesExpressionFunctionDefinition => ({ - name, + name: OVERLAY_ANOMALIES, type: TYPE_OF_EXPR_VIS_LAYERS, inputTypes: [TYPE_OF_EXPR_VIS_LAYERS], help: i18n.translate('data.functions.overlay_anomalies.help', { diff --git a/public/pages/ConfigureModel/components/FeatureAccordion/FeatureAccordion.tsx b/public/pages/ConfigureModel/components/FeatureAccordion/FeatureAccordion.tsx index 17efc289..f1805858 100644 --- a/public/pages/ConfigureModel/components/FeatureAccordion/FeatureAccordion.tsx +++ b/public/pages/ConfigureModel/components/FeatureAccordion/FeatureAccordion.tsx @@ -20,6 +20,7 @@ import { EuiButton, EuiFieldText, EuiCheckbox, + EuiButtonIcon, } from '@elastic/eui'; import { Field, FieldProps } from 'formik'; import { @@ -40,6 +41,7 @@ interface FeatureAccordionProps { index: number; feature: any; handleChange(event: React.ChangeEvent): void; + displayMode?: string; } export const FeatureAccordion = (props: FeatureAccordionProps) => { @@ -94,11 +96,25 @@ export const FeatureAccordion = (props: FeatureAccordionProps) => { ); }; - const deleteAction = (onClick: any) => ( - - Delete - - ); + const deleteAction = (onClick: any) => { + if (props.displayMode === 'flyout') { + return ( + + ); + } else { + return ( + + Delete + + ); + } + }; return ( Date: Thu, 25 May 2023 17:50:53 -0700 Subject: [PATCH 23/43] Adding functionality to associate existing detector with a visualization (#484) * Adding associate existing Signed-off-by: Amit Galitzky * removed usememo, addressed other comments Signed-off-by: Amit Galitzky * merge cleanup Signed-off-by: Amit Galitzky * added integration to call on alerting Signed-off-by: Amit Galitzky * cleaned up files and added changes to check if detector is deleted in expr fn Signed-off-by: Amit Galitzky * fixing dependency and notifcations issues Signed-off-by: Amit Galitzky * removed long toast life time Signed-off-by: Amit Galitzky --------- Signed-off-by: Amit Galitzky --- .../AnywhereParentFlyout.tsx | 14 +- .../AnywhereParentFlyout/constants.ts | 13 + .../containers/AssociatedDetectors.tsx | 60 ++-- .../AddAnomalyDetector.tsx | 267 +++++++++++++---- .../containers/AssociateExisting.tsx | 272 ++++++++++++++++++ .../AssociateExisting/index.ts | 6 + .../CreateAnomalyDetector/styles.scss | 10 + public/expressions/constants.ts | 2 + public/expressions/overlay_anomalies.ts | 41 ++- public/models/interfaces.ts | 1 + public/plugin.ts | 30 +- public/services.ts | 12 +- public/utils/contextMenu/getActions.tsx | 5 +- server/utils/helpers.ts | 4 + 14 files changed, 630 insertions(+), 107 deletions(-) create mode 100644 public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/constants.ts create mode 100644 public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AssociateExisting/containers/AssociateExisting.tsx create mode 100644 public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AssociateExisting/index.ts diff --git a/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/AnywhereParentFlyout.tsx b/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/AnywhereParentFlyout.tsx index d2e624ca..5ab72b2d 100644 --- a/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/AnywhereParentFlyout.tsx +++ b/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/AnywhereParentFlyout.tsx @@ -6,7 +6,8 @@ import React, { useState } from 'react'; import { get } from 'lodash'; import AssociatedDetectors from '../AssociatedDetectors/containers/AssociatedDetectors'; import { getEmbeddable } from '../../../../public/services'; -import AddAnomalyDetector from '../CreateAnomalyDetector/AddAnomalyDetector'; +import AddAnomalyDetector from '../CreateAnomalyDetector'; +import { FLYOUT_MODES } from './constants'; const AnywhereParentFlyout = ({ startingFlyout, ...props }) => { const embeddable = getEmbeddable().getEmbeddableFactory; @@ -15,11 +16,12 @@ const AnywhereParentFlyout = ({ startingFlyout, ...props }) => { ]; const [mode, setMode] = useState(startingFlyout); - const [selectedDetectorId, setSelectedDetectorId] = useState(); + const [selectedDetector, setSelectedDetector] = useState(undefined); const AnywhereFlyout = { - create: AddAnomalyDetector, - associated: AssociatedDetectors, + [FLYOUT_MODES.create]: AddAnomalyDetector, + [FLYOUT_MODES.associated]: AssociatedDetectors, + [FLYOUT_MODES.existing]: AddAnomalyDetector, }[mode]; return ( @@ -29,8 +31,8 @@ const AnywhereParentFlyout = ({ startingFlyout, ...props }) => { setMode, mode, indices, - selectedDetectorId, - setSelectedDetectorId, + selectedDetector, + setSelectedDetector, }} /> ); diff --git a/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/constants.ts b/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/constants.ts new file mode 100644 index 00000000..fa470962 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/constants.ts @@ -0,0 +1,13 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +//created: Flyout for creating a new anomaly detector from a visualization +//associated: Flyout for listing all the associated detectors to the given visualization +//existing: Flyout for associating existing detectors with the current visualizations +export enum FLYOUT_MODES { + create = 'create', + associated = 'associated', + existing = 'existing', +} diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx index c0b4f64f..69f299cc 100644 --- a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx @@ -24,6 +24,7 @@ import { DetectorListItem } from '../../../../models/interfaces'; import { getSavedFeatureAnywhereLoader, getNotifications, + getUISettings, } from '../../../../services'; import { GET_ALL_DETECTORS_QUERY_PARAMS, @@ -39,7 +40,11 @@ import { EmptyAssociatedDetectorMessage, ConfirmUnlinkDetectorModal, } from '../components'; -import { ISavedAugmentVis } from '../../../../../../../src/plugins/vis_augmenter/public'; +import { + ISavedAugmentVis, + SavedAugmentVisLoader, + getAugmentVisSavedObjs, +} from '../../../../../../../src/plugins/vis_augmenter/public'; import { ASSOCIATED_DETECTOR_ACTION } from '../utils/constants'; interface ConfirmModalState { @@ -82,8 +87,10 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) { ); // Establish savedObjectLoader for all operations on vis_augment saved objects - const savedObjectLoader: SavedObjectLoader = getSavedFeatureAnywhereLoader(); + const savedObjectLoader: SavedAugmentVisLoader = + getSavedFeatureAnywhereLoader(); + const uiSettings = getUISettings(); const notifications = getNotifications(); useEffect(() => { @@ -127,15 +134,12 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) { // Handles all changes in the assoicated detectors such as unlinking or new detectors associated useEffect(() => { - // Gets all augmented saved objects - savedObjectLoader - .findAll() - .then((resp: any) => { - if (resp != undefined) { - const savedAugmentObjectsArr: ISavedAugmentVis[] = get( - resp, - 'hits', - [] + // Gets all augmented saved objects that are associated to the given visualization + getAugmentVisSavedObjs(embeddable.vis.id, savedObjectLoader, uiSettings) + .then((savedAugmentObjectsArr: any) => { + if (savedAugmentObjectsArr != undefined) { + console.log( + 'savedAugmentObjectsArr: ' + JSON.stringify(savedAugmentObjectsArr) ); const curSelectedDetectors = getAssociatedDetectors( Object.values(allDetectors), @@ -156,14 +160,8 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) { // that are associated to the current visualization const getAssociatedDetectors = ( detectors: DetectorListItem[], - savedAugmentObjects: ISavedAugmentVis[] + savedAugmentForThisVisualization: ISavedAugmentVis[] ) => { - // Filter all savedAugmentObjects that aren't linked to the specific visualization - const savedAugmentForThisVisualization: ISavedAugmentVis[] = - savedAugmentObjects.filter( - (savedObj) => get(savedObj, 'visId', '') === embeddable.vis.id - ); - // Map all detector IDs for all the found augmented vis objects const savedAugmentDetectorsSet = new Set( savedAugmentForThisVisualization.map((savedObject) => @@ -180,18 +178,13 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) { const onUnlinkDetector = async () => { setIsLoadingFinalDetectors(true); - await savedObjectLoader.findAll().then(async (resp: any) => { - if (resp != undefined) { - // gets all the saved object for this visualization - const savedAugmentForThisVisualization: ISavedAugmentVis[] = get( - resp, - 'hits', - [] as ISavedAugmentVis[] - ).filter( - (savedObj: ISavedAugmentVis[]) => - get(savedObj, 'visId', '') === embeddable.vis.id - ); - + // Gets all augmented saved objects that are associated to the given visualization + await getAugmentVisSavedObjs( + embeddable.vis.id, + savedObjectLoader, + uiSettings + ).then(async (savedAugmentForThisVisualization: any) => { + if (savedAugmentForThisVisualization != undefined) { // find saved augment object matching detector we want to unlink // There should only be one detector and vis pairing const savedAugmentToUnlink = savedAugmentForThisVisualization.find( @@ -239,11 +232,6 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) { dispatch(getDetectorList(GET_ALL_DETECTORS_QUERY_PARAMS)); }; - // TODO: this part is incomplete because it is pending on a different PR that will have all the associate existing changes - const openAssociateDetectorFlyout = async () => { - console.log('inside create anomaly detector'); - }; - const handleUnlinkDetectorAction = (detector: DetectorListItem) => { setDetectorToUnlink(detector); setConfirmModalState({ @@ -326,7 +314,7 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) { fill iconType="link" onClick={() => { - openAssociateDetectorFlyout(); + setMode('existing'); }} > Associate a detector diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx index ead4014b..307f26de 100644 --- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx @@ -31,6 +31,7 @@ import { createAugmentVisSavedObject, ISavedAugmentVis, ISavedPluginResource, + SavedAugmentVisLoader, VisLayerExpressionFn, VisLayerTypes, } from '../../../../../../src/plugins/vis_augmenter/public'; @@ -80,9 +81,15 @@ import { FeatureAccordion } from '../../../../public/pages/ConfigureModel/compon import { AD_DOCS_LINK, AD_HIGH_CARDINALITY_LINK, + DEFAULT_SHINGLE_SIZE, MAX_FEATURE_NUM, } from '../../../../public/utils/constants'; -import { getNotifications } from '../../../../public/services'; +import { + getNotifications, + getSavedFeatureAnywhereLoader, + getUISettings, + getUiActions, +} from '../../../../public/services'; import { prettifyErrorMessage } from '../../../../server/utils/helpers'; import { ORIGIN_PLUGIN_VIS_LAYER, @@ -90,8 +97,18 @@ import { VIS_LAYER_PLUGIN_TYPE, } from '../../../../public/expressions/constants'; import { formikToDetectorName, visFeatureListToFormik } from './helpers'; +import { AssociateExisting } from './AssociateExisting'; +import { mountReactNode } from '../../../../../../src/core/public/utils'; +import { FLYOUT_MODES } from '../AnywhereParentFlyout/constants'; -function AddAnomalyDetector({ embeddable, closeFlyout, mode, setMode }) { +function AddAnomalyDetector({ + embeddable, + closeFlyout, + mode, + setMode, + selectedDetector, + setSelectedDetector, +}) { const dispatch = useDispatch(); const [queryText, setQueryText] = useState(''); useEffect(() => { @@ -148,21 +165,48 @@ function AddAnomalyDetector({ embeddable, closeFlyout, mode, setMode }) { } }; - const handleSubmit = (formikProps) => { + const uiSettings = getUISettings(); + const savedObjectLoader: SavedAugmentVisLoader = + getSavedFeatureAnywhereLoader(); + + const getAugmentVisSavedObject = (detectorId: string) => { + const fn = { + type: VisLayerTypes.PointInTimeEvents, + name: OVERLAY_ANOMALIES, + args: { + detectorId: detectorId, + }, + } as VisLayerExpressionFn; + + const pluginResource = { + type: VIS_LAYER_PLUGIN_TYPE, + id: detectorId, + } as ISavedPluginResource; + + return { + title: embeddable.vis.title, + originPlugin: ORIGIN_PLUGIN_VIS_LAYER, + pluginResource: pluginResource, + visId: embeddable.vis.id, + visLayerExpressionFn: fn, + } as ISavedAugmentVis; + }; + + // Error handeling/notification cases listed here as many things are being done sequentially + //1. if detector is created succesfully, started succesfully and associated succesfully and alerting exists -> show end message with alerting button + //2. If detector is created succesfully, started succesfully and associated succesfully and alerting doesn't exist -> show end message with OUT alerting button + //3. If detector is created succesfully, started succesfully and fails association -> show one toast with detector created, and one toast with failed association + //4. If detector is created succesfully, fails starting and fails association -> show one toast with detector created succesfully, one toast with failed association + //5. If detector is created successfully, fails starting and fails associating -> show one toast with detector created succesfully, one toast with fail starting, one toast with failed association + //6. If detector fails creating -> show one toast with detector failed creating + const handleSubmit = async (formikProps) => { formikProps.setSubmitting(true); try { const detectorToCreate = formikToDetector(formikProps.values); dispatch(createDetector(detectorToCreate)) .then(async (response) => { - notifications.toasts.addSuccess( - `Detector created: ${formikProps.values.name}` - ); dispatch(startDetector(response.response.id)) - .then((startDetectorResponse) => { - notifications.toasts.addSuccess( - `Successfully started the real-time detector` - ); - }) + .then((startDetectorResponse) => {}) .catch((err: any) => { notifications.toasts.addDanger( prettifyErrorMessage( @@ -174,34 +218,61 @@ function AddAnomalyDetector({ embeddable, closeFlyout, mode, setMode }) { ); }); - const fn = { - type: VisLayerTypes.PointInTimeEvents, - name: OVERLAY_ANOMALIES, - args: { - detectorId: response.response.id, - }, - } as VisLayerExpressionFn; - - const pluginResource = { - type: VIS_LAYER_PLUGIN_TYPE, - id: response.response.id, - } as ISavedPluginResource; - - const savedObjectToCreate = { - title: embeddable.vis.title, - originPlugin: ORIGIN_PLUGIN_VIS_LAYER, - pluginResource: pluginResource, - visId: embeddable.vis.id, - savedObjectType: 'visualization', - visLayerExpressionFn: fn, - } as ISavedAugmentVis; - - // TODO: catch saved object failure - const savedObject = await createAugmentVisSavedObject( - savedObjectToCreate - ); - - const saveObjectResponse = await savedObject.save({}); + const detectorId = response.response.id; + const augmentVisSavedObjectToCreate: ISavedAugmentVis = + getAugmentVisSavedObject(detectorId); + + createAugmentVisSavedObject( + augmentVisSavedObjectToCreate, + uiSettings, + savedObjectLoader + ) + .then((savedObject: any) => { + savedObject + .save({}) + .then((response: any) => { + const shingleSize = get( + formikProps.values, + 'shingleSize', + DEFAULT_SHINGLE_SIZE + ); + const detectorId = get(savedObject, 'pluginResource.id', ''); + notifications.toasts.addSuccess({ + title: `The ${formikProps.values.name} is associated with the ${title} visualization`, + text: mountReactNode( + getEverythingSuccessfulButton(detectorId, shingleSize) + ), + className: 'createdAndAssociatedSuccessToast', + }); + closeFlyout(); + }) + .catch((error) => { + console.error( + `Error associating selected detector in save process: ${error}` + ); + notifications.toasts.addDanger( + prettifyErrorMessage( + `Error associating selected detector in save process: ${error}` + ) + ); + notifications.toasts.addSuccess( + `Detector created: ${formikProps.values.name}` + ); + }); + }) + .catch((error) => { + console.error( + `Error associating selected detector in create process: ${error}` + ); + notifications.toasts.addDanger( + prettifyErrorMessage( + `Error associating selected detector in create process: ${error}` + ) + ); + notifications.toasts.addSuccess( + `Detector created: ${formikProps.values.name}` + ); + }); }) .catch((err: any) => { dispatch(getDetectorCount()).then((response: any) => { @@ -231,6 +302,84 @@ function AddAnomalyDetector({ embeddable, closeFlyout, mode, setMode }) { } }; + const getEverythingSuccessfulButton = (detectorId, shingleSize) => { + return ( + +

+ Attempting to initialize the detector with historical data. This + initializing process takes approximately 1 minute if you have data in + each of the last {32 + shingleSize} consecutive intervals. +

+ {alertingExists() ? ( + + +

Set up alerts to be notified of any anomalies.

+
+ +
+ openAlerting(detectorId)}> + Set up alerts + +
+
+
+ ) : null} +
+ ); + }; + + const alertingExists = () => { + try { + const uiActionService = getUiActions(); + uiActionService.getTrigger('ALERTING_TRIGGER_AD_ID'); + return true; + } catch (e) { + console.error('No alerting trigger exists', e); + return false; + } + }; + + const openAlerting = (detectorId: string) => { + const uiActionService = getUiActions(); + uiActionService + .getTrigger('ALERTING_TRIGGER_AD_ID') + .exec({ embeddable, detectorId }); + }; + + const handleAssociate = async (detector: DetectorListItem) => { + const augmentVisSavedObjectToCreate: ISavedAugmentVis = + getAugmentVisSavedObject(detector.id); + + createAugmentVisSavedObject( + augmentVisSavedObjectToCreate, + uiSettings, + savedObjectLoader + ) + .then((savedObject: any) => { + savedObject + .save({}) + .then((response: any) => { + notifications.toasts.addSuccess({ + title: `The ${detector.name} is associated with the ${title} visualization`, + text: "The detector's anomalies do not appear on the visualization. Refresh your dashboard to update the visualization", + }); + closeFlyout(); + }) + .catch((error) => { + notifications.toasts.addDanger( + prettifyErrorMessage( + `Error associating selected detector: ${error}` + ) + ); + }); + }) + .catch((error) => { + notifications.toasts.addDanger( + prettifyErrorMessage(`Error associating selected detector: ${error}`) + ); + }); + }; + const validateVisDetectorName = async (detectorName: string) => { if (isEmpty(detectorName)) { return 'Detector name cannot be empty'; @@ -325,7 +474,14 @@ function AddAnomalyDetector({ embeddable, closeFlyout, mode, setMode }) { ))} - {mode === 'create' && ( + {mode === FLYOUT_MODES.existing && ( + + )} + {mode === FLYOUT_MODES.create && (

@@ -695,16 +851,27 @@ function AddAnomalyDetector({ embeddable, closeFlyout, mode, setMode }) { Cancel - { - handleValidationAndSubmit(formikProps); - }} - > - Create Detector - + {mode === FLYOUT_MODES.existing ? ( + handleAssociate(selectedDetector)} + > + Associate detector + + ) : ( + { + handleValidationAndSubmit(formikProps); + }} + > + Create detector + + )} diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AssociateExisting/containers/AssociateExisting.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AssociateExisting/containers/AssociateExisting.tsx new file mode 100644 index 00000000..ba5e12fc --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AssociateExisting/containers/AssociateExisting.tsx @@ -0,0 +1,272 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useMemo, useState } from 'react'; +import { + EuiTitle, + EuiSpacer, + EuiIcon, + EuiText, + EuiComboBox, + EuiLoadingSpinner, + EuiLink, + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiHorizontalRule, +} from '@elastic/eui'; +import { useDispatch, useSelector } from 'react-redux'; +import { get } from 'lodash'; +import { CoreServicesContext } from '../../../../../components/CoreServices/CoreServices'; +import { CoreStart } from '../../../../../../../../src/core/public'; +import { AppState } from '../../../../../redux/reducers'; +import { DetectorListItem } from '../../../../../models/interfaces'; +import { + GET_ALL_DETECTORS_QUERY_PARAMS, + SINGLE_DETECTOR_NOT_FOUND_MSG, +} from '../../../../../pages/utils/constants'; +import { + NO_PERMISSIONS_KEY_WORD, + prettifyErrorMessage, +} from '../../../../../../server/utils/helpers'; +import { getDetectorList } from '../../../../../redux/reducers/ad'; +import { + getSavedFeatureAnywhereLoader, + getUISettings, +} from '../../../../../services'; +import { + ISavedAugmentVis, + SavedAugmentVisLoader, + getAugmentVisSavedObjs, +} from '../../../../../../../../src/plugins/vis_augmenter/public'; +import { stateToColorMap } from '../../../../../pages/utils/constants'; +import { + BASE_DOCS_LINK, + PLUGIN_NAME, +} from '../../../../../../public/utils/constants'; +import { renderTime } from '../../../../../../public/pages/DetectorsList/utils/tableUtils'; + +interface AssociateExistingProps { + embeddableVisId: string; + selectedDetector: DetectorListItem | undefined; + setSelectedDetector(detector: DetectorListItem | undefined): void; +} + +export function AssociateExisting( + associateExistingProps: AssociateExistingProps +) { + const core = React.useContext(CoreServicesContext) as CoreStart; + const dispatch = useDispatch(); + const allDetectors = useSelector((state: AppState) => state.ad.detectorList); + const isRequestingFromES = useSelector( + (state: AppState) => state.ad.requesting + ); + const uiSettings = getUISettings(); + const [isLoadingFinalDetectors, setIsLoadingFinalDetectors] = + useState(true); + const isLoading = isRequestingFromES || isLoadingFinalDetectors; + const errorGettingDetectors = useSelector( + (state: AppState) => state.ad.errorMessage + ); + const [ + existingDetectorsAvailableToAssociate, + setExistingDetectorsAvailableToAssociate, + ] = useState([] as DetectorListItem[]); + + // Establish savedObjectLoader for all operations on vis augmented saved objects + const savedObjectLoader: SavedAugmentVisLoader = + getSavedFeatureAnywhereLoader(); + + useEffect(() => { + if ( + errorGettingDetectors && + !errorGettingDetectors.includes(SINGLE_DETECTOR_NOT_FOUND_MSG) + ) { + console.error(errorGettingDetectors); + core.notifications.toasts.addDanger( + typeof errorGettingDetectors === 'string' && + errorGettingDetectors.includes(NO_PERMISSIONS_KEY_WORD) + ? prettifyErrorMessage(errorGettingDetectors) + : 'Unable to get all detectors' + ); + setIsLoadingFinalDetectors(false); + } + }, [errorGettingDetectors]); + + // Handle all changes in the assoicated detectors such as unlinking or new detectors associated + useEffect(() => { + // Gets all augmented saved objects for the given visualization + getAugmentVisSavedObjs( + associateExistingProps.embeddableVisId, + savedObjectLoader, + uiSettings + ).then((savedAugmentObjectsArr: any) => { + if (savedAugmentObjectsArr != undefined) { + const curDetectorsToDisplayOnList = + getExistingDetectorsAvailableToAssociate( + Object.values(allDetectors), + savedAugmentObjectsArr + ); + setExistingDetectorsAvailableToAssociate(curDetectorsToDisplayOnList); + setIsLoadingFinalDetectors(false); + } + }); + }, [allDetectors]); + + // cross checks all the detectors that exist with all the savedAugment Objects to only display ones + // that are associated to the current visualization + const getExistingDetectorsAvailableToAssociate = ( + detectors: DetectorListItem[], + savedAugmentForThisVisualization: ISavedAugmentVis[] + ) => { + // Map all detector IDs for all the found augmented vis objects + const savedAugmentDetectorsSet = new Set( + savedAugmentForThisVisualization.map((savedObject) => + get(savedObject, 'pluginResource.id', '') + ) + ); + + // detectors here is all detectors + // for each detector in all detectors return that detector if that detector ID isnt in the set + // filter out any detectors that aren't on the set of detectors IDs from the augmented vis objects. + const detectorsToDisplay = detectors.filter((detector) => { + if ( + !savedAugmentDetectorsSet.has(detector.id) && + detector.detectorType === 'SINGLE_ENTITY' + ) { + return detector; + } + }); + return detectorsToDisplay; + }; + + useEffect(() => { + getDetectors(); + }, []); + + const getDetectors = async () => { + dispatch(getDetectorList(GET_ALL_DETECTORS_QUERY_PARAMS)); + }; + + const selectedOptions = useMemo(() => { + if ( + !existingDetectorsAvailableToAssociate || + !associateExistingProps.selectedDetector + ) { + return []; + } + + const detector = (existingDetectorsAvailableToAssociate || []).find( + (detector) => + detector.id === get(associateExistingProps.selectedDetector, 'id', '') + ); + return detector ? [{ label: detector.name }] : []; + }, [ + associateExistingProps.selectedDetector, + existingDetectorsAvailableToAssociate, + ]); + + const detector = associateExistingProps.selectedDetector; + + const options = useMemo(() => { + if (!existingDetectorsAvailableToAssociate) { + return []; + } + + return existingDetectorsAvailableToAssociate.map((detector) => ({ + label: detector.name, + })); + }, [existingDetectorsAvailableToAssociate]); + + return ( +

+ +

+ View existing anomaly detectors across your system and add the + detector(s) to a dashboard and visualization.{' '} + + Learn more + +

+
+ + +

Select detector to associate

+
+ + + Eligible detectors don't include high-cardinality detectors. + + {existingDetectorsAvailableToAssociate ? ( + { + let detector = {} as DetectorListItem | undefined; + + if (selectedOptions && selectedOptions.length) { + const match = existingDetectorsAvailableToAssociate.find( + (detector) => detector.name === selectedOptions[0].label + ); + detector = match; + } + associateExistingProps.setSelectedDetector(detector); + }} + aria-label="Select an anomaly detector to associate" + isClearable + singleSelection + placeholder="Search for an anomaly detector" + /> + ) : ( + + )} + + {detector && ( + <> + + + +

{detector.name}

+
+ + + {renderTime(detector.enabledTime)} + +
+ + + View detector page + + +
+ +
    + {[ + ['Indices', (detector) => detector.indices], + [ + 'Anomalies last 24 hours', + (detector) => detector.totalAnomalies, + ], + [ + 'Last real-time occurrence', + (detector) => renderTime(detector.lastActiveAnomaly), + ], + ].map(([label, getValue]) => ( +
  • + + {label}: {getValue(detector)} + +
  • + ))} +
+ + )} +
+ ); +} + +export default AssociateExisting; diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AssociateExisting/index.ts b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AssociateExisting/index.ts new file mode 100644 index 00000000..90aa3ae3 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AssociateExisting/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { AssociateExisting } from './containers/AssociateExisting'; diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/styles.scss b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/styles.scss index bf457fc5..e16e3895 100644 --- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/styles.scss +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/styles.scss @@ -56,3 +56,13 @@ height: 100%; min-height: 40px; } + +.euiGlobalToastList { + width: 650px; +} + +.createdAndAssociatedSuccessToast { + width: 550px; + position: relative; + right: 15px; +} diff --git a/public/expressions/constants.ts b/public/expressions/constants.ts index 2082e349..41a79276 100644 --- a/public/expressions/constants.ts +++ b/public/expressions/constants.ts @@ -11,3 +11,5 @@ export const VIS_LAYER_PLUGIN_TYPE = 'Anomaly Detectors'; export const TYPE_OF_EXPR_VIS_LAYERS = 'vis_layers'; export const OVERLAY_ANOMALIES = 'overlay_anomalies'; + +export const PLUGIN_EVENT_TYPE = 'Anomalies'; diff --git a/public/expressions/overlay_anomalies.ts b/public/expressions/overlay_anomalies.ts index 5d639297..3ff8c128 100644 --- a/public/expressions/overlay_anomalies.ts +++ b/public/expressions/overlay_anomalies.ts @@ -29,10 +29,15 @@ import { VisLayerErrorTypes, } from '../../../../src/plugins/vis_augmenter/public'; import { PLUGIN_NAME } from '../utils/constants'; -import { NO_PERMISSIONS_KEY_WORD } from '../../server/utils/helpers'; +import { + CANT_FIND_KEY_WORD, + DOES_NOT_HAVE_PERMISSIONS_KEY_WORD, + NO_PERMISSIONS_KEY_WORD, +} from '../../server/utils/helpers'; import { ORIGIN_PLUGIN_VIS_LAYER, OVERLAY_ANOMALIES, + PLUGIN_EVENT_TYPE, TYPE_OF_EXPR_VIS_LAYERS, VIS_LAYER_PLUGIN_TYPE, } from './constants'; @@ -41,6 +46,8 @@ type Input = ExprVisLayers; type Output = Promise; type Name = typeof OVERLAY_ANOMALIES; +const DETECTOR_HAS_BEEN_DELETED = 'detector has been deleted'; + interface Arguments { detectorId: string; } @@ -72,9 +79,9 @@ const getAnomalies = async ( return parsePureAnomalies(anomalySummaryResponse); }; -const getDetectorName = async (detectorId: string) => { +const getDetectorResponse = async (detectorId: string) => { const resp = await getClient().get(`..${AD_NODE_API.DETECTOR}/${detectorId}`); - return get(resp.response, 'name', ''); + return resp; }; // This takes anomalies and returns them as vis layer of type PointInTimeEvents @@ -152,7 +159,17 @@ export const overlayAnomaliesFunction = urlPath: `${PLUGIN_NAME}#/detectors/${detectorId}/results`, //details page for detector in AD plugin }; try { - const detectorName = await getDetectorName(detectorId); + const detectorResponse = await getDetectorResponse(detectorId); + if (get(detectorResponse, 'error', '').includes(CANT_FIND_KEY_WORD)) { + throw new Error('Anomaly Detector - ' + DETECTOR_HAS_BEEN_DELETED); + } else if ( + get(detectorResponse, 'error', '').includes( + DOES_NOT_HAVE_PERMISSIONS_KEY_WORD + ) + ) { + throw new Error(get(detectorResponse, 'error', '')); + } + const detectorName = get(detectorResponse.response, 'name', ''); if (detectorName === '') { throw new Error('Anomaly Detector - Unable to get detector'); } @@ -177,15 +194,24 @@ export const overlayAnomaliesFunction = : ([anomalyLayer] as VisLayers), }; } catch (error) { - console.log('Anomaly Detector - Unable to get anomalies: ', error); + console.error('Anomaly Detector - Unable to get anomalies: ', error); let visLayerError: VisLayerError = {} as VisLayerError; if ( typeof error === 'string' && - error.includes(NO_PERMISSIONS_KEY_WORD) + (error.includes(NO_PERMISSIONS_KEY_WORD) || + error.includes(DOES_NOT_HAVE_PERMISSIONS_KEY_WORD)) ) { visLayerError = { type: VisLayerErrorTypes.PERMISSIONS_FAILURE, - message: error, //TODO: might just change this to a generic message like rest of AD plugin + message: error, + }; + } else if ( + typeof error === 'string' && + error.includes(DETECTOR_HAS_BEEN_DELETED) + ) { + visLayerError = { + type: VisLayerErrorTypes.RESOURCE_DELETED, + message: error, }; } else { visLayerError = { @@ -204,6 +230,7 @@ export const overlayAnomaliesFunction = pluginResource: ADPluginResource, events: [], error: visLayerError, + pluginEventType: PLUGIN_EVENT_TYPE, } as PointInTimeEventsVisLayer; return { type: TYPE_OF_EXPR_VIS_LAYERS, diff --git a/public/models/interfaces.ts b/public/models/interfaces.ts index f8fbc248..eff5ead5 100644 --- a/public/models/interfaces.ts +++ b/public/models/interfaces.ts @@ -217,6 +217,7 @@ export type DetectorListItem = { lastActiveAnomaly: number; lastUpdateTime: number; enabledTime?: number; + detectorType?: string; }; export type EntityData = { diff --git a/public/plugin.ts b/public/plugin.ts index 0c45e15e..83fd40eb 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -13,6 +13,7 @@ import { AppMountParameters, CoreSetup, CoreStart, + NotificationsSetup, NotificationsStart, Plugin, } from '../../../src/core/public'; @@ -30,11 +31,19 @@ import { setEmbeddable, setNotifications, setOverlays, - setSavedFeatureAnywhereLoader + setSavedFeatureAnywhereLoader, + setUiActions, + setUISettings, } from './services'; import { AnomalyDetectionOpenSearchDashboardsPluginStart } from 'public'; -import { VisAugmenterStart } from '../../../src/plugins/vis_augmenter/public'; - +import { + VisAugmenterSetup, + VisAugmenterStart, +} from '../../../src/plugins/vis_augmenter/public'; +import { + UiActionsSetup, + UiActionsStart, +} from '../../../src/plugins/ui_actions/public'; declare module '../../../src/plugins/ui_actions/public' { export interface ActionContextMapping { @@ -42,14 +51,19 @@ declare module '../../../src/plugins/ui_actions/public' { } } +//TODO: there is currently no savedAugmentVisLoader in VisAugmentSetup interface, this needs to be fixed export interface AnomalyDetectionSetupDeps { embeddable: EmbeddableSetup; + notifications: NotificationsSetup; + visAugmenter: VisAugmenterSetup; + //uiActions: UiActionsSetup; } export interface AnomalyDetectionStartDeps { embeddable: EmbeddableStart; notifications: NotificationsStart; visAugmenter: VisAugmenterStart; + uiActions: UiActionsStart; } export class AnomalyDetectionOpenSearchDashboardsPlugin @@ -72,6 +86,12 @@ export class AnomalyDetectionOpenSearchDashboardsPlugin }, }); + // // set embeddable plugin for feature anywhere create flyout + // setEmbeddable(embeddable); + + // // set vis argumenter loader for feature anywhere associated flyout + // setSavedFeatureAnywhereLoader(visAugmenter.savedAugmentVisLoader); + // Set the HTTP client so it can be pulled into expression fns to make // direct server-side calls setClient(core.http); @@ -91,12 +111,14 @@ export class AnomalyDetectionOpenSearchDashboardsPlugin public start( core: CoreStart, - { embeddable, visAugmenter }: AnomalyDetectionStartDeps + { embeddable, visAugmenter, uiActions }: AnomalyDetectionStartDeps ): AnomalyDetectionOpenSearchDashboardsPluginStart { + setUISettings(core.uiSettings); setEmbeddable(embeddable); setOverlays(core.overlays); setSavedFeatureAnywhereLoader(visAugmenter.savedAugmentVisLoader); setNotifications(core.notifications); + setUiActions(uiActions); return {}; } } diff --git a/public/services.ts b/public/services.ts index 1908f443..7e0d7843 100644 --- a/public/services.ts +++ b/public/services.ts @@ -5,15 +5,17 @@ import { CoreStart, + IUiSettingsClient, NotificationsStart, OverlayStart, } from '../../../src/core/public'; import { EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { createGetterSetter } from '../../../src/plugins/opensearch_dashboards_utils/public'; -import { SavedObjectLoader } from '../../../src/plugins/saved_objects/public'; +import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; +import { SavedAugmentVisLoader } from '../../../src/plugins/vis_augmenter/public'; export const [getSavedFeatureAnywhereLoader, setSavedFeatureAnywhereLoader] = - createGetterSetter('savedFeatureAnywhereLoader'); + createGetterSetter('savedFeatureAnywhereLoader'); export const [getClient, setClient] = createGetterSetter('http'); @@ -26,3 +28,9 @@ export const [getOverlays, setOverlays] = export const [getNotifications, setNotifications] = createGetterSetter('Notifications'); + +export const [getUiActions, setUiActions] = + createGetterSetter('UIActions'); + +export const [getUISettings, setUISettings] = + createGetterSetter('UISettings'); diff --git a/public/utils/contextMenu/getActions.tsx b/public/utils/contextMenu/getActions.tsx index 0c1302e4..cccfd399 100644 --- a/public/utils/contextMenu/getActions.tsx +++ b/public/utils/contextMenu/getActions.tsx @@ -15,6 +15,7 @@ import configureStore from '../../redux/configureStore'; import DocumentationTitle from '../../components/FeatureAnywhereContextMenu/DocumentationTitle/containers/DocumentationTitle'; import { AD_DOCS_LINK, APM_TRACE } from '../constants'; import { getClient, getOverlays } from '../../../public/services'; +import { FLYOUT_MODES } from '../../../public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/constants'; // This is used to create all actions in the same context menu const grouping: Action['grouping'] = [ @@ -58,7 +59,7 @@ export const getActions = () => { ), icon: 'plusInCircle' as EuiIconType, order: 100, - onClick: getOnClick('create'), + onClick: getOnClick(FLYOUT_MODES.create), }, { grouping, @@ -71,7 +72,7 @@ export const getActions = () => { ), icon: 'gear' as EuiIconType, order: 99, - onClick: getOnClick('associated'), + onClick: getOnClick(FLYOUT_MODES.associated), }, { id: 'documentationAnomalyDetector', diff --git a/server/utils/helpers.ts b/server/utils/helpers.ts index 035d2c74..15c80b3e 100644 --- a/server/utils/helpers.ts +++ b/server/utils/helpers.ts @@ -66,6 +66,10 @@ const PERMISSIONS_ERROR_PATTERN = export const NO_PERMISSIONS_KEY_WORD = 'no permissions'; +export const DOES_NOT_HAVE_PERMISSIONS_KEY_WORD = 'does not have permissions'; + +export const CANT_FIND_KEY_WORD = "Can't find"; + export const prettifyErrorMessage = (rawErrorMessage: string) => { if (isEmpty(rawErrorMessage) || rawErrorMessage === 'undefined') { return 'Unknown error is returned.'; From 5e76e44dc2fbe76acb71dfe683ecc04ea4a39186 Mon Sep 17 00:00:00 2001 From: Amit Galitzky Date: Fri, 26 May 2023 10:02:27 -0700 Subject: [PATCH 24/43] switched argument order (#494) Signed-off-by: Amit Galitzky --- .../CreateAnomalyDetector/AddAnomalyDetector.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx index 307f26de..9e490801 100644 --- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx @@ -224,8 +224,8 @@ function AddAnomalyDetector({ createAugmentVisSavedObject( augmentVisSavedObjectToCreate, - uiSettings, - savedObjectLoader + savedObjectLoader, + uiSettings ) .then((savedObject: any) => { savedObject @@ -352,8 +352,8 @@ function AddAnomalyDetector({ createAugmentVisSavedObject( augmentVisSavedObjectToCreate, - uiSettings, - savedObjectLoader + savedObjectLoader, + uiSettings ) .then((savedObject: any) => { savedObject From 9308a4ccb120399202446982f396186ae8dcb793 Mon Sep 17 00:00:00 2001 From: Jackie Han Date: Tue, 30 May 2023 13:54:15 -0700 Subject: [PATCH 25/43] add jest transform config (#497) * add jest transform config Signed-off-by: Jackie Han * add license header Signed-off-by: Jackie Han * update file as .ts file Signed-off-by: Jackie Han * add comment on jest config file Signed-off-by: Jackie Han * add more comments Signed-off-by: Jackie Han * cleanup Signed-off-by: Jackie Han * update file export Signed-off-by: Jackie Han --------- Signed-off-by: Jackie Han --- test/jest.config.js | 10 ++++++++++ test/mocks/transformMock.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 test/mocks/transformMock.ts diff --git a/test/jest.config.js b/test/jest.config.js index c726f920..242924cc 100644 --- a/test/jest.config.js +++ b/test/jest.config.js @@ -44,4 +44,14 @@ module.exports = { testPathIgnorePatterns: ['/build/', '/node_modules/'], transformIgnorePatterns: ['/node_modules'], globalSetup: '/global-setup.js', + + /** + * This configuration specifies different file extensions + * and the corresponding transformers to be used + */ + transform: { + '\\.[jt]sx?$': 'babel-jest', + '^.+\\.svg$': '/test/mocks/transformMock.ts', + '^.+\\.html$': '/test/mocks/transformMock.ts', + }, }; diff --git a/test/mocks/transformMock.ts b/test/mocks/transformMock.ts new file mode 100644 index 00000000..ac888d71 --- /dev/null +++ b/test/mocks/transformMock.ts @@ -0,0 +1,27 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * The transform configuration in Jest allows you to + * specify custom transformation logic for specific file types during testing. + */ +module.exports = { + /** + * This function is responsible for transforming the file. + * @returns the string module.exports = {};, which is an empty CommonJS module. + */ + process() { + return { + code: `module.exports = {};`, + }; + }, + /** + * The cache key helps Jest determine if a file needs to be retransformed or if it can use the cached transformation result. + * @returns a unique string that serves as a cache key for the transformation. + */ + getCacheKey() { + return 'svgTransform'; + }, +}; \ No newline at end of file From db4e039c03d2b67d0ea385180e1db1cb29bd33c0 Mon Sep 17 00:00:00 2001 From: Amit Galitzky Date: Wed, 31 May 2023 16:31:33 -0700 Subject: [PATCH 26/43] Merging main branch into featureAnywhere (#499) * Add branch constants in CI workflow (#345) Signed-off-by: Tyler Ohlsen * Bump decode-uri-component (#359) Signed-off-by: Tyler Ohlsen * removed duplicate popout icon and ran prettier (#382) Signed-off-by: Amit Galitzky * Change detector out of time range modal warning into a callout warning (#384) Signed-off-by: Jackie Han Signed-off-by: Jackie Han * Fix undefined entity list when heatmap is empty (#383) Signed-off-by: Tyler Ohlsen * Updated MAINTAINERS.md to match recommended opensearch-project format. (#388) Signed-off-by: dblock * Add windows env to integration test workflow (#390) Signed-off-by: Tyler Ohlsen * Bump json5 to 2.2.3 (#393) Signed-off-by: Tyler Ohlsen * Add 2.5 release notes (#395) Signed-off-by: Tyler Ohlsen * Update cold start message (#398) Signed-off-by: Kaituo Li * upgrade filter bug (#402) Previously, we didn't actually add filter type when loading old detector. This PR fixed that. Testing done: 1. added a unit tes 2. verified e2et Signed-off-by: Kaituo Li * Changed required minimum intervals in cold start message (#411) Required minimum intervals to finish cold start is related to shingle size. This PR adds the shingle size in the computation and puts the result in the cold start message. Testing done: 1. verified changing shingle size will change the message. Signed-off-by: Kaituo Li * Bump @sideway/formula to 3.0.1 (#418) Signed-off-by: Tyler Ohlsen * Remove auto_expand_replicas override in sample data indices (#423) Signed-off-by: Tyler Ohlsen * Created untriaged issue workflow. (#422) Signed-off-by: dblock * Add 2.6 release notes (#429) Signed-off-by: Tyler Ohlsen * Fix Node.js and Yarn installation in CI (#433) Signed-off-by: Miki * run prettier command against all files (#444) Signed-off-by: Jackie Han * Add 2.7 release notes (#456) Signed-off-by: Jackie Han * updating maintainers and code owners (#476) Signed-off-by: Amit Galitzky * fixing test to pass with node 18 (#491) Signed-off-by: Amit Galitzky --------- Signed-off-by: Tyler Ohlsen Signed-off-by: Amit Galitzky Signed-off-by: Jackie Han Signed-off-by: dblock Signed-off-by: Kaituo Li Signed-off-by: Miki Co-authored-by: Tyler Ohlsen Co-authored-by: Jackie Han Co-authored-by: Daniel (dB.) Doubrovkine Co-authored-by: Kaituo Li Co-authored-by: Miki --- .github/CODEOWNERS | 3 +-- MAINTAINERS.md | 15 ++++++++++++--- .../__tests__/NameAndDescription.test.tsx | 10 +++++++--- ...-detection-dashboards.release-notes-2.7.0.0.md | 7 +++++++ 4 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 release-notes/opensearch-anomaly-detection-dashboards.release-notes-2.7.0.0.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cd025f06..48711d5f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1 @@ -# This should match the owning team set up in https://github.com/orgs/opensearch-project/teams -* @opensearch-project/anomaly-detection \ No newline at end of file +* @ohltyler @kaituo @jackiehanyang @amitgalitz @sean-zheng-amazon \ No newline at end of file diff --git a/MAINTAINERS.md b/MAINTAINERS.md index bafb14f5..6a406fb4 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -4,9 +4,18 @@ This document contains a list of maintainers in this repo. See [opensearch-proje ## Current Maintainers -| Maintainer | GitHub ID | Affiliation | -| ----------------------- | ------------------------------------------------------- | ----------- | -| Tyler Ohlsen | [ohltyler](https://github.com/ohltyler) | Amazon | +| Maintainer | GitHub ID | Affiliation | +| ----------------------- | -------------------------------------------------------- | ----------- | +| Tyler Ohlsen | [ohltyler](https://github.com/ohltyler) | Amazon | +| Kaituo Li | [kaituo](https://github.com/kaituo) | Amazon | +| Jackie Han | [jackiehanyang](https://github.com/jackiehanyang) | Amazon | +| Amit Galitzky | [amitgalitz](https://github.com/amitgalitz) | Amazon | +| Sean Zheng | [sean-zheng-amazon](https://github.com/sean-zheng-amazon)| Amazon | + +## Emeritus Maintainers + +| Maintainer | GitHub ID | Affiliation | +| ----------------- | ------------------------------------------------------- | ----------- | | Yaliang | [ylwu-amzn](https://github.com/ylwu-amzn) | Amazon | | Yizhe Liu | [yizheliu-amazon](https://github.com/yizheliu-amazon) | Amazon | | Vijayan Balasubramanian | [VijayanB](https://github.com/VijayanB) | Amazon | diff --git a/public/pages/DefineDetector/components/NameAndDescription/__tests__/NameAndDescription.test.tsx b/public/pages/DefineDetector/components/NameAndDescription/__tests__/NameAndDescription.test.tsx index 32b564fa..dbd5908c 100644 --- a/public/pages/DefineDetector/components/NameAndDescription/__tests__/NameAndDescription.test.tsx +++ b/public/pages/DefineDetector/components/NameAndDescription/__tests__/NameAndDescription.test.tsx @@ -29,11 +29,14 @@ describe(' spec', () => { expect(container.firstChild).toMatchSnapshot(); }); test('shows error for detector name input when toggling focus/blur', async () => { - const handleValidateName = jest.fn().mockImplementation(() => { - throw 'Required'; + const handleValidateName = jest.fn().mockImplementationOnce(() => { + return 'Required'; }); const { queryByText, findByText, getByPlaceholderText } = render( - + {() => (
@@ -49,6 +52,7 @@ describe(' spec', () => { expect(handleValidateName).toHaveBeenCalledTimes(1); expect(findByText('Required')).not.toBeNull(); }); + test('shows error for detector description input when toggling focus/bur', async () => { const { queryByText, findByText, getByPlaceholderText } = render( Date: Wed, 7 Jun 2023 17:22:37 -0700 Subject: [PATCH 27/43] Unit tests for expression function and additional components (#503) * adding UT for expression function and some components Signed-off-by: Amit Galitzky * moved helper functions to separate files, cleaned up other tests Signed-off-by: Amit Galitzky * custom result bug fix along with a few others Signed-off-by: Amit Galitzky * revert historical boolean Signed-off-by: Amit Galitzky * add pluginEventType when no error Signed-off-by: Amit Galitzky --------- Signed-off-by: Amit Galitzky --- .../ConfirmUnlinkDetectorModal.tsx | 6 +- .../ConfirmUnlinkDetectorModal.test.tsx | 69 +++++++ .../EmptyAssociatedDetectorMessage.test.tsx | 40 ++++ ...ptyAssociatedDetectorMessage.test.tsx.snap | 69 +++++++ .../containers/AssociatedDetectors.tsx | 8 +- .../AssociatedDetectors/styles.scss | 4 + .../__tests__/overlay_anomalies.test.ts | 152 +++++++++++++++ public/expressions/constants.ts | 4 + public/expressions/helpers.ts | 139 ++++++++++++++ public/expressions/overlay_anomalies.ts | 112 ++--------- .../__tests__/AnomaliesLiveCharts.test.tsx | 4 +- .../Dashboard/utils/__tests__/utils.test.tsx | 18 +- .../__tests__/anomalyResultUtils.test.ts | 18 +- public/pages/utils/__tests__/constants.ts | 177 +++++++++++++++++- public/utils/contextMenu/getActions.tsx | 4 +- 15 files changed, 698 insertions(+), 126 deletions(-) create mode 100644 public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/__tests__/ConfirmUnlinkDetectorModal.test.tsx create mode 100644 public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/__tests__/EmptyAssociatedDetectorMessage.test.tsx create mode 100644 public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/__tests__/__snapshots__/EmptyAssociatedDetectorMessage.test.tsx.snap create mode 100644 public/expressions/__tests__/overlay_anomalies.test.ts create mode 100644 public/expressions/helpers.ts diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/ConfirmUnlinkDetectorModal/ConfirmUnlinkDetectorModal.tsx b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/ConfirmUnlinkDetectorModal/ConfirmUnlinkDetectorModal.tsx index 25687ed5..98d5d155 100644 --- a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/ConfirmUnlinkDetectorModal/ConfirmUnlinkDetectorModal.tsx +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/ConfirmUnlinkDetectorModal/ConfirmUnlinkDetectorModal.tsx @@ -34,7 +34,7 @@ export const ConfirmUnlinkDetectorModal = ( return ( @@ -52,14 +52,14 @@ export const ConfirmUnlinkDetectorModal = ( {isLoading ? null : ( Cancel )} { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const testDetectors = [ + { + id: 'detectorId1', + name: 'test-detector-1', + }, + { + id: 'detectorId2', + name: 'test-detector-2', + }, + ] as DetectorListItem[]; + + const ConfirmUnlinkDetectorModalProps = { + detector: testDetectors[0], + onHide: jest.fn(), + onConfirm: jest.fn(), + onUnlinkDetector: jest.fn(), + isListLoading: false, + }; + + test('renders the component correctly', () => { + const { container, getByText } = render( + + ); + getByText('Remove association?'); + getByText( + 'Removing association unlinks test-detector-1 detector from the visualization but does not delete it. The detector association can be restored.' + ); + }); + test('should call onConfirm() when closing', async () => { + const { container, getByText, getByTestId } = render( + + ); + getByText('Remove association?'); + userEvent.click(getByTestId('confirmUnlinkButton')); + expect(ConfirmUnlinkDetectorModalProps.onConfirm).toHaveBeenCalled(); + }); + test('should call onConfirm() when closing', async () => { + const { container, getByText, getByTestId } = render( + + ); + getByText('Remove association?'); + userEvent.click(getByTestId('confirmUnlinkButton')); + expect(ConfirmUnlinkDetectorModalProps.onConfirm).toHaveBeenCalled(); + }); + test('should call onHide() when closing', async () => { + const { getByTestId } = render( + + ); + userEvent.click(getByTestId('cancelUnlinkButton')); + expect(ConfirmUnlinkDetectorModalProps.onHide).toHaveBeenCalled(); + }); +}); diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/__tests__/EmptyAssociatedDetectorMessage.test.tsx b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/__tests__/EmptyAssociatedDetectorMessage.test.tsx new file mode 100644 index 00000000..21b684be --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/__tests__/EmptyAssociatedDetectorMessage.test.tsx @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { findAllByTestId, render, waitFor } from '@testing-library/react'; +import { EmptyAssociatedDetectorMessage } from '../index'; +import { getRandomDetector } from '../../../../../../public/redux/reducers/__tests__/utils'; +import { DetectorListItem } from '../../../../../../public/models/interfaces'; +import userEvent from '@testing-library/user-event'; + +describe('ConfirmUnlinkDetectorModal spec', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('renders the component with filter applied', () => { + const { container, getByText } = render( + + ); + getByText('There are no detectors matching your search'); + expect(container).toMatchSnapshot(); + }); + test('renders the component with filter applied', () => { + const { container, getByText } = render( + + ); + getByText( + 'There are no anomaly detectors associated with test-title visualization.' + ); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/__tests__/__snapshots__/EmptyAssociatedDetectorMessage.test.tsx.snap b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/__tests__/__snapshots__/EmptyAssociatedDetectorMessage.test.tsx.snap new file mode 100644 index 00000000..15c1a6c3 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/__tests__/__snapshots__/EmptyAssociatedDetectorMessage.test.tsx.snap @@ -0,0 +1,69 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ConfirmUnlinkDetectorModal spec renders the component with filter applied 1`] = ` +
+
+

+ No anomaly detectors to display +

+ +
+
+
+

+ There are no detectors matching your search +

+
+
+ +
+
+`; + +exports[`ConfirmUnlinkDetectorModal spec renders the component with filter applied 2`] = ` +
+
+

+ No anomaly detectors to display +

+ +
+
+
+

+ There are no anomaly detectors associated with test-title visualization. +

+
+
+ +
+
+`; diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx index 69f299cc..0829f4ec 100644 --- a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx @@ -35,7 +35,6 @@ import { prettifyErrorMessage, NO_PERMISSIONS_KEY_WORD, } from '../../../../../server/utils/helpers'; -import { SavedObjectLoader } from '../../../../../../../src/plugins/saved_objects/public'; import { EmptyAssociatedDetectorMessage, ConfirmUnlinkDetectorModal, @@ -138,9 +137,6 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) { getAugmentVisSavedObjs(embeddable.vis.id, savedObjectLoader, uiSettings) .then((savedAugmentObjectsArr: any) => { if (savedAugmentObjectsArr != undefined) { - console.log( - 'savedAugmentObjectsArr: ' + JSON.stringify(savedAugmentObjectsArr) - ); const curSelectedDetectors = getAssociatedDetectors( Object.values(allDetectors), savedAugmentObjectsArr @@ -287,7 +283,7 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) {
- +

Associated anomaly detectors

@@ -306,7 +302,7 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) { -

{embeddableTitle}

+

Visualization: {embeddableTitle}

diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/styles.scss b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/styles.scss index 6598f00e..0c3fe230 100644 --- a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/styles.scss +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/styles.scss @@ -16,4 +16,8 @@ &__flex-group { height: 100%; } + + &__associate-button { + flex: 0 0 auto; + } } diff --git a/public/expressions/__tests__/overlay_anomalies.test.ts b/public/expressions/__tests__/overlay_anomalies.test.ts new file mode 100644 index 00000000..d55ef2e4 --- /dev/null +++ b/public/expressions/__tests__/overlay_anomalies.test.ts @@ -0,0 +1,152 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { setClient } from '../../services'; +import { httpClientMock } from '../../../test/mocks'; +import { + convertAnomaliesToPointInTimeEventsVisLayer, + getAnomalies, + getVisLayerError, + getDetectorResponse, +} from '../helpers'; +import { + ANOMALY_RESULT_SUMMARY, + ANOMALY_RESULT_SUMMARY_DETECTOR_ID, + NO_ANOMALIES_RESULT_RESPONSE, + PARSED_ANOMALIES, + SELECTED_DETECTORS, +} from '../../pages/utils/__tests__/constants'; +import { + DETECTOR_HAS_BEEN_DELETED, + START_OR_END_TIME_INVALID_ERROR, + VIS_LAYER_PLUGIN_TYPE, +} from '../constants'; +import { PLUGIN_NAME } from '../../utils/constants'; +import { VisLayerErrorTypes } from '../../../../../src/plugins/vis_augmenter/public'; +import { DOES_NOT_HAVE_PERMISSIONS_KEY_WORD } from '../../../server/utils/helpers'; + +describe('overlay_anomalies spec', () => { + setClient(httpClientMock); + + const ADPluginResource = { + type: VIS_LAYER_PLUGIN_TYPE, + id: ANOMALY_RESULT_SUMMARY_DETECTOR_ID, + name: 'test-1', + urlPath: `${PLUGIN_NAME}#/detectors/${ANOMALY_RESULT_SUMMARY_DETECTOR_ID}/results`, //details page for detector in AD plugin + }; + + describe('getAnomalies()', () => { + test('One anomaly', async () => { + httpClientMock.post = jest.fn().mockResolvedValue(ANOMALY_RESULT_SUMMARY); + const receivedAnomalies = await getAnomalies( + ANOMALY_RESULT_SUMMARY_DETECTOR_ID, + 1589258564789, + 1589258684789, + '' + ); + expect(receivedAnomalies).toStrictEqual(PARSED_ANOMALIES); + }); + test('No Anomalies', async () => { + httpClientMock.post = jest + .fn() + .mockResolvedValue(NO_ANOMALIES_RESULT_RESPONSE); + const receivedAnomalies = await getAnomalies( + ANOMALY_RESULT_SUMMARY_DETECTOR_ID, + 1589258564789, + 1589258684789, + '' + ); + expect(receivedAnomalies).toStrictEqual([]); + }); + test('Failed response', async () => { + httpClientMock.post = jest.fn().mockResolvedValue({ ok: false }); + const receivedAnomalies = await getAnomalies( + ANOMALY_RESULT_SUMMARY_DETECTOR_ID, + 1589258564789, + 1589258684789, + '' + ); + expect(receivedAnomalies).toStrictEqual([]); + }); + }); + describe('getDetectorResponse()', () => { + test('get detector', async () => { + httpClientMock.get = jest + .fn() + .mockResolvedValue({ ok: true, response: SELECTED_DETECTORS[0] }); + const receivedAnomalies = await getDetectorResponse( + 'gtU2l4ABuV34PY9ITTdm' + ); + expect(receivedAnomalies).toStrictEqual({ + ok: true, + response: SELECTED_DETECTORS[0], + }); + }); + }); + describe('convertAnomaliesToPointInTimeEventsVisLayer()', () => { + test('convert anomalies to PointInTimeEventsVisLayer', async () => { + const expectedTimeStamp = + PARSED_ANOMALIES[0].startTime + + (PARSED_ANOMALIES[0].endTime - PARSED_ANOMALIES[0].startTime) / 2; + const expectedPointInTimeEventsVisLayer = { + events: [ + { + metadata: {}, + timestamp: expectedTimeStamp, + }, + ], + originPlugin: 'anomalyDetectionDashboards', + pluginResource: { + id: ANOMALY_RESULT_SUMMARY_DETECTOR_ID, + name: 'test-1', + type: 'Anomaly Detectors', + urlPath: `anomaly-detection-dashboards#/detectors/${ANOMALY_RESULT_SUMMARY_DETECTOR_ID}/results`, + }, + type: 'PointInTimeEvents', + }; + const pointInTimeEventsVisLayer = + await convertAnomaliesToPointInTimeEventsVisLayer( + PARSED_ANOMALIES, + ADPluginResource + ); + expect(pointInTimeEventsVisLayer).toStrictEqual( + expectedPointInTimeEventsVisLayer + ); + }); + }); + describe('getErrorLayerVisLayer()', () => { + test('get resource deleted ErrorVisLayer', async () => { + const error = new Error( + 'Anomaly Detector - ' + DETECTOR_HAS_BEEN_DELETED + ); + const expectedVisLayerError = { + type: VisLayerErrorTypes.RESOURCE_DELETED, + message: error.message, + }; + const visLayerError = await getVisLayerError(error); + expect(visLayerError).toStrictEqual(expectedVisLayerError); + }); + test('get no permission ErrorVisLayer', async () => { + const error = new Error( + 'Anomaly Detector - ' + DOES_NOT_HAVE_PERMISSIONS_KEY_WORD + ); + const expectedVisLayerError = { + type: VisLayerErrorTypes.PERMISSIONS_FAILURE, + message: error.message, + }; + const visLayerError = await getVisLayerError(error); + expect(visLayerError).toStrictEqual(expectedVisLayerError); + }); + test('get fetch issue ErrorVisLayer', async () => { + const error = new Error(START_OR_END_TIME_INVALID_ERROR); + const expectedVisLayerError = { + type: VisLayerErrorTypes.FETCH_FAILURE, + message: error.message, + }; + const visLayerError = await getVisLayerError(error); + expect(visLayerError).toStrictEqual(expectedVisLayerError); + }); + }); +}); diff --git a/public/expressions/constants.ts b/public/expressions/constants.ts index 41a79276..066795c0 100644 --- a/public/expressions/constants.ts +++ b/public/expressions/constants.ts @@ -13,3 +13,7 @@ export const TYPE_OF_EXPR_VIS_LAYERS = 'vis_layers'; export const OVERLAY_ANOMALIES = 'overlay_anomalies'; export const PLUGIN_EVENT_TYPE = 'Anomalies'; + +export const DETECTOR_HAS_BEEN_DELETED = 'detector has been deleted'; + +export const START_OR_END_TIME_INVALID_ERROR = 'start or end time invalid'; diff --git a/public/expressions/helpers.ts b/public/expressions/helpers.ts new file mode 100644 index 00000000..ed3db94b --- /dev/null +++ b/public/expressions/helpers.ts @@ -0,0 +1,139 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + getAnomalySummaryQuery, + parsePureAnomalies, +} from '../pages/utils/anomalyResultUtils'; +import { AD_NODE_API } from '../../utils/constants'; +import { AnomalyData } from '../models/interfaces'; +import { getClient } from '../services'; +import { + PluginResource, + PointInTimeEventsVisLayer, + VisLayerError, + VisLayerErrorTypes, + VisLayerTypes, +} from '../../../../src/plugins/vis_augmenter/public'; +import { + DETECTOR_HAS_BEEN_DELETED, + ORIGIN_PLUGIN_VIS_LAYER, + PLUGIN_EVENT_TYPE, +} from './constants'; +import { + DOES_NOT_HAVE_PERMISSIONS_KEY_WORD, + NO_PERMISSIONS_KEY_WORD, +} from '../../server/utils/helpers'; +import { get } from 'lodash'; + +// This gets all the needed anomalies for the given detector ID and time range +export const getAnomalies = async ( + detectorId: string, + startTime: number, + endTime: number, + resultIndex: string +): Promise => { + const anomalySummaryQuery = getAnomalySummaryQuery( + startTime, + endTime, + detectorId, + undefined, + false + ); + let anomalySummaryResponse; + if (resultIndex === '') { + anomalySummaryResponse = await getClient().post( + `..${AD_NODE_API.DETECTOR}/results/_search`, + { + body: JSON.stringify(anomalySummaryQuery), + } + ); + } else { + anomalySummaryResponse = await getClient().post( + `..${AD_NODE_API.DETECTOR}/results/_search/${resultIndex}/true`, + { + body: JSON.stringify(anomalySummaryQuery), + } + ); + } + + return parsePureAnomalies(anomalySummaryResponse); +}; + +export const getDetectorResponse = async (detectorId: string) => { + const resp = await getClient().get(`..${AD_NODE_API.DETECTOR}/${detectorId}`); + return resp; +}; + +// This takes anomalies and returns them as vis layer of type PointInTimeEvents +export const convertAnomaliesToPointInTimeEventsVisLayer = ( + anomalies: AnomalyData[], + ADPluginResource: PluginResource +): PointInTimeEventsVisLayer => { + const events = anomalies.map((anomaly: AnomalyData) => { + return { + timestamp: anomaly.startTime + (anomaly.endTime - anomaly.startTime) / 2, + metadata: {}, + }; + }); + return { + originPlugin: ORIGIN_PLUGIN_VIS_LAYER, + type: VisLayerTypes.PointInTimeEvents, + pluginResource: ADPluginResource, + events: events, + pluginEventType: PLUGIN_EVENT_TYPE + } as PointInTimeEventsVisLayer; +}; + +const checkIfPermissionErrors = (error): boolean => { + return typeof error === 'string' + ? error.includes(NO_PERMISSIONS_KEY_WORD) || + error.includes(DOES_NOT_HAVE_PERMISSIONS_KEY_WORD) + : get(error, 'message', '').includes(NO_PERMISSIONS_KEY_WORD) || + get(error, 'message', '').includes(DOES_NOT_HAVE_PERMISSIONS_KEY_WORD); +}; + +const checkIfDeletionErrors = (error): boolean => { + return typeof error === 'string' + ? error.includes(DETECTOR_HAS_BEEN_DELETED) + : get(error, 'message', '').includes(DETECTOR_HAS_BEEN_DELETED); +}; + +//Helps convert any possible errors into either permission, deletion or fetch related failures +export const getVisLayerError = (error): VisLayerError => { + let visLayerError: VisLayerError = {} as VisLayerError; + if (checkIfPermissionErrors(error)) { + visLayerError = { + type: VisLayerErrorTypes.PERMISSIONS_FAILURE, + message: + error === 'string' + ? error + : error instanceof Error + ? get(error, 'message', '') + : '', + }; + } else if (checkIfDeletionErrors(error)) { + visLayerError = { + type: VisLayerErrorTypes.RESOURCE_DELETED, + message: + error === 'string' + ? error + : error instanceof Error + ? get(error, 'message', '') + : '', + }; + } else { + visLayerError = { + type: VisLayerErrorTypes.FETCH_FAILURE, + message: + error === 'string' + ? error + : error instanceof Error + ? get(error, 'message', '') + : '', + }; + } + return visLayerError; +}; diff --git a/public/expressions/overlay_anomalies.ts b/public/expressions/overlay_anomalies.ts index 3ff8c128..0df420b2 100644 --- a/public/expressions/overlay_anomalies.ts +++ b/public/expressions/overlay_anomalies.ts @@ -3,51 +3,43 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { get, isEmpty } from 'lodash'; +import { get } from 'lodash'; import { i18n } from '@osd/i18n'; import { ExpressionFunctionDefinition } from '../../../../src/plugins/expressions/public'; import { VisLayerTypes, VisLayers, ExprVisLayers, - PluginResource, } from '../../../../src/plugins/vis_augmenter/public'; import { TimeRange, calculateBounds, } from '../../../../src/plugins/data/common'; -import { - getAnomalySummaryQuery, - parsePureAnomalies, -} from '../pages/utils/anomalyResultUtils'; -import { AD_NODE_API } from '../../utils/constants'; -import { AnomalyData } from '../models/interfaces'; -import { getClient } from '../services'; -import { - PointInTimeEventsVisLayer, - VisLayerError, - VisLayerErrorTypes, -} from '../../../../src/plugins/vis_augmenter/public'; +import { PointInTimeEventsVisLayer } from '../../../../src/plugins/vis_augmenter/public'; import { PLUGIN_NAME } from '../utils/constants'; import { CANT_FIND_KEY_WORD, DOES_NOT_HAVE_PERMISSIONS_KEY_WORD, - NO_PERMISSIONS_KEY_WORD, } from '../../server/utils/helpers'; import { - ORIGIN_PLUGIN_VIS_LAYER, + DETECTOR_HAS_BEEN_DELETED, OVERLAY_ANOMALIES, PLUGIN_EVENT_TYPE, + START_OR_END_TIME_INVALID_ERROR, TYPE_OF_EXPR_VIS_LAYERS, VIS_LAYER_PLUGIN_TYPE, } from './constants'; +import { + convertAnomaliesToPointInTimeEventsVisLayer, + getAnomalies, + getDetectorResponse, + getVisLayerError, +} from './helpers'; type Input = ExprVisLayers; type Output = Promise; type Name = typeof OVERLAY_ANOMALIES; -const DETECTOR_HAS_BEEN_DELETED = 'detector has been deleted'; - interface Arguments { detectorId: string; } @@ -55,54 +47,6 @@ interface Arguments { export type OverlayAnomaliesExpressionFunctionDefinition = ExpressionFunctionDefinition; -// This gets all the needed anomalies for the given detector ID and time range -const getAnomalies = async ( - detectorId: string, - startTime: number, - endTime: number -): Promise => { - const anomalySummaryQuery = getAnomalySummaryQuery( - startTime, - endTime, - detectorId, - undefined, - false - ); - - const anomalySummaryResponse = await getClient().post( - `..${AD_NODE_API.DETECTOR}/results/_search`, - { - body: JSON.stringify(anomalySummaryQuery), - } - ); - - return parsePureAnomalies(anomalySummaryResponse); -}; - -const getDetectorResponse = async (detectorId: string) => { - const resp = await getClient().get(`..${AD_NODE_API.DETECTOR}/${detectorId}`); - return resp; -}; - -// This takes anomalies and returns them as vis layer of type PointInTimeEvents -const convertAnomaliesToPointInTimeEventsVisLayer = ( - anomalies: AnomalyData[], - ADPluginResource: PluginResource -): PointInTimeEventsVisLayer => { - const events = anomalies.map((anomaly: AnomalyData) => { - return { - timestamp: anomaly.startTime + (anomaly.endTime - anomaly.startTime) / 2, - metadata: {}, - }; - }); - return { - originPlugin: ORIGIN_PLUGIN_VIS_LAYER, - type: VisLayerTypes.PointInTimeEvents, - pluginResource: ADPluginResource, - events: events, - } as PointInTimeEventsVisLayer; -}; - /* * This function defines the Anomaly Detection expression function of type vis_layers. * The expression-fn defined takes an argument of detectorId and an array of VisLayers as input, @@ -170,18 +114,20 @@ export const overlayAnomaliesFunction = throw new Error(get(detectorResponse, 'error', '')); } const detectorName = get(detectorResponse.response, 'name', ''); + const resultIndex = get(detectorResponse.response, 'resultIndex', ''); if (detectorName === '') { throw new Error('Anomaly Detector - Unable to get detector'); } ADPluginResource.name = detectorName; if (startTimeInMillis === undefined || endTimeInMillis === undefined) { - throw new RangeError('start or end time invalid'); + throw new RangeError(START_OR_END_TIME_INVALID_ERROR); } const anomalies = await getAnomalies( detectorId, startTimeInMillis, - endTimeInMillis + endTimeInMillis, + resultIndex ); const anomalyLayer = convertAnomaliesToPointInTimeEventsVisLayer( anomalies, @@ -195,35 +141,7 @@ export const overlayAnomaliesFunction = }; } catch (error) { console.error('Anomaly Detector - Unable to get anomalies: ', error); - let visLayerError: VisLayerError = {} as VisLayerError; - if ( - typeof error === 'string' && - (error.includes(NO_PERMISSIONS_KEY_WORD) || - error.includes(DOES_NOT_HAVE_PERMISSIONS_KEY_WORD)) - ) { - visLayerError = { - type: VisLayerErrorTypes.PERMISSIONS_FAILURE, - message: error, - }; - } else if ( - typeof error === 'string' && - error.includes(DETECTOR_HAS_BEEN_DELETED) - ) { - visLayerError = { - type: VisLayerErrorTypes.RESOURCE_DELETED, - message: error, - }; - } else { - visLayerError = { - type: VisLayerErrorTypes.FETCH_FAILURE, - message: - error === 'string' - ? error - : error instanceof Error - ? error.message - : '', - }; - } + const visLayerError = getVisLayerError(error); const anomalyErrorLayer = { type: VisLayerTypes.PointInTimeEvents, originPlugin: PLUGIN_NAME, diff --git a/public/pages/Dashboard/Components/__tests__/AnomaliesLiveCharts.test.tsx b/public/pages/Dashboard/Components/__tests__/AnomaliesLiveCharts.test.tsx index 5c497688..23f65912 100644 --- a/public/pages/Dashboard/Components/__tests__/AnomaliesLiveCharts.test.tsx +++ b/public/pages/Dashboard/Components/__tests__/AnomaliesLiveCharts.test.tsx @@ -6,7 +6,7 @@ import { render, waitFor } from '@testing-library/react'; import React from 'react'; import { AnomaliesLiveChart } from '../AnomaliesLiveChart'; -import { selectedDetectors } from '../../../../pages/utils/__tests__/constants'; +import { SELECTED_DETECTORS } from '../../../../pages/utils/__tests__/constants'; import { Provider } from 'react-redux'; import { coreServicesMock } from '../../../../../test/mocks'; import { CoreServicesContext } from '../../../../components/CoreServices/CoreServices'; @@ -45,7 +45,7 @@ describe(' spec', () => { const { container, getByTestId, getAllByText, getByText } = render( - + ); diff --git a/public/pages/Dashboard/utils/__tests__/utils.test.tsx b/public/pages/Dashboard/utils/__tests__/utils.test.tsx index 7694dea8..0f805217 100644 --- a/public/pages/Dashboard/utils/__tests__/utils.test.tsx +++ b/public/pages/Dashboard/utils/__tests__/utils.test.tsx @@ -8,16 +8,10 @@ import { getLatestAnomalyResultsByTimeRange, getLatestAnomalyResultsForDetectorsByTimeRange, } from '../utils'; -import { httpClientMock, coreServicesMock } from '../../../../../test/mocks'; import { - Detector, - FeatureAttributes, - DetectorListItem, -} from '../../../../models/interfaces'; -import { - selectedDetectors, - anomalyResultQuery, - anomalyResultQueryPerDetector, + SELECTED_DETECTORS, + ANOMALY_RESULT_QUERY, + ANOMALY_RESULT_QUERY_PER_DETECTOR, } from '../../../../pages/utils/__tests__/constants'; const anomalyResult = { detector_id: 'gtU2l4ABuV34PY9ITTdm', @@ -114,14 +108,14 @@ describe('get latest anomaly result by time range', () => { 'opensearch-ad-plugin-result-*', false ); - expect(response[0]).toStrictEqual(anomalyResultQuery); + expect(response[0]).toStrictEqual(ANOMALY_RESULT_QUERY); }, 10000); }); describe('get latest anomaly result for detectors', () => { test('get latest by detectors and time range ', async () => { const response = await getLatestAnomalyResultsForDetectorsByTimeRange( jest.fn(), - selectedDetectors, + SELECTED_DETECTORS, '30m', jest.fn().mockResolvedValue(searchResponseGetLatestAnomalyResults), -1, @@ -131,6 +125,6 @@ describe('get latest anomaly result for detectors', () => { 'opensearch-ad-plugin-result-*', false ); - expect(response[0]).toStrictEqual(anomalyResultQueryPerDetector); + expect(response[0]).toStrictEqual(ANOMALY_RESULT_QUERY_PER_DETECTOR); }, 10000); }); diff --git a/public/pages/utils/__tests__/anomalyResultUtils.test.ts b/public/pages/utils/__tests__/anomalyResultUtils.test.ts index 65ebc8d6..914c4c1f 100644 --- a/public/pages/utils/__tests__/anomalyResultUtils.test.ts +++ b/public/pages/utils/__tests__/anomalyResultUtils.test.ts @@ -12,9 +12,16 @@ import { getFeatureMissingDataAnnotations, getFeatureDataPointsForDetector, + parsePureAnomalies, } from '../anomalyResultUtils'; import { getRandomDetector } from '../../../redux/reducers/__tests__/utils'; -import { UNITS, Detector, FeatureAttributes } from '../../../models/interfaces'; +import { + UNITS, + Detector, + FeatureAttributes, + AnomalyData, +} from '../../../models/interfaces'; +import { ANOMALY_RESULT_SUMMARY, PARSED_ANOMALIES } from './constants'; describe('anomalyResultUtils', () => { let randomDetector_20_min: Detector; @@ -563,4 +570,13 @@ describe('anomalyResultUtils', () => { ).toEqual([]); }); }); + + describe('parsePureAnomalies()', () => { + test('parse anomalies', async () => { + const parsedPureAnomalies: AnomalyData[] = await parsePureAnomalies( + ANOMALY_RESULT_SUMMARY + ); + expect(parsedPureAnomalies).toStrictEqual(PARSED_ANOMALIES); + }); + }); }); diff --git a/public/pages/utils/__tests__/constants.ts b/public/pages/utils/__tests__/constants.ts index a43dd4bc..e5c3d916 100644 --- a/public/pages/utils/__tests__/constants.ts +++ b/public/pages/utils/__tests__/constants.ts @@ -71,20 +71,20 @@ export const FAKE_ENTITY_ANOMALY_SUMMARIES = { anomalySummaries: [FAKE_ENTITY_ANOMALY_SUMMARY], } as EntityAnomalySummaries; -export const anomalyResultQuery = { +export const ANOMALY_RESULT_QUERY = { anomaly_grade: 0.10949221682655441, data_start_time: 1651817250642, data_end_time: 1651817310642, detector_id: 'gtU2l4ABuV34PY9ITTdm', }; -export const anomalyResultQueryPerDetector = { +export const ANOMALY_RESULT_QUERY_PER_DETECTOR = { anomaly_grade: 0.10949221682655441, data_start_time: 1651817250642, data_end_time: 1651817310642, detector_id: 'gtU2l4ABuV34PY9ITTdm', name: 'test3', }; -export const selectedDetectors = [ +export const SELECTED_DETECTORS = [ { id: 'gtU2l4ABuV34PY9ITTdm', name: 'test2', @@ -134,3 +134,174 @@ export const selectedDetectors = [ lastUpdateTime: 1651818220194, }, ] as DetectorListItem[]; + +export const ANOMALY_RESULT_SUMMARY_DETECTOR_ID: string = + 'hNX8l4ABuV34PY9I1EAZ'; + +export const ANOMALY_RESULT_SUMMARY = { + ok: true, + response: { + took: 1, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 255, + relation: 'eq', + }, + max_score: 0, + hits: [ + { + _index: '.opendistro-anomaly-results-history-2022.05.06-1', + _id: ANOMALY_RESULT_SUMMARY_DETECTOR_ID, + _version: 1, + _seq_no: 2980, + _primary_term: 1, + _score: 0, + _source: { + relevant_attribution: [ + { + feature_id: 'j-fObYgB3BV2P4BXAga2', + data: 0, + }, + { + feature_id: 'kOfObYgB3BV2P4BXAga5', + data: 1, + }, + ], + detector_id: 'gtU2l4ABuV34PY9ITTdm', + data_start_time: 1651817250642, + data_end_time: 1651817310642, + feature_data: [ + { + feature_id: 'j-fObYgB3BV2P4BXAga2', + feature_name: 'sum_http_4xx', + data: 0, + }, + { + feature_id: 'kOfObYgB3BV2P4BXAga5', + feature_name: 'sum_http_5xx', + data: 3, + }, + ], + execution_start_time: 1651817370642, + execution_end_time: 1651817370649, + anomaly_score: 0.44207098120965693, + anomaly_grade: 0.10949221682655441, + confidence: 0.9821335094192676, + }, + expected_values: [ + { + likelihood: 1, + value_list: [ + { + feature_id: 'j-fObYgB3BV2P4BXAga2', + data: 0, + }, + { + feature_id: 'kOfObYgB3BV2P4BXAga5', + data: 0, + }, + ], + }, + ], + }, + ], + }, + aggregations: { + max_confidence: { + value: 0.9669652473591948, + }, + max_anomaly_grade: { + value: 1, + }, + max_data_end_time: { + value: 1685424000000, + value_as_string: '2023-05-30T05:20:00.000Z', + }, + avg_anomaly_grade: { + value: 1, + }, + min_confidence: { + value: 0.41885100904406947, + }, + count_anomalies: { + value: 1, + }, + min_anomaly_grade: { + value: 1, + }, + }, + }, +}; + +export const NO_ANOMALIES_RESULT_RESPONSE = { + ok: true, + response: { + took: 13, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 0, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + aggregations: { + max_confidence: { + value: null, + }, + max_anomaly_grade: { + value: null, + }, + max_data_end_time: { + value: null, + }, + avg_anomaly_grade: { + value: null, + }, + min_confidence: { + value: null, + }, + count_anomalies: { + value: 0, + }, + min_anomaly_grade: { + value: null, + }, + }, + }, +}; + +export const PARSED_ANOMALIES: AnomalyData[] = [ + { + anomalyGrade: 0.11, + confidence: 0.98, + contributions: { + 'j-fObYgB3BV2P4BXAga2': { + attribution: undefined, + name: 'sum_http_4xx', + }, + kOfObYgB3BV2P4BXAga5: { + attribution: 1, + name: 'sum_http_5xx', + }, + }, + endTime: 1651817310642, + entity: undefined, + plotTime: 1651817310642, + startTime: 1651817250642, + }, +]; diff --git a/public/utils/contextMenu/getActions.tsx b/public/utils/contextMenu/getActions.tsx index cccfd399..e10e61b2 100644 --- a/public/utils/contextMenu/getActions.tsx +++ b/public/utils/contextMenu/getActions.tsx @@ -67,10 +67,10 @@ export const getActions = () => { title: i18n.translate( 'dashboard.actions.adMenuItem.associatedAnomalyDetector.displayName', { - defaultMessage: 'Associated anomaly detector', + defaultMessage: 'Associated detectors', } ), - icon: 'gear' as EuiIconType, + icon: 'kqlSelector' as EuiIconType, order: 99, onClick: getOnClick(FLYOUT_MODES.associated), }, From 65676497ed825361beac8af8c31798b53299f0b9 Mon Sep 17 00:00:00 2001 From: Jackie Han Date: Thu, 8 Jun 2023 09:48:25 -0700 Subject: [PATCH 28/43] Fix various bugs (#504) * fix bug bash bugs Signed-off-by: Jackie Han * bug fix Signed-off-by: Jackie Han * yarn prettier Signed-off-by: Jackie Han * bug fix Signed-off-by: Jackie Han * bug fixes Signed-off-by: Jackie Han * clean up code Signed-off-by: Jackie Han * removed unused snapshot Signed-off-by: Jackie Han --------- Signed-off-by: Jackie Han Signed-off-by: Jackie Han --- .../AddAnomalyDetector.tsx | 75 +++++++++++++++---- .../CreateAnomalyDetector/helpers.tsx | 57 ++++++++++---- public/expressions/constants.ts | 6 ++ .../FeatureAccordion/FeatureAccordion.tsx | 15 +++- .../components/FeatureAccordion/styles.scss | 3 + public/utils/contextMenu/getActions.tsx | 4 +- 6 files changed, 128 insertions(+), 32 deletions(-) create mode 100644 public/pages/ConfigureModel/components/FeatureAccordion/styles.scss diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx index 9e490801..766cee4e 100644 --- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx @@ -70,6 +70,7 @@ import { import { focusOnFirstWrongFeature, initialFeatureValue, + validateFeatures, } from '../../../../public/pages/ConfigureModel/utils/helpers'; import { getIndices, @@ -95,6 +96,8 @@ import { ORIGIN_PLUGIN_VIS_LAYER, OVERLAY_ANOMALIES, VIS_LAYER_PLUGIN_TYPE, + PLUGIN_AUGMENTATION_ENABLE_SETTING, + PLUGIN_AUGMENTATION_MAX_OBJECTS_SETTING, } from '../../../../public/expressions/constants'; import { formikToDetectorName, visFeatureListToFormik } from './helpers'; import { AssociateExisting } from './AssociateExisting'; @@ -157,11 +160,55 @@ function AddAnomalyDetector({ const notifications = getNotifications(); const handleValidationAndSubmit = (formikProps) => { - if (!isEmpty(formikProps.errors)) { - focusOnFirstWrongFeature(formikProps.errors, formikProps.setFieldTouched); - notifications.toasts.addDanger('One or more input fields is invalid'); + if (formikProps.values.featureList.length !== 0) { + formikProps.setFieldTouched('featureList', true); + formikProps.validateForm().then(async (errors) => { + if (!isEmpty(errors)) { + focusOnFirstWrongFeature(errors, formikProps.setFieldTouched); + notifications.toasts.addDanger( + 'One or more input fields is invalid.' + ); + } else { + const isAugmentationEnabled = uiSettings.get( + PLUGIN_AUGMENTATION_ENABLE_SETTING + ); + if (!isAugmentationEnabled) { + notifications.toasts.addDanger( + 'Visualization augmentation is disabled, please enable visualization:enablePluginAugmentation.' + ); + } else { + const maxAssociatedCount = uiSettings.get( + PLUGIN_AUGMENTATION_MAX_OBJECTS_SETTING + ); + await savedObjectLoader.findAll().then(async (resp) => { + if (resp !== undefined) { + const savedAugmentObjects = get(resp, 'hits', []); + // gets all the saved object for this visualization + const savedObjectsForThisVisualization = + savedAugmentObjects.filter( + (savedObj) => + get(savedObj, 'visId', '') === embeddable.vis.id + ); + if ( + maxAssociatedCount <= savedObjectsForThisVisualization.length + ) { + notifications.toasts.addDanger( + `Cannot create the detector and associate it to the visualization due to the limit of the max + amount of associated plugin resources (${maxAssociatedCount}) with + ${savedObjectsForThisVisualization.length} associated to the visualization` + ); + } else { + handleSubmit(formikProps); + } + } + }); + } + } + }); } else { - handleSubmit(formikProps); + notifications.toasts.addDanger( + 'One or more features are required.' + ); } }; @@ -203,7 +250,7 @@ function AddAnomalyDetector({ formikProps.setSubmitting(true); try { const detectorToCreate = formikToDetector(formikProps.values); - dispatch(createDetector(detectorToCreate)) + await dispatch(createDetector(detectorToCreate)) .then(async (response) => { dispatch(startDetector(response.response.id)) .then((startDetectorResponse) => {}) @@ -222,7 +269,7 @@ function AddAnomalyDetector({ const augmentVisSavedObjectToCreate: ISavedAugmentVis = getAugmentVisSavedObject(detectorId); - createAugmentVisSavedObject( + await createAugmentVisSavedObject( augmentVisSavedObjectToCreate, savedObjectLoader, uiSettings @@ -408,7 +455,7 @@ function AddAnomalyDetector({ windowDelay: delayValue, shingleSize: 8, filterQuery: { match_all: {} }, - description: '', + description: 'Created based on ' + embeddable.vis.title, resultIndex: undefined, filters: [], featureList: visFeatureListToFormik( @@ -426,6 +473,7 @@ function AddAnomalyDetector({ initialValues={initialDetectorValue} onSubmit={handleSubmit} validateOnChange={true} + validate={validateFeatures} > {(formikProps) => ( <> @@ -532,8 +580,8 @@ function AddAnomalyDetector({ subTitle={

- Detector interval: {intervalValue} minutes; Window - delay: {delayValue} minutes + Detector interval: {intervalValue} minute(s); Window + delay: {delayValue} minute(s)

} @@ -584,7 +632,7 @@ function AddAnomalyDetector({
-

minutes

+

minute(s)

@@ -618,7 +666,7 @@ function AddAnomalyDetector({ -

minutes

+

minute(s)

@@ -788,8 +836,6 @@ function AddAnomalyDetector({ isOpen={accordionsOpen.modelFeatures} onToggle={() => onAccordionToggle('modelFeatures')} > - - {({ push, @@ -811,6 +857,8 @@ function AddAnomalyDetector({ /> ) )} + + +
)}
diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/helpers.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/helpers.tsx index 5c69015c..4a990b67 100644 --- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/helpers.tsx +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/helpers.tsx @@ -1,9 +1,12 @@ -import { dispatch } from 'd3'; -import { matchDetector } from 'public/redux/reducers/ad'; -import { validateDetectorName } from 'public/utils/utils'; +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + import { FEATURE_TYPE } from '../../../../public/models/interfaces'; import { FeaturesFormikValues } from '../../../../public/pages/ConfigureModel/models/interfaces'; -import { find, get, isEmpty, snakeCase } from 'lodash'; +import { find, snakeCase } from 'lodash'; +import { AGGREGATION_TYPES } from '../../../../public/pages/ConfigureModel/utils/constants'; export function visFeatureListToFormik( featureList, @@ -17,7 +20,7 @@ export function visFeatureListToFormik( featureType: FEATURE_TYPE.SIMPLE, importance: 1, newFeature: false, - aggregationBy: 'sum', + aggregationBy: visAggregationTypeToFormik(feature), aggregationOf: visAggregationToFormik(feature), aggregationQuery: JSON.stringify( visAggregationQueryToFormik(feature, seriesParams) @@ -44,18 +47,40 @@ const getFeatureNameFromVisParams = (id, seriesParams) => { }; function visAggregationToFormik(value) { - return [ - { - label: value.params.field.name, - type: 'number', - }, - ]; + if (Object.values(value.params).length !== 0) { + return [ + { + label: value.params?.field?.name, + type: value.type, + }, + ]; + } + // for count type of vis, there's no field name in the embeddable-vis schema + return []; } function visAggregationQueryToFormik(value, seriesParams) { - return { - [snakeCase(getFeatureNameFromVisParams(value.id, seriesParams))]: { - sum: { field: value.params.field.name }, - }, - }; + if (Object.values(value.params).length !== 0) { + return { + [snakeCase(getFeatureNameFromVisParams(value.id, seriesParams))]: { + [visAggregationTypeToFormik(value)]: { + field: value.params?.field?.name, + }, + }, + }; + } + // for count type of vis, there's no field name in the embeddable-vis schema + // return '' as the custom expression query + return ''; +} + +function visAggregationTypeToFormik(feature) { + const aggType = feature.__type.name; + if (AGGREGATION_TYPES.some((type) => type.value === aggType)) { + return aggType; + } + if (aggType === 'count') { + return 'value_count'; + } + return 'sum'; } diff --git a/public/expressions/constants.ts b/public/expressions/constants.ts index 066795c0..71d696bc 100644 --- a/public/expressions/constants.ts +++ b/public/expressions/constants.ts @@ -14,6 +14,12 @@ export const OVERLAY_ANOMALIES = 'overlay_anomalies'; export const PLUGIN_EVENT_TYPE = 'Anomalies'; +export const PLUGIN_AUGMENTATION_ENABLE_SETTING = + 'visualization:enablePluginAugmentation'; + +export const PLUGIN_AUGMENTATION_MAX_OBJECTS_SETTING = + 'visualization:enablePluginAugmentation.maxPluginObjects'; + export const DETECTOR_HAS_BEEN_DELETED = 'detector has been deleted'; export const START_OR_END_TIME_INVALID_ERROR = 'start or end time invalid'; diff --git a/public/pages/ConfigureModel/components/FeatureAccordion/FeatureAccordion.tsx b/public/pages/ConfigureModel/components/FeatureAccordion/FeatureAccordion.tsx index f1805858..a819ed8f 100644 --- a/public/pages/ConfigureModel/components/FeatureAccordion/FeatureAccordion.tsx +++ b/public/pages/ConfigureModel/components/FeatureAccordion/FeatureAccordion.tsx @@ -22,6 +22,7 @@ import { EuiCheckbox, EuiButtonIcon, } from '@elastic/eui'; +import './styles.scss'; import { Field, FieldProps } from 'formik'; import { required, @@ -80,6 +81,18 @@ export const FeatureAccordion = (props: FeatureAccordionProps) => { }; const featureButtonContent = (feature: any, index: number) => { + if (props.displayMode === 'flyout') { + return ( +
+ +
+ {feature.featureName ? feature.featureName : 'Add feature'} +
+
+ {showSubtitle ? showFeatureDescription(feature) : null} +
+ ); + } return (
@@ -125,7 +138,7 @@ export const FeatureAccordion = (props: FeatureAccordionProps) => { buttonClassName={ props.index === 0 ? 'euiAccordionForm__noTopPaddingButton' - : 'euiAccordionForm__button' + : 'euiFormAccordion_button' } className="euiAccordion__noTopBorder" paddingSize="l" diff --git a/public/pages/ConfigureModel/components/FeatureAccordion/styles.scss b/public/pages/ConfigureModel/components/FeatureAccordion/styles.scss new file mode 100644 index 00000000..5d819b8a --- /dev/null +++ b/public/pages/ConfigureModel/components/FeatureAccordion/styles.scss @@ -0,0 +1,3 @@ +.euiFormAccordion_button { + padding: 20px 16px 0 0; +} diff --git a/public/utils/contextMenu/getActions.tsx b/public/utils/contextMenu/getActions.tsx index e10e61b2..26bf8f06 100644 --- a/public/utils/contextMenu/getActions.tsx +++ b/public/utils/contextMenu/getActions.tsx @@ -21,7 +21,7 @@ import { FLYOUT_MODES } from '../../../public/components/FeatureAnywhereContextM const grouping: Action['grouping'] = [ { id: 'ad-dashboard-context-menu', - getDisplayName: () => 'Anomaly Detector', + getDisplayName: () => 'Anomaly Detection', getIconType: () => APM_TRACE, }, ]; @@ -54,7 +54,7 @@ export const getActions = () => { title: i18n.translate( 'dashboard.actions.adMenuItem.createAnomalyDetector.displayName', { - defaultMessage: 'Create anomaly detector', + defaultMessage: 'Add anomaly detector', } ), icon: 'plusInCircle' as EuiIconType, From b4de09a68e13ed2a165658fc0de490bb52df17a0 Mon Sep 17 00:00:00 2001 From: Amit Galitzky Date: Thu, 8 Jun 2023 14:38:02 -0700 Subject: [PATCH 29/43] Various bug fixes and unit tests for AssociatedDetectors (#505) * associated detectors unit tests Signed-off-by: Amit Galitzky * fixed some bugs and added unit tests for associated detectors Signed-off-by: Amit Galitzky --------- Signed-off-by: Amit Galitzky --- public/action/ad_dashboard_action.tsx | 17 +- .../__tests__/AssociatedDetectors.test.tsx | 389 ++++++++++++++++++ .../AssociatedDetectors/utils/helpers.tsx | 1 + .../AddAnomalyDetector.tsx | 10 +- .../containers/AssociateExisting.tsx | 4 +- .../__tests__/overlay_anomalies.test.ts | 2 + public/expressions/helpers.ts | 2 +- public/services.ts | 11 + 8 files changed, 417 insertions(+), 19 deletions(-) create mode 100644 public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/__tests__/AssociatedDetectors.test.tsx diff --git a/public/action/ad_dashboard_action.tsx b/public/action/ad_dashboard_action.tsx index a845351a..417de3a9 100644 --- a/public/action/ad_dashboard_action.tsx +++ b/public/action/ad_dashboard_action.tsx @@ -14,6 +14,9 @@ import { } from '../../../../src/plugins/ui_actions/public'; import { isReferenceOrValueEmbeddable } from '../../../../src/plugins/embeddable/public'; import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; +import { isEmpty } from 'lodash'; +import { VisualizeEmbeddable } from '../../../../src/plugins/visualizations/public'; +import { isEligibleForVisLayers } from '../../../../src/plugins/vis_augmenter/public'; export const ACTION_AD = 'ad'; @@ -57,15 +60,13 @@ export const createADAction = ({ type: ACTION_AD, grouping, isCompatible: async ({ embeddable }: ActionContext) => { - const paramsType = embeddable.vis?.params?.type; - const seriesParams = embeddable.vis?.params?.seriesParams || []; - const series = embeddable.vis?.params?.series || []; - const isLineGraph = - seriesParams.find((item) => item.type === 'line') || - series.find((item) => item.chart_type === 'line'); - const isValidVis = isLineGraph && paramsType !== 'table'; + const vis = (embeddable as VisualizeEmbeddable).vis; return Boolean( - embeddable.parent && isDashboard(embeddable.parent) && isValidVis + embeddable.parent && + isDashboard(embeddable.parent) && + vis !== undefined && + isEligibleForVisLayers(vis) && + !isEmpty((embeddable as VisualizeEmbeddable).visLayers) ); }, execute: async ({ embeddable }: ActionContext) => { diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/__tests__/AssociatedDetectors.test.tsx b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/__tests__/AssociatedDetectors.test.tsx new file mode 100644 index 00000000..7ee94119 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/__tests__/AssociatedDetectors.test.tsx @@ -0,0 +1,389 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import AssociatedDetectors from '../AssociatedDetectors'; +import { createMockVisEmbeddable } from '../../../../../../../../src/plugins/vis_augmenter/public/mocks'; +import { FLYOUT_MODES } from '../../../../../../public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/constants'; +import { CoreServicesContext } from '../../../../../../public/components/CoreServices/CoreServices'; +import { coreServicesMock, httpClientMock } from '../../../../../../test/mocks'; +import { + HashRouter as Router, + RouteComponentProps, + Route, + Switch, +} from 'react-router-dom'; +import { Provider } from 'react-redux'; +import configureStore from '../../../../../../public/redux/configureStore'; +import { VisualizeEmbeddable } from '../../../../../../../../src/plugins/visualizations/public'; +import { + setSavedFeatureAnywhereLoader, + setUISettings, +} from '../../../../../services'; +import { + generateAugmentVisSavedObject, + VisLayerExpressionFn, + VisLayerTypes, + createSavedAugmentVisLoader, + setUISettings as setVisAugUISettings, + getMockAugmentVisSavedObjectClient, + SavedObjectLoaderAugmentVis, +} from '../../../../../../../../src/plugins/vis_augmenter/public'; +import { getAugmentVisSavedObjs } from '../../../../../../../../src/plugins/vis_augmenter/public/utils'; +import { uiSettingsServiceMock } from '../../../../../../../../src/core/public/mocks'; +import { + PLUGIN_AUGMENTATION_ENABLE_SETTING, + PLUGIN_AUGMENTATION_MAX_OBJECTS_SETTING, +} from '../../../../../../../../src/plugins/vis_augmenter/common'; +import userEvent from '@testing-library/user-event'; +const fn = { + type: VisLayerTypes.PointInTimeEvents, + name: 'test-fn', + args: { + testArg: 'test-value', + }, +} as VisLayerExpressionFn; +const originPlugin = 'test-plugin'; + +const uiSettingsMock = uiSettingsServiceMock.createStartContract(); +setUISettings(uiSettingsMock); +setVisAugUISettings(uiSettingsMock); +const setUIAugSettings = (isEnabled = true, maxCount = 10) => { + uiSettingsMock.get.mockImplementation((key: string) => { + if (key === PLUGIN_AUGMENTATION_MAX_OBJECTS_SETTING) return maxCount; + else if (key === PLUGIN_AUGMENTATION_ENABLE_SETTING) return isEnabled; + else return false; + }); +}; + +setUIAugSettings(); + +jest.mock('../../../../../services', () => ({ + ...jest.requireActual('../../../../../services'), + + getUISettings: () => { + return { + get: (config: string) => { + switch (config) { + case 'visualization:enablePluginAugmentation': + return true; + case 'visualization:enablePluginAugmentation.maxPluginObjects': + return 10; + default: + throw new Error( + `Accessing ${config} is not supported in the mock.` + ); + } + }, + }; + }, + getNotifications: () => { + return { + toasts: { + addDanger: jest.fn().mockName('addDanger'), + addSuccess: jest.fn().mockName('addSuccess'), + }, + }; + }, +})); + +jest.mock( + '../../../../../../../../src/plugins/vis_augmenter/public/utils', + () => ({ + getAugmentVisSavedObjs: jest.fn(), + }) +); +const visEmbeddable = createMockVisEmbeddable( + 'test-saved-obj-id', + 'test-title', + false +); + +const renderWithRouter = (visEmbeddable: VisualizeEmbeddable) => ({ + ...render( + + + + ( + + + + )} + /> + + + + ), +}); +describe('AssociatedDetectors spec', () => { + let augmentVisLoader: SavedObjectLoaderAugmentVis; + let mockDeleteFn: jest.Mock; + let detectorsToAssociate = new Array(2).fill(null).map((_, index) => { + return { + id: `detector_id_${index}`, + name: `detector_name_${index}`, + indices: [`index_${index}`], + totalAnomalies: 5, + lastActiveAnomaly: Date.now() + index, + }; + }); + //change one of the two detectors to have an ID not matching the ID in saved object + detectorsToAssociate[1].id = '5'; + + const savedObjects = new Array(2).fill(null).map((_, index) => { + const pluginResource = { + type: 'test-plugin', + id: `detector_id_${index}`, + }; + return generateAugmentVisSavedObject( + `valid-obj-id-${index}`, + fn, + `vis-id-${index}`, + originPlugin, + pluginResource + ); + }); + beforeEach(() => { + mockDeleteFn = jest.fn().mockResolvedValue('someValue'); + augmentVisLoader = createSavedAugmentVisLoader({ + savedObjectsClient: { + ...getMockAugmentVisSavedObjectClient(savedObjects), + delete: mockDeleteFn, + }, + } as any) as SavedObjectLoaderAugmentVis; + setSavedFeatureAnywhereLoader(augmentVisLoader); + }); + describe('Renders loading component', () => { + test('renders the detector is loading', async () => { + httpClientMock.get = jest.fn().mockResolvedValue({ + ok: true, + response: { detectorList: [], totalDetectors: 0 }, + }); + (getAugmentVisSavedObjs as jest.Mock).mockImplementation(() => + Promise.resolve(savedObjects) + ); + const { getByText } = renderWithRouter(visEmbeddable); + getByText('Loading detectors...'); + getByText('Real-time state'); + getByText('Associate a detector'); + }); + }); + + describe('renders either one or zero detectors', () => { + test('renders one associated detector', async () => { + httpClientMock.get = jest.fn().mockResolvedValue({ + ok: true, + response: { + detectorList: detectorsToAssociate, + totalDetectors: detectorsToAssociate.length, + }, + }); + (getAugmentVisSavedObjs as jest.Mock).mockImplementation(() => + Promise.resolve(savedObjects) + ); + const { getByText, queryByText } = renderWithRouter(visEmbeddable); + getByText('Loading detectors...'); + await waitFor(() => getByText('detector_name_0')); + getByText('5'); + expect(queryByText('detector_name_1')).toBeNull(); + }, 80000); + test('renders no associated detectors', async () => { + httpClientMock.get = jest.fn().mockResolvedValue({ + ok: true, + response: { + detectorList: [detectorsToAssociate[1]], + totalDetectors: 1, + }, + }); + (getAugmentVisSavedObjs as jest.Mock).mockImplementation(() => + Promise.resolve(savedObjects) + ); + const { getByText, findByText } = renderWithRouter(visEmbeddable); + getByText('Loading detectors...'); + await waitFor(() => + findByText( + 'There are no anomaly detectors associated with test-title visualization.', + undefined, + { timeout: 100000 } + ) + ); + }, 150000); + }); + + describe('tests unlink functionality', () => { + test('unlinks a single detector', async () => { + httpClientMock.get = jest.fn().mockResolvedValue({ + ok: true, + response: { + detectorList: detectorsToAssociate, + totalDetectors: detectorsToAssociate.length, + }, + }); + (getAugmentVisSavedObjs as jest.Mock).mockImplementation(() => + Promise.resolve(savedObjects) + ); + const { getByText, queryByText, getAllByTestId } = + renderWithRouter(visEmbeddable); + getByText('Loading detectors...'); + await waitFor(() => getByText('detector_name_0')); + getByText('5'); + expect(queryByText('detector_name_1')).toBeNull(); + userEvent.click(getAllByTestId('unlinkButton')[0]); + await waitFor(() => + getByText( + 'Removing association unlinks detector_name_0 detector from the visualization but does not delete it. The detector association can be restored.' + ) + ); + userEvent.click(getAllByTestId('confirmUnlinkButton')[0]); + expect( + ( + await getAugmentVisSavedObjs( + 'valid-obj-id-0', + augmentVisLoader, + uiSettingsMock + ) + ).length + ).toEqual(2); + await waitFor(() => expect(mockDeleteFn).toHaveBeenCalledTimes(1)); + }, 100000); + }); +}); + +//I have a new beforeEach because I making a lot more detectors and saved objects for these tests +describe('test over 10 associated objects functionality', () => { + let augmentVisLoader: SavedObjectLoaderAugmentVis; + let mockDeleteFn: jest.Mock; + const detectorsToAssociate = new Array(16).fill(null).map((_, index) => { + const hasAnomaly = Math.random() > 0.5; + return { + id: `detector_id_${index}`, + name: `detector_name_${index}`, + indices: [`index_${index}`], + totalAnomalies: hasAnomaly ? Math.floor(Math.random() * 10) : 0, + lastActiveAnomaly: hasAnomaly ? Date.now() + index : 0, + }; + }); + + const savedObjects = new Array(16).fill(null).map((_, index) => { + const pluginResource = { + type: 'test-plugin', + id: `detector_id_${index}`, + }; + return generateAugmentVisSavedObject( + `valid-obj-id-${index}`, + fn, + `vis-id-${index}`, + originPlugin, + pluginResource + ); + }); + beforeEach(() => { + mockDeleteFn = jest.fn().mockResolvedValue('someValue'); + augmentVisLoader = createSavedAugmentVisLoader({ + savedObjectsClient: { + ...getMockAugmentVisSavedObjectClient(savedObjects), + delete: mockDeleteFn, + }, + } as any) as SavedObjectLoaderAugmentVis; + setSavedFeatureAnywhereLoader(augmentVisLoader); + }); + test('create 20 detectors and saved objects', async () => { + httpClientMock.get = jest.fn().mockResolvedValue({ + ok: true, + response: { + detectorList: detectorsToAssociate, + totalDetectors: detectorsToAssociate.length, + }, + }); + (getAugmentVisSavedObjs as jest.Mock).mockImplementation(() => + Promise.resolve(savedObjects) + ); + + const { getByText, queryByText, getAllByTestId, findByText } = + renderWithRouter(visEmbeddable); + + await waitFor(() => + findByText('detector_name_1', undefined, { timeout: 200000 }) + ); + expect(queryByText('detector_name_15')).toBeNull(); + // Navigate to next page + await waitFor(() => + userEvent.click(getAllByTestId('pagination-button-next')[0]) + ); + await waitFor(() => findByText('detector_name_15')); + + expect(queryByText('detector_name_0')).toBeNull(); + // Navigate to previous page + await waitFor(() => + userEvent.click(getAllByTestId('pagination-button-previous')[0]) + ); + getByText('detector_name_0'); + expect(queryByText('detector_name_15')).toBeNull(); + }, 200000); + + test('searching functionality', async () => { + httpClientMock.get = jest.fn().mockResolvedValue({ + ok: true, + response: { + detectorList: detectorsToAssociate, + totalDetectors: detectorsToAssociate.length, + }, + }); + (getAugmentVisSavedObjs as jest.Mock).mockImplementation(() => + Promise.resolve(savedObjects) + ); + + const { queryByText, getByPlaceholderText, findByText } = + renderWithRouter(visEmbeddable); + + // initial load only first 10 detectors + await waitFor(() => + findByText('detector_name_1', undefined, { timeout: 60000 }) + ); + expect(queryByText('detector_name_15')).toBeNull(); + + //Input search event + userEvent.type(getByPlaceholderText('Search...'), 'detector_name_15'); + await waitFor(() => { + findByText('detector_name_15'); + }); + expect(queryByText('detector_name_1')).toBeNull(); + }, 100000); + + test('sorting functionality', async () => { + httpClientMock.get = jest.fn().mockResolvedValue({ + ok: true, + response: { + detectorList: detectorsToAssociate, + totalDetectors: detectorsToAssociate.length, + }, + }); + (getAugmentVisSavedObjs as jest.Mock).mockImplementation(() => + Promise.resolve(savedObjects) + ); + + const { queryByText, getAllByTestId, findByText } = + renderWithRouter(visEmbeddable); + + // initial load only first 10 detectors (string sort means detector_name_0 -> detector_name_9 show up) + await waitFor(() => + findByText('detector_name_0', undefined, { timeout: 100000 }) + ); + expect(queryByText('detector_name_15')).toBeNull(); + + // Sort by name (string sorting) + userEvent.click(getAllByTestId('tableHeaderSortButton')[0]); + await waitFor(() => + findByText('detector_name_15', undefined, { timeout: 150000 }) + ); + expect(queryByText('detector_name_9')).toBeNull(); + }, 200000); +}); diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/utils/helpers.tsx b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/utils/helpers.tsx index c6125537..e01a4505 100644 --- a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/utils/helpers.tsx +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/utils/helpers.tsx @@ -62,6 +62,7 @@ export const getColumns = ({ handleUnlinkDetectorAction }) => description: 'Remove association', icon: 'unlink', onClick: handleUnlinkDetectorAction, + 'data-test-subj': 'unlinkButton', }, ], }, diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx index 766cee4e..849f2734 100644 --- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx @@ -413,17 +413,11 @@ function AddAnomalyDetector({ closeFlyout(); }) .catch((error) => { - notifications.toasts.addDanger( - prettifyErrorMessage( - `Error associating selected detector: ${error}` - ) - ); + notifications.toasts.addDanger(prettifyErrorMessage(error)); }); }) .catch((error) => { - notifications.toasts.addDanger( - prettifyErrorMessage(`Error associating selected detector: ${error}`) - ); + notifications.toasts.addDanger(prettifyErrorMessage(error)); }); }; diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AssociateExisting/containers/AssociateExisting.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AssociateExisting/containers/AssociateExisting.tsx index ba5e12fc..d8ee78d3 100644 --- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AssociateExisting/containers/AssociateExisting.tsx +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AssociateExisting/containers/AssociateExisting.tsx @@ -206,7 +206,7 @@ export function AssociateExisting( options={options} selectedOptions={selectedOptions} onChange={(selectedOptions) => { - let detector = {} as DetectorListItem | undefined; + let detector = undefined as DetectorListItem | undefined; if (selectedOptions && selectedOptions.length) { const match = existingDetectorsAvailableToAssociate.find( @@ -234,7 +234,7 @@ export function AssociateExisting( - {renderTime(detector.enabledTime)} + Running since {renderTime(detector.enabledTime)} diff --git a/public/expressions/__tests__/overlay_anomalies.test.ts b/public/expressions/__tests__/overlay_anomalies.test.ts index d55ef2e4..c503c601 100644 --- a/public/expressions/__tests__/overlay_anomalies.test.ts +++ b/public/expressions/__tests__/overlay_anomalies.test.ts @@ -20,6 +20,7 @@ import { } from '../../pages/utils/__tests__/constants'; import { DETECTOR_HAS_BEEN_DELETED, + PLUGIN_EVENT_TYPE, START_OR_END_TIME_INVALID_ERROR, VIS_LAYER_PLUGIN_TYPE, } from '../constants'; @@ -104,6 +105,7 @@ describe('overlay_anomalies spec', () => { type: 'Anomaly Detectors', urlPath: `anomaly-detection-dashboards#/detectors/${ANOMALY_RESULT_SUMMARY_DETECTOR_ID}/results`, }, + pluginEventType: PLUGIN_EVENT_TYPE, type: 'PointInTimeEvents', }; const pointInTimeEventsVisLayer = diff --git a/public/expressions/helpers.ts b/public/expressions/helpers.ts index ed3db94b..4b76e0fb 100644 --- a/public/expressions/helpers.ts +++ b/public/expressions/helpers.ts @@ -83,7 +83,7 @@ export const convertAnomaliesToPointInTimeEventsVisLayer = ( type: VisLayerTypes.PointInTimeEvents, pluginResource: ADPluginResource, events: events, - pluginEventType: PLUGIN_EVENT_TYPE + pluginEventType: PLUGIN_EVENT_TYPE, } as PointInTimeEventsVisLayer; }; diff --git a/public/services.ts b/public/services.ts index 7e0d7843..a684a019 100644 --- a/public/services.ts +++ b/public/services.ts @@ -34,3 +34,14 @@ export const [getUiActions, setUiActions] = export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); + +// This is primarily used for mocking this module and each of its fns in tests. +export default { + getSavedFeatureAnywhereLoader, + getUISettings, + getUiActions, + getEmbeddable, + getNotifications, + getOverlays, + setUISettings, +}; From 7d24544801c6a0579739613a6da0aaa0f8d0f339 Mon Sep 17 00:00:00 2001 From: Jackie Han Date: Fri, 9 Jun 2023 14:38:10 -0700 Subject: [PATCH 30/43] make feature anywhere formatted detector name an assgiend value (#506) * make feature anywhere formatted detector name an assgiend value to return Signed-off-by: Jackie Han * update feature anywhere documentation link Signed-off-by: Jackie Han * correct type Signed-off-by: Jackie Han * correct typo Signed-off-by: Jackie Han --------- Signed-off-by: Jackie Han --- .../CreateAnomalyDetector/helpers.tsx | 9 +++++---- public/utils/constants.ts | 3 +++ public/utils/contextMenu/getActions.tsx | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/helpers.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/helpers.tsx index 4a990b67..9bd6de0a 100644 --- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/helpers.tsx +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/helpers.tsx @@ -32,18 +32,19 @@ export function visFeatureListToFormik( export function formikToDetectorName(title) { const detectorName = title + '_anomaly_detector_' + Math.floor(100000 + Math.random() * 900000); - detectorName.replace(/[^a-zA-Z0-9-_]/g, '_'); - return detectorName; + const formattedName = detectorName.replace(/[^a-zA-Z0-9\-_]/g, '_'); + return formattedName; } const getFeatureNameFromVisParams = (id, seriesParams) => { - let name = find(seriesParams, function (param) { + const name = find(seriesParams, function (param) { if (param.data.id === id) { return true; } }); - return name.data.label.replace(/[^a-zA-Z0-9-_]/g, '_'); + const formattedFeatureName = name.data.label.replace(/[^a-zA-Z0-9-_]/g, '_'); + return formattedFeatureName }; function visAggregationToFormik(value) { diff --git a/public/utils/constants.ts b/public/utils/constants.ts index ce576412..fb2d2c72 100644 --- a/public/utils/constants.ts +++ b/public/utils/constants.ts @@ -59,6 +59,9 @@ export const AD_DOCS_LINK = export const AD_HIGH_CARDINALITY_LINK = 'https://opensearch.org/docs/latest/observing-your-data/ad/index/#optional-set-category-fields-for-high-cardinality'; +export const AD_FEATURE_ANYWHERE_LINK = + 'https://opensearch.org/docs/latest/observing-your-data/ad/dashboards-anomaly-detection/'; + export const MAX_DETECTORS = 1000; export const MAX_ANOMALIES = 10000; diff --git a/public/utils/contextMenu/getActions.tsx b/public/utils/contextMenu/getActions.tsx index 26bf8f06..baf31b70 100644 --- a/public/utils/contextMenu/getActions.tsx +++ b/public/utils/contextMenu/getActions.tsx @@ -13,7 +13,7 @@ import AnywhereParentFlyout from '../../components/FeatureAnywhereContextMenu/An import { Provider } from 'react-redux'; import configureStore from '../../redux/configureStore'; import DocumentationTitle from '../../components/FeatureAnywhereContextMenu/DocumentationTitle/containers/DocumentationTitle'; -import { AD_DOCS_LINK, APM_TRACE } from '../constants'; +import { AD_FEATURE_ANYWHERE_LINK, APM_TRACE } from '../constants'; import { getClient, getOverlays } from '../../../public/services'; import { FLYOUT_MODES } from '../../../public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/constants'; @@ -80,7 +80,7 @@ export const getActions = () => { icon: 'documentation' as EuiIconType, order: 98, onClick: () => { - window.open(AD_DOCS_LINK, '_blank'); + window.open(AD_FEATURE_ANYWHERE_LINK, '_blank'); }, }, ].map((options) => createADAction({ ...options, grouping })); From 261df11a63c9053b869bf9d9b18bcf7862e9a883 Mon Sep 17 00:00:00 2001 From: Amit Galitzky Date: Mon, 12 Jun 2023 14:02:53 -0700 Subject: [PATCH 31/43] various bug fixes for anywhere features (#507) Signed-off-by: Amit Galitzky --- public/action/ad_dashboard_action.tsx | 5 +---- .../containers/AssociatedDetectors.tsx | 22 ++++++++++--------- .../AddAnomalyDetector.tsx | 5 ++--- .../containers/AssociateExisting.tsx | 15 ++++++++++--- .../CreateAnomalyDetector/helpers.tsx | 2 +- public/plugin.ts | 5 +---- 6 files changed, 29 insertions(+), 25 deletions(-) diff --git a/public/action/ad_dashboard_action.tsx b/public/action/ad_dashboard_action.tsx index 417de3a9..f8bfbb1d 100644 --- a/public/action/ad_dashboard_action.tsx +++ b/public/action/ad_dashboard_action.tsx @@ -14,7 +14,6 @@ import { } from '../../../../src/plugins/ui_actions/public'; import { isReferenceOrValueEmbeddable } from '../../../../src/plugins/embeddable/public'; import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; -import { isEmpty } from 'lodash'; import { VisualizeEmbeddable } from '../../../../src/plugins/visualizations/public'; import { isEligibleForVisLayers } from '../../../../src/plugins/vis_augmenter/public'; @@ -65,15 +64,13 @@ export const createADAction = ({ embeddable.parent && isDashboard(embeddable.parent) && vis !== undefined && - isEligibleForVisLayers(vis) && - !isEmpty((embeddable as VisualizeEmbeddable).visLayers) + isEligibleForVisLayers(vis) ); }, execute: async ({ embeddable }: ActionContext) => { if (!isReferenceOrValueEmbeddable(embeddable)) { throw new IncompatibleActionError(); } - onClick({ embeddable }); }, }); diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx index 0829f4ec..004c81d1 100644 --- a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx @@ -305,16 +305,18 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) {

Visualization: {embeddableTitle}

- - { - setMode('existing'); - }} - > - Associate a detector - + +
+ { + setMode('existing'); + }} + > + Associate a detector + +
diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx index 849f2734..09098dce 100644 --- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx @@ -103,6 +103,7 @@ import { formikToDetectorName, visFeatureListToFormik } from './helpers'; import { AssociateExisting } from './AssociateExisting'; import { mountReactNode } from '../../../../../../src/core/public/utils'; import { FLYOUT_MODES } from '../AnywhereParentFlyout/constants'; +import { DetectorListItem } from '../../../../public/models/interfaces'; function AddAnomalyDetector({ embeddable, @@ -206,9 +207,7 @@ function AddAnomalyDetector({ } }); } else { - notifications.toasts.addDanger( - 'One or more features are required.' - ); + notifications.toasts.addDanger('One or more features are required.'); } }; diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AssociateExisting/containers/AssociateExisting.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AssociateExisting/containers/AssociateExisting.tsx index d8ee78d3..cad7a718 100644 --- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AssociateExisting/containers/AssociateExisting.tsx +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AssociateExisting/containers/AssociateExisting.tsx @@ -186,7 +186,11 @@ export function AssociateExisting(

View existing anomaly detectors across your system and add the detector(s) to a dashboard and visualization.{' '} - + Learn more

@@ -218,7 +222,7 @@ export function AssociateExisting( }} aria-label="Select an anomaly detector to associate" isClearable - singleSelection + singleSelection={{ asPlainText: true }} placeholder="Search for an anomaly detector" /> ) : ( @@ -238,7 +242,12 @@ export function AssociateExisting( - + View detector page diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/helpers.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/helpers.tsx index 9bd6de0a..685571e9 100644 --- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/helpers.tsx +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/helpers.tsx @@ -44,7 +44,7 @@ const getFeatureNameFromVisParams = (id, seriesParams) => { }); const formattedFeatureName = name.data.label.replace(/[^a-zA-Z0-9-_]/g, '_'); - return formattedFeatureName + return formattedFeatureName; }; function visAggregationToFormik(value) { diff --git a/public/plugin.ts b/public/plugin.ts index 83fd40eb..86aeb149 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -40,10 +40,7 @@ import { VisAugmenterSetup, VisAugmenterStart, } from '../../../src/plugins/vis_augmenter/public'; -import { - UiActionsSetup, - UiActionsStart, -} from '../../../src/plugins/ui_actions/public'; +import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; declare module '../../../src/plugins/ui_actions/public' { export interface ActionContextMapping { From c286b8cf2d0be8e613fcb39f3c339e6d61e394d4 Mon Sep 17 00:00:00 2001 From: David Sinclair <24573542+sikhote@users.noreply.github.com> Date: Mon, 19 Jun 2023 13:41:59 -0700 Subject: [PATCH 32/43] add group category and order for context menu (#495) Signed-off-by: David Sinclair --- public/utils/contextMenu/getActions.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/utils/contextMenu/getActions.tsx b/public/utils/contextMenu/getActions.tsx index baf31b70..f10756f6 100644 --- a/public/utils/contextMenu/getActions.tsx +++ b/public/utils/contextMenu/getActions.tsx @@ -23,6 +23,8 @@ const grouping: Action['grouping'] = [ id: 'ad-dashboard-context-menu', getDisplayName: () => 'Anomaly Detection', getIconType: () => APM_TRACE, + category: 'vis_augmenter', + order: 20, }, ]; From c1a48462bd95d48e0574b3e3fe84b8d156bc49da Mon Sep 17 00:00:00 2001 From: Jackie Han Date: Tue, 20 Jun 2023 10:54:26 -0700 Subject: [PATCH 33/43] Update Advanced configuration accordion title (#513) Signed-off-by: Jackie Han --- .../CreateAnomalyDetector/AddAnomalyDetector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx index 09098dce..f55cc9b0 100644 --- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx @@ -672,7 +672,7 @@ function AddAnomalyDetector({ onAccordionToggle('advancedConfiguration') From 01883d2f39a4f9197b0823a69a517b9c6fbe6ec0 Mon Sep 17 00:00:00 2001 From: Jackie Han Date: Tue, 27 Jun 2023 15:36:37 -0700 Subject: [PATCH 34/43] update categorical field text on Feature Anywhere flyout (#516) * update categorical field text on Feature Anywhere flyout Signed-off-by: Jackie Han * Use non-breaking space character to prevent removing needed empty space by yarn prettier Signed-off-by: Jackie Han * Fix the max width of associate detector flyout to 740px Signed-off-by: Jackie Han --------- Signed-off-by: Jackie Han --- .../AssociatedDetectors/containers/AssociatedDetectors.tsx | 2 +- .../CreateAnomalyDetector/AddAnomalyDetector.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx index 004c81d1..6421f863 100644 --- a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx @@ -281,7 +281,7 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) { }; return (
- +

diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx index f55cc9b0..bb6b12b9 100644 --- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx @@ -808,7 +808,7 @@ function AddAnomalyDetector({

The dashboard does not support high-cardinality - detectors. + detectors.  Learn more From bb80536114ccbb3ba9c84f21872497d0f3843270 Mon Sep 17 00:00:00 2001 From: Amit Galitzky Date: Thu, 29 Jun 2023 15:11:54 -0700 Subject: [PATCH 35/43] fixing vis eligibility (#519) Signed-off-by: Amit Galitzky --- public/action/ad_dashboard_action.tsx | 4 +++- .../containers/AssociatedDetectors.tsx | 8 +++++++- public/plugin.ts | 6 +----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/public/action/ad_dashboard_action.tsx b/public/action/ad_dashboard_action.tsx index f8bfbb1d..2cde952b 100644 --- a/public/action/ad_dashboard_action.tsx +++ b/public/action/ad_dashboard_action.tsx @@ -16,6 +16,7 @@ import { isReferenceOrValueEmbeddable } from '../../../../src/plugins/embeddable import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; import { VisualizeEmbeddable } from '../../../../src/plugins/visualizations/public'; import { isEligibleForVisLayers } from '../../../../src/plugins/vis_augmenter/public'; +import { getUISettings } from '../services'; export const ACTION_AD = 'ad'; @@ -62,9 +63,10 @@ export const createADAction = ({ const vis = (embeddable as VisualizeEmbeddable).vis; return Boolean( embeddable.parent && + embeddable.getInput()?.viewMode === 'view' && isDashboard(embeddable.parent) && vis !== undefined && - isEligibleForVisLayers(vis) + isEligibleForVisLayers(vis, getUISettings()) ); }, execute: async ({ embeddable }: ActionContext) => { diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx index 6421f863..8ab93c76 100644 --- a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx @@ -281,7 +281,13 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) { }; return (

- +

diff --git a/public/plugin.ts b/public/plugin.ts index 86aeb149..53b9aa9a 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -83,11 +83,7 @@ export class AnomalyDetectionOpenSearchDashboardsPlugin }, }); - // // set embeddable plugin for feature anywhere create flyout - // setEmbeddable(embeddable); - - // // set vis argumenter loader for feature anywhere associated flyout - // setSavedFeatureAnywhereLoader(visAugmenter.savedAugmentVisLoader); + setUISettings(core.uiSettings); // Set the HTTP client so it can be pulled into expression fns to make // direct server-side calls From 8e905d12e03dbbbd92ac9c300003f2c5330e87c9 Mon Sep 17 00:00:00 2001 From: Jackie Han Date: Mon, 3 Jul 2023 09:10:19 -0700 Subject: [PATCH 36/43] render embeddable by using vis embeddable (#521) Signed-off-by: Jackie Han --- .../AddAnomalyDetector.tsx | 20 ++++++++++++++++--- public/plugin.ts | 6 +++++- public/services.ts | 5 +++++ public/utils/constants.ts | 2 +- public/utils/contextMenu/getActions.tsx | 4 ++-- 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx index bb6b12b9..da658662 100644 --- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx @@ -29,6 +29,7 @@ import { import './styles.scss'; import { createAugmentVisSavedObject, + fetchVisEmbeddable, ISavedAugmentVis, ISavedPluginResource, SavedAugmentVisLoader, @@ -50,7 +51,7 @@ import { matchDetector, startDetector, } from '../../../../public/redux/reducers/ad'; -import { EmbeddableRenderer } from '../../../../../../src/plugins/embeddable/public'; +import { EmbeddableRenderer, ErrorEmbeddable } from '../../../../../../src/plugins/embeddable/public'; import './styles.scss'; import EnhancedAccordion from '../EnhancedAccordion'; import MinimalAccordion from '../MinimalAccordion'; @@ -86,10 +87,12 @@ import { MAX_FEATURE_NUM, } from '../../../../public/utils/constants'; import { + getEmbeddable, getNotifications, getSavedFeatureAnywhereLoader, getUISettings, getUiActions, + getQueryService } from '../../../../public/services'; import { prettifyErrorMessage } from '../../../../server/utils/helpers'; import { @@ -104,6 +107,7 @@ import { AssociateExisting } from './AssociateExisting'; import { mountReactNode } from '../../../../../../src/core/public/utils'; import { FLYOUT_MODES } from '../AnywhereParentFlyout/constants'; import { DetectorListItem } from '../../../../public/models/interfaces'; +import { VisualizeEmbeddable } from '../../../../../../src/plugins/visualizations/public'; function AddAnomalyDetector({ embeddable, @@ -115,14 +119,24 @@ function AddAnomalyDetector({ }) { const dispatch = useDispatch(); const [queryText, setQueryText] = useState(''); + const [generatedEmbeddable, setGeneratedEmbeddable] = useState< + VisualizeEmbeddable | ErrorEmbeddable +>(); + useEffect(() => { const getInitialIndices = async () => { await dispatch(getIndices(queryText)); }; getInitialIndices(); dispatch(getMappings(embeddable.vis.data.aggs.indexPattern.title)); - }, []); + const createEmbeddable = async () => { + const visEmbeddable = await fetchVisEmbeddable(embeddable.vis.id, getEmbeddable(), getQueryService()); + setGeneratedEmbeddable(visEmbeddable); + }; + + createEmbeddable(); + }, []); const [isShowVis, setIsShowVis] = useState(false); const [accordionsOpen, setAccordionsOpen] = useState({ modelFeatures: true }); const [detectorNameFromVis, setDetectorNameFromVis] = useState( @@ -557,7 +571,7 @@ function AddAnomalyDetector({ }`} > - +

diff --git a/public/plugin.ts b/public/plugin.ts index 53b9aa9a..8d8f6db2 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -34,6 +34,7 @@ import { setSavedFeatureAnywhereLoader, setUiActions, setUISettings, + setQueryService } from './services'; import { AnomalyDetectionOpenSearchDashboardsPluginStart } from 'public'; import { @@ -41,6 +42,7 @@ import { VisAugmenterStart, } from '../../../src/plugins/vis_augmenter/public'; import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; +import { DataPublicPluginStart } from '../../../src/plugins/data/public'; declare module '../../../src/plugins/ui_actions/public' { export interface ActionContextMapping { @@ -61,6 +63,7 @@ export interface AnomalyDetectionStartDeps { notifications: NotificationsStart; visAugmenter: VisAugmenterStart; uiActions: UiActionsStart; + data: DataPublicPluginStart; } export class AnomalyDetectionOpenSearchDashboardsPlugin @@ -104,7 +107,7 @@ export class AnomalyDetectionOpenSearchDashboardsPlugin public start( core: CoreStart, - { embeddable, visAugmenter, uiActions }: AnomalyDetectionStartDeps + { embeddable, visAugmenter, uiActions, data }: AnomalyDetectionStartDeps ): AnomalyDetectionOpenSearchDashboardsPluginStart { setUISettings(core.uiSettings); setEmbeddable(embeddable); @@ -112,6 +115,7 @@ export class AnomalyDetectionOpenSearchDashboardsPlugin setSavedFeatureAnywhereLoader(visAugmenter.savedAugmentVisLoader); setNotifications(core.notifications); setUiActions(uiActions); + setQueryService(data.query); return {}; } } diff --git a/public/services.ts b/public/services.ts index a684a019..0ee47f2b 100644 --- a/public/services.ts +++ b/public/services.ts @@ -9,6 +9,7 @@ import { NotificationsStart, OverlayStart, } from '../../../src/core/public'; +import { DataPublicPluginStart } from '../../../src/plugins/data/public'; import { EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { createGetterSetter } from '../../../src/plugins/opensearch_dashboards_utils/public'; import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; @@ -35,6 +36,9 @@ export const [getUiActions, setUiActions] = export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); +export const [getQueryService, setQueryService] = +createGetterSetter('Query'); + // This is primarily used for mocking this module and each of its fns in tests. export default { getSavedFeatureAnywhereLoader, @@ -44,4 +48,5 @@ export default { getNotifications, getOverlays, setUISettings, + setQueryService, }; diff --git a/public/utils/constants.ts b/public/utils/constants.ts index fb2d2c72..6f244704 100644 --- a/public/utils/constants.ts +++ b/public/utils/constants.ts @@ -97,4 +97,4 @@ export enum MISSING_FEATURE_DATA_SEVERITY { export const SPACE_STR = ' '; -export const APM_TRACE = 'apmTrace'; +export const ANOMALY_DETECTION_ICON = 'anomalyDetection'; diff --git a/public/utils/contextMenu/getActions.tsx b/public/utils/contextMenu/getActions.tsx index f10756f6..f58a7a9e 100644 --- a/public/utils/contextMenu/getActions.tsx +++ b/public/utils/contextMenu/getActions.tsx @@ -13,7 +13,7 @@ import AnywhereParentFlyout from '../../components/FeatureAnywhereContextMenu/An import { Provider } from 'react-redux'; import configureStore from '../../redux/configureStore'; import DocumentationTitle from '../../components/FeatureAnywhereContextMenu/DocumentationTitle/containers/DocumentationTitle'; -import { AD_FEATURE_ANYWHERE_LINK, APM_TRACE } from '../constants'; +import { AD_FEATURE_ANYWHERE_LINK, ANOMALY_DETECTION_ICON } from '../constants'; import { getClient, getOverlays } from '../../../public/services'; import { FLYOUT_MODES } from '../../../public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/constants'; @@ -22,7 +22,7 @@ const grouping: Action['grouping'] = [ { id: 'ad-dashboard-context-menu', getDisplayName: () => 'Anomaly Detection', - getIconType: () => APM_TRACE, + getIconType: () => ANOMALY_DETECTION_ICON, category: 'vis_augmenter', order: 20, }, From 39894dd3f0deae1f76359c0947044b42f444abd7 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Mon, 3 Jul 2023 09:31:58 -0700 Subject: [PATCH 37/43] Change to anomaly start time; add test ids (#522) Signed-off-by: Tyler Ohlsen --- .../containers/AssociatedDetectors.tsx | 1 + .../CreateAnomalyDetector/AddAnomalyDetector.tsx | 8 ++++---- .../EnhancedAccordion/EnhancedAccordion.tsx | 1 + public/expressions/helpers.ts | 2 +- public/pages/DetectorConfig/containers/Features.tsx | 1 + 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx index 8ab93c76..ac6ea8a8 100644 --- a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx @@ -314,6 +314,7 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) {
{ diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx index da658662..6e22edab 100644 --- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx @@ -580,7 +580,7 @@ function AddAnomalyDetector({ onAccordionToggle('detectorDetails')} @@ -685,7 +685,7 @@ function AddAnomalyDetector({ @@ -838,7 +838,7 @@ function AddAnomalyDetector({ onAccordionToggle('modelFeatures')} @@ -910,7 +910,7 @@ function AddAnomalyDetector({ {mode === FLYOUT_MODES.existing ? ( handleAssociate(selectedDetector)} > diff --git a/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/EnhancedAccordion.tsx b/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/EnhancedAccordion.tsx index 17075e27..b129bc20 100644 --- a/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/EnhancedAccordion.tsx +++ b/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/EnhancedAccordion.tsx @@ -51,6 +51,7 @@ const EnhancedAccordion = ({ buttonContent={
{ const events = anomalies.map((anomaly: AnomalyData) => { return { - timestamp: anomaly.startTime + (anomaly.endTime - anomaly.startTime) / 2, + timestamp: anomaly.startTime, metadata: {}, }; }); diff --git a/public/pages/DetectorConfig/containers/Features.tsx b/public/pages/DetectorConfig/containers/Features.tsx index d5b8e455..9cdb609d 100644 --- a/public/pages/DetectorConfig/containers/Features.tsx +++ b/public/pages/DetectorConfig/containers/Features.tsx @@ -234,6 +234,7 @@ export const Features = (props: FeaturesProps) => { titleSize="s" > Date: Thu, 6 Jul 2023 22:16:37 -0700 Subject: [PATCH 38/43] Adding a callout on flyout when association limit has been reached (#524) * adding a callout when association limit has been reached Signed-off-by: Amit Galitzky * fixed limit check and ran prettier Signed-off-by: Amit Galitzky --------- Signed-off-by: Amit Galitzky --- .../containers/AssociatedDetectors.tsx | 29 + .../AddAnomalyDetector.tsx | 825 ++++++++++-------- public/plugin.ts | 2 +- public/services.ts | 4 +- 4 files changed, 477 insertions(+), 383 deletions(-) diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx index ac6ea8a8..1cbdbc82 100644 --- a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx @@ -14,6 +14,7 @@ import { EuiFlyout, EuiFlexItem, EuiFlexGroup, + EuiCallOut, } from '@elastic/eui'; import { get, isEmpty } from 'lodash'; import '../styles.scss'; @@ -45,6 +46,7 @@ import { getAugmentVisSavedObjs, } from '../../../../../../../src/plugins/vis_augmenter/public'; import { ASSOCIATED_DETECTOR_ACTION } from '../utils/constants'; +import { PLUGIN_AUGMENTATION_MAX_OBJECTS_SETTING } from '../../../../../public/expressions/constants'; interface ConfirmModalState { isOpen: boolean; @@ -74,6 +76,8 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) { const [detectorToUnlink, setDetectorToUnlink] = useState( {} as DetectorListItem ); + const [associationLimitReached, setAssociationLimitReached] = + useState(false); const [confirmModalState, setConfirmModalState] = useState( { isOpen: false, @@ -91,6 +95,9 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) { const uiSettings = getUISettings(); const notifications = getNotifications(); + let maxAssociatedCount = uiSettings.get( + PLUGIN_AUGMENTATION_MAX_OBJECTS_SETTING + ); useEffect(() => { if ( @@ -137,11 +144,19 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) { getAugmentVisSavedObjs(embeddable.vis.id, savedObjectLoader, uiSettings) .then((savedAugmentObjectsArr: any) => { if (savedAugmentObjectsArr != undefined) { + if (maxAssociatedCount <= savedAugmentObjectsArr.length) { + setAssociationLimitReached(true); + } else { + setAssociationLimitReached(false); + } const curSelectedDetectors = getAssociatedDetectors( Object.values(allDetectors), savedAugmentObjectsArr ); setSelectedDetectors(curSelectedDetectors); + maxAssociatedCount = uiSettings.get( + PLUGIN_AUGMENTATION_MAX_OBJECTS_SETTING + ); setIsLoadingFinalDetectors(false); } }) @@ -295,6 +310,19 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) {

+ {associationLimitReached ? ( + + Adding more objects may affect cluster performance and prevent + dashboards from rendering properly. Remove associations before + adding new ones. + + ) : null} {confirmModalState.isOpen ? ( { setMode('existing'); diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx index 6e22edab..106d0dff 100644 --- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx @@ -51,7 +51,10 @@ import { matchDetector, startDetector, } from '../../../../public/redux/reducers/ad'; -import { EmbeddableRenderer, ErrorEmbeddable } from '../../../../../../src/plugins/embeddable/public'; +import { + EmbeddableRenderer, + ErrorEmbeddable, +} from '../../../../../../src/plugins/embeddable/public'; import './styles.scss'; import EnhancedAccordion from '../EnhancedAccordion'; import MinimalAccordion from '../MinimalAccordion'; @@ -92,7 +95,7 @@ import { getSavedFeatureAnywhereLoader, getUISettings, getUiActions, - getQueryService + getQueryService, } from '../../../../public/services'; import { prettifyErrorMessage } from '../../../../server/utils/helpers'; import { @@ -120,8 +123,8 @@ function AddAnomalyDetector({ const dispatch = useDispatch(); const [queryText, setQueryText] = useState(''); const [generatedEmbeddable, setGeneratedEmbeddable] = useState< - VisualizeEmbeddable | ErrorEmbeddable ->(); + VisualizeEmbeddable | ErrorEmbeddable + >(); useEffect(() => { const getInitialIndices = async () => { @@ -131,7 +134,11 @@ function AddAnomalyDetector({ dispatch(getMappings(embeddable.vis.data.aggs.indexPattern.title)); const createEmbeddable = async () => { - const visEmbeddable = await fetchVisEmbeddable(embeddable.vis.id, getEmbeddable(), getQueryService()); + const visEmbeddable = await fetchVisEmbeddable( + embeddable.vis.id, + getEmbeddable(), + getQueryService() + ); setGeneratedEmbeddable(visEmbeddable); }; @@ -145,6 +152,8 @@ function AddAnomalyDetector({ const [intervalValue, setIntervalalue] = useState(10); const [delayValue, setDelayValue] = useState(1); const [enabled, setEnabled] = useState(false); + const [associationLimitReached, setAssociationLimitReached] = + useState(false); const title = embeddable.getTitle(); const onAccordionToggle = (key) => { @@ -229,6 +238,67 @@ function AddAnomalyDetector({ const savedObjectLoader: SavedAugmentVisLoader = getSavedFeatureAnywhereLoader(); + let maxAssociatedCount = uiSettings.get( + PLUGIN_AUGMENTATION_MAX_OBJECTS_SETTING + ); + + useEffect(async () => { + // Gets all augmented saved objects + await savedObjectLoader.findAll().then(async (resp) => { + if (resp !== undefined) { + const savedAugmentObjects = get(resp, 'hits', []); + // gets all the saved object for this visualization + const savedObjectsForThisVisualization = savedAugmentObjects.filter( + (savedObj) => get(savedObj, 'visId', '') === embeddable.vis.id + ); + if (maxAssociatedCount <= savedObjectsForThisVisualization.length) { + setAssociationLimitReached(true); + } else { + setAssociationLimitReached(false); + } + } + }); + }, []); + + const getEmbeddableSection = () => { + return ( + <> + +

+ Create and configure an anomaly detector to automatically detect + anomalies in your data and to view real-time results on the + visualization.{' '} + + Learn more + +

+
+ +
+ +

+ + {title} +

+
+ setIsShowVis(!isShowVis)} + /> +
+
+ + +
+ + ); + }; + const getAugmentVisSavedObject = (detectorId: string) => { const fn = { type: VisLayerTypes.PointInTimeEvents, @@ -490,416 +560,410 @@ function AddAnomalyDetector({ -
- - - Options to create a new detector or associate an - existing detector - - - ), - }} - className="add-anomaly-detector__modes" - > - {[ - { - id: 'add-anomaly-detector__create', - label: 'Create new detector', - value: 'create', - }, - { - id: 'add-anomaly-detector__existing', - label: 'Associate existing detector', - value: 'existing', - }, - ].map((option) => ( - setMode(option.value), - }} - /> - ))} - - - {mode === FLYOUT_MODES.existing && ( - - )} - {mode === FLYOUT_MODES.create && ( -
- -

- Create and configure an anomaly detector to - automatically detect anomalies in your data and to view - real-time results on the visualization.{' '} - - Learn more - -

-
- -
- -

- - {title} -

-
- setIsShowVis(!isShowVis)} + {associationLimitReached ? ( +
+ + Adding more objects may affect cluster performance and + prevent dashboards from rendering properly. Remove + associations before adding new ones. + + {getEmbeddableSection()} +
+ ) : ( +
+ + + Options to create a new detector or associate an + existing detector + + + ), + }} + className="add-anomaly-detector__modes" + > + {[ + { + id: 'add-anomaly-detector__create', + label: 'Create new detector', + value: 'create', + }, + { + id: 'add-anomaly-detector__existing', + label: 'Associate existing detector', + value: 'existing', + }, + ].map((option) => ( + setMode(option.value), + }} /> -
-
- - -
- - -

Detector details

-
- - - onAccordionToggle('detectorDetails')} - subTitle={ - -

- Detector interval: {intervalValue} minute(s); Window - delay: {delayValue} minute(s) -

-
- } - > - - {({ field, form }: FieldProps) => ( - - + + {mode === FLYOUT_MODES.existing && ( + + )} + {mode === FLYOUT_MODES.create && ( +
+ {getEmbeddableSection()} + + +

Detector details

+
+ + + onAccordionToggle('detectorDetails')} + subTitle={ + +

+ Detector interval: {intervalValue} minute(s); + Window delay: {delayValue} minute(s) +

+
+ } + > + + {({ field, form }: FieldProps) => ( + onDetectorNameChange(e, field)} - /> - - )} - - - - - {({ field, form }: FieldProps) => ( - - - + - onDetectorNameChange(e, field)} + /> + + )} + + + + + {({ field, form }: FieldProps) => ( + + + - - - onIntervalChange(e, field) - } - /> - - - -

minute(s)

-
-
-
- - - - )} -
- - - - {({ field, form }: FieldProps) => ( - - - - onDelayChange(e, field)} - /> - - - -

minute(s)

-
+ + + + onIntervalChange(e, field) + } + /> + + + +

minute(s)

+
+
+
+
- - )} -
-
- - - - - onAccordionToggle('advancedConfiguration') - } - initialIsOpen={false} - > - - - - -

- Source:{' '} - {embeddable.vis.data.aggs.indexPattern.title} -

-
+ )} + + - -
- - - {({ field, form }: FieldProps) => ( onDelayChange(e, field)} /> -

intervals

+

minute(s)

)}
-
- - + + + + + onAccordionToggle('advancedConfiguration') + } + initialIsOpen={false} > - - {({ field, form }: FieldProps) => ( - - - { - if (enabled) { - form.setFieldValue('resultIndex', ''); - } - setEnabled(!enabled); - }} - /> - - - {enabled ? ( - - - - ) : null} - - {enabled ? ( - - - + + + +

+ Source:{' '} + {embeddable.vis.data.aggs.indexPattern.title} +

+
+ + +
+ + + + + {({ field, form }: FieldProps) => ( + + + + -
+
+ + +

intervals

+
+
+
+ + )} +
+
+ + + + {({ field, form }: FieldProps) => ( + + + { + if (enabled) { + form.setFieldValue('resultIndex', ''); + } + setEnabled(!enabled); + }} + /> - ) : null} - - )} - - - + + + ) : null} + + {enabled ? ( + + + + + + ) : null} + + )} + + + + + +

+ The dashboard does not support high-cardinality + detectors.  + + Learn more + +

+
+
+
+ + + +

Model Features

+
+ + + onAccordionToggle('modelFeatures')} > - -

- The dashboard does not support high-cardinality - detectors.  - - Learn more - -

-
- -
- - - -

Model Features

-
- - - onAccordionToggle('modelFeatures')} - > - - {({ - push, - remove, - form: { values }, - }: FieldArrayRenderProps) => { - return ( - - {values.featureList.map( - (feature: any, index: number) => ( - { - remove(index); + + {({ + push, + remove, + form: { values }, + }: FieldArrayRenderProps) => { + return ( + + {values.featureList.map( + (feature: any, index: number) => ( + { + remove(index); + }} + index={index} + feature={feature} + handleChange={formikProps.handleChange} + displayMode="flyout" + /> + ) + )} + + + + = + MAX_FEATURE_NUM + } + onClick={() => { + push(initialFeatureValue()); }} - index={index} - feature={feature} - handleChange={formikProps.handleChange} - displayMode="flyout" - /> - ) - )} - - - - = MAX_FEATURE_NUM - } - onClick={() => { - push(initialFeatureValue()); - }} - > - Add another feature - - - - -

- You can add up to{' '} - {Math.max( - MAX_FEATURE_NUM - values.featureList.length, - 0 - )}{' '} - more features. -

-
-
- ); - }} -
-
- -
- )} -
+ > + Add another feature + + + + +

+ You can add up to{' '} + {Math.max( + MAX_FEATURE_NUM - + values.featureList.length, + 0 + )}{' '} + more features. +

+
+ + ); + }} + + + +
+ )} +
+ )}
@@ -919,6 +983,7 @@ function AddAnomalyDetector({ ) : ( { diff --git a/public/plugin.ts b/public/plugin.ts index 8d8f6db2..25ea0ebf 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -34,7 +34,7 @@ import { setSavedFeatureAnywhereLoader, setUiActions, setUISettings, - setQueryService + setQueryService, } from './services'; import { AnomalyDetectionOpenSearchDashboardsPluginStart } from 'public'; import { diff --git a/public/services.ts b/public/services.ts index 0ee47f2b..ef899307 100644 --- a/public/services.ts +++ b/public/services.ts @@ -36,8 +36,8 @@ export const [getUiActions, setUiActions] = export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); -export const [getQueryService, setQueryService] = -createGetterSetter('Query'); +export const [getQueryService, setQueryService] = + createGetterSetter('Query'); // This is primarily used for mocking this module and each of its fns in tests. export default { From 3d78992e2194487311d62d5e6f611e1fd9a3b881 Mon Sep 17 00:00:00 2001 From: Amit Galitzky Date: Fri, 7 Jul 2023 13:02:41 -0700 Subject: [PATCH 39/43] Merge main into featureAnywhere (#525) * Add branch constants in CI workflow (#345) Signed-off-by: Tyler Ohlsen * Bump decode-uri-component (#359) Signed-off-by: Tyler Ohlsen * removed duplicate popout icon and ran prettier (#382) Signed-off-by: Amit Galitzky * Change detector out of time range modal warning into a callout warning (#384) Signed-off-by: Jackie Han Signed-off-by: Jackie Han * Fix undefined entity list when heatmap is empty (#383) Signed-off-by: Tyler Ohlsen * Updated MAINTAINERS.md to match recommended opensearch-project format. (#388) Signed-off-by: dblock * Add windows env to integration test workflow (#390) Signed-off-by: Tyler Ohlsen * Bump json5 to 2.2.3 (#393) Signed-off-by: Tyler Ohlsen * Add 2.5 release notes (#395) Signed-off-by: Tyler Ohlsen * Update cold start message (#398) Signed-off-by: Kaituo Li * upgrade filter bug (#402) Previously, we didn't actually add filter type when loading old detector. This PR fixed that. Testing done: 1. added a unit tes 2. verified e2et Signed-off-by: Kaituo Li * Changed required minimum intervals in cold start message (#411) Required minimum intervals to finish cold start is related to shingle size. This PR adds the shingle size in the computation and puts the result in the cold start message. Testing done: 1. verified changing shingle size will change the message. Signed-off-by: Kaituo Li * Bump @sideway/formula to 3.0.1 (#418) Signed-off-by: Tyler Ohlsen * Remove auto_expand_replicas override in sample data indices (#423) Signed-off-by: Tyler Ohlsen * Created untriaged issue workflow. (#422) Signed-off-by: dblock * Add 2.6 release notes (#429) Signed-off-by: Tyler Ohlsen * Fix Node.js and Yarn installation in CI (#433) Signed-off-by: Miki * run prettier command against all files (#444) Signed-off-by: Jackie Han * Add 2.7 release notes (#456) Signed-off-by: Jackie Han * updating maintainers and code owners (#476) Signed-off-by: Amit Galitzky * fixing test to pass with node 18 (#491) Signed-off-by: Amit Galitzky * add 2.8.0 release notes (#500) Signed-off-by: Kaituo Li * Remove invalid link (#470) Signed-off-by: Peter Nied Signed-off-by: Peter Nied * Remove global OUI style override (#511) Signed-off-by: Matt Provost --------- Signed-off-by: Tyler Ohlsen Signed-off-by: Amit Galitzky Signed-off-by: Jackie Han Signed-off-by: dblock Signed-off-by: Kaituo Li Signed-off-by: Miki Signed-off-by: Peter Nied Signed-off-by: Peter Nied Signed-off-by: Matt Provost Co-authored-by: Tyler Ohlsen Co-authored-by: Jackie Han Co-authored-by: Daniel (dB.) Doubrovkine Co-authored-by: Kaituo Li Co-authored-by: Miki Co-authored-by: Peter Nied Co-authored-by: Matt Provost --- .../pages/AnomalyCharts/containers/AnomaliesChart.tsx | 3 +++ public/pages/AnomalyCharts/index.scss | 5 ----- public/pages/Dashboard/utils/utils.tsx | 1 - ...maly-detection-dashboards.release-notes-2.8.0.0.md | 11 +++++++++++ 4 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 release-notes/opensearch-anomaly-detection-dashboards.release-notes-2.8.0.0.md diff --git a/public/pages/AnomalyCharts/containers/AnomaliesChart.tsx b/public/pages/AnomalyCharts/containers/AnomaliesChart.tsx index 0a9a4e03..49f97213 100644 --- a/public/pages/AnomalyCharts/containers/AnomaliesChart.tsx +++ b/public/pages/AnomalyCharts/containers/AnomaliesChart.tsx @@ -171,6 +171,9 @@ export const AnomaliesChart = React.memo((props: AnomaliesChartProps) => { }} isPaused={true} commonlyUsedRanges={DATE_PICKER_QUICK_OPTIONS} + updateButtonProps={{ + fill: false, + }} /> ); diff --git a/public/pages/AnomalyCharts/index.scss b/public/pages/AnomalyCharts/index.scss index 7ec70974..ee30bd28 100644 --- a/public/pages/AnomalyCharts/index.scss +++ b/public/pages/AnomalyCharts/index.scss @@ -10,8 +10,3 @@ */ @import 'components/AlertsFlyout/alertsFlyout.scss'; - -.euiSuperUpdateButton { - background-color: transparent !important; - color: #006bb4 !important; -} diff --git a/public/pages/Dashboard/utils/utils.tsx b/public/pages/Dashboard/utils/utils.tsx index 603bb37a..451d2b90 100644 --- a/public/pages/Dashboard/utils/utils.tsx +++ b/public/pages/Dashboard/utils/utils.tsx @@ -313,7 +313,6 @@ export const buildColors = palleteBuilder( rgbColors.map(([r, g, b]) => [r, g, b, 0.8]) ); -// referred to here: https://tiny.amazon.com/337xpvcq/githelaselasblobv1822stor export const fillOutColors = (d: any, i: number, a: any[]) => { return buildColors(i / (a.length + 1)); }; diff --git a/release-notes/opensearch-anomaly-detection-dashboards.release-notes-2.8.0.0.md b/release-notes/opensearch-anomaly-detection-dashboards.release-notes-2.8.0.0.md new file mode 100644 index 00000000..f523d58e --- /dev/null +++ b/release-notes/opensearch-anomaly-detection-dashboards.release-notes-2.8.0.0.md @@ -0,0 +1,11 @@ +## Version 2.8.0.0 Release Notes + +Compatible with OpenSearch Dashboards 2.8.0 + +### Bug Fixes + +* fixing test to pass with node 18 ([#491](https://github.com/opensearch-project/anomaly-detection-dashboards-plugin/pull/491)) + +### Documentation + +* Updating maintainers and code owners ([#476](https://github.com/opensearch-project/anomaly-detection-dashboards-plugin/pull/476)) \ No newline at end of file From 3dfda672d034adc25a941465b3e89546d5d64e11 Mon Sep 17 00:00:00 2001 From: Amit Galitzky Date: Mon, 10 Jul 2023 10:43:50 -0700 Subject: [PATCH 40/43] Merging main into featureAnywhere with conflicts resolved (#527) * Add branch constants in CI workflow (#345) Signed-off-by: Tyler Ohlsen * Bump decode-uri-component (#359) Signed-off-by: Tyler Ohlsen * removed duplicate popout icon and ran prettier (#382) Signed-off-by: Amit Galitzky * Change detector out of time range modal warning into a callout warning (#384) Signed-off-by: Jackie Han Signed-off-by: Jackie Han * Fix undefined entity list when heatmap is empty (#383) Signed-off-by: Tyler Ohlsen * Updated MAINTAINERS.md to match recommended opensearch-project format. (#388) Signed-off-by: dblock * Add windows env to integration test workflow (#390) Signed-off-by: Tyler Ohlsen * Bump json5 to 2.2.3 (#393) Signed-off-by: Tyler Ohlsen * Add 2.5 release notes (#395) Signed-off-by: Tyler Ohlsen * Update cold start message (#398) Signed-off-by: Kaituo Li * upgrade filter bug (#402) Previously, we didn't actually add filter type when loading old detector. This PR fixed that. Testing done: 1. added a unit tes 2. verified e2et Signed-off-by: Kaituo Li * Changed required minimum intervals in cold start message (#411) Required minimum intervals to finish cold start is related to shingle size. This PR adds the shingle size in the computation and puts the result in the cold start message. Testing done: 1. verified changing shingle size will change the message. Signed-off-by: Kaituo Li * Bump @sideway/formula to 3.0.1 (#418) Signed-off-by: Tyler Ohlsen * Remove auto_expand_replicas override in sample data indices (#423) Signed-off-by: Tyler Ohlsen * Created untriaged issue workflow. (#422) Signed-off-by: dblock * Add 2.6 release notes (#429) Signed-off-by: Tyler Ohlsen * Fix Node.js and Yarn installation in CI (#433) Signed-off-by: Miki * run prettier command against all files (#444) Signed-off-by: Jackie Han * Add 2.7 release notes (#456) Signed-off-by: Jackie Han * updating maintainers and code owners (#476) Signed-off-by: Amit Galitzky * fixing test to pass with node 18 (#491) Signed-off-by: Amit Galitzky * add 2.8.0 release notes (#500) Signed-off-by: Kaituo Li * Remove invalid link (#470) Signed-off-by: Peter Nied Signed-off-by: Peter Nied * Remove global OUI style override (#511) Signed-off-by: Matt Provost --------- Signed-off-by: Tyler Ohlsen Signed-off-by: Amit Galitzky Signed-off-by: Jackie Han Signed-off-by: dblock Signed-off-by: Kaituo Li Signed-off-by: Miki Signed-off-by: Peter Nied Signed-off-by: Peter Nied Signed-off-by: Matt Provost Co-authored-by: Tyler Ohlsen Co-authored-by: Jackie Han Co-authored-by: Daniel (dB.) Doubrovkine Co-authored-by: Kaituo Li Co-authored-by: Miki Co-authored-by: Peter Nied Co-authored-by: Matt Provost From 9d20563fbecc6dcd8be5c1f9955be1e9137a294b Mon Sep 17 00:00:00 2001 From: Jackie Han Date: Mon, 10 Jul 2023 15:36:25 -0700 Subject: [PATCH 41/43] copy files from main to resolve conflicts (#533) Signed-off-by: Jackie Han --- MAINTAINERS.md | 2 +- .../__tests__/anomalyResultUtils.test.ts | 20 ++----------------- test/jest.config.js | 12 +---------- 3 files changed, 4 insertions(+), 30 deletions(-) diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 6a406fb4..9b4551dd 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -19,4 +19,4 @@ This document contains a list of maintainers in this repo. See [opensearch-proje | Yaliang | [ylwu-amzn](https://github.com/ylwu-amzn) | Amazon | | Yizhe Liu | [yizheliu-amazon](https://github.com/yizheliu-amazon) | Amazon | | Vijayan Balasubramanian | [VijayanB](https://github.com/VijayanB) | Amazon | -| Sarat Vemulapalli | [saratvemulapalli](https://github.com/saratvemulapalli) | Amazon | +| Sarat Vemulapalli | [saratvemulapalli](https://github.com/saratvemulapalli) | Amazon | \ No newline at end of file diff --git a/public/pages/utils/__tests__/anomalyResultUtils.test.ts b/public/pages/utils/__tests__/anomalyResultUtils.test.ts index 914c4c1f..cbd73c5b 100644 --- a/public/pages/utils/__tests__/anomalyResultUtils.test.ts +++ b/public/pages/utils/__tests__/anomalyResultUtils.test.ts @@ -12,16 +12,9 @@ import { getFeatureMissingDataAnnotations, getFeatureDataPointsForDetector, - parsePureAnomalies, } from '../anomalyResultUtils'; import { getRandomDetector } from '../../../redux/reducers/__tests__/utils'; -import { - UNITS, - Detector, - FeatureAttributes, - AnomalyData, -} from '../../../models/interfaces'; -import { ANOMALY_RESULT_SUMMARY, PARSED_ANOMALIES } from './constants'; +import { UNITS, Detector, FeatureAttributes } from '../../../models/interfaces'; describe('anomalyResultUtils', () => { let randomDetector_20_min: Detector; @@ -570,13 +563,4 @@ describe('anomalyResultUtils', () => { ).toEqual([]); }); }); - - describe('parsePureAnomalies()', () => { - test('parse anomalies', async () => { - const parsedPureAnomalies: AnomalyData[] = await parsePureAnomalies( - ANOMALY_RESULT_SUMMARY - ); - expect(parsedPureAnomalies).toStrictEqual(PARSED_ANOMALIES); - }); - }); -}); +}); \ No newline at end of file diff --git a/test/jest.config.js b/test/jest.config.js index 242924cc..70c3a53f 100644 --- a/test/jest.config.js +++ b/test/jest.config.js @@ -44,14 +44,4 @@ module.exports = { testPathIgnorePatterns: ['/build/', '/node_modules/'], transformIgnorePatterns: ['/node_modules'], globalSetup: '/global-setup.js', - - /** - * This configuration specifies different file extensions - * and the corresponding transformers to be used - */ - transform: { - '\\.[jt]sx?$': 'babel-jest', - '^.+\\.svg$': '/test/mocks/transformMock.ts', - '^.+\\.html$': '/test/mocks/transformMock.ts', - }, -}; +}; \ No newline at end of file From 8e1fe6775ceb82bd087a81aa96d918bc0456da3b Mon Sep 17 00:00:00 2001 From: Jackie Han Date: Mon, 10 Jul 2023 17:19:01 -0700 Subject: [PATCH 42/43] copy files to resolve conflicts (#538) Signed-off-by: Jackie Han --- .../__tests__/anomalyResultUtils.test.ts | 837 ++++++------------ test/jest.config.js | 2 +- 2 files changed, 294 insertions(+), 545 deletions(-) diff --git a/public/pages/utils/__tests__/anomalyResultUtils.test.ts b/public/pages/utils/__tests__/anomalyResultUtils.test.ts index cbd73c5b..d83cfccf 100644 --- a/public/pages/utils/__tests__/anomalyResultUtils.test.ts +++ b/public/pages/utils/__tests__/anomalyResultUtils.test.ts @@ -9,558 +9,307 @@ * GitHub history for details. */ -import { - getFeatureMissingDataAnnotations, - getFeatureDataPointsForDetector, -} from '../anomalyResultUtils'; +import { getFeatureMissingDataAnnotations, getFeatureDataPointsForDetector } from '../anomalyResultUtils'; import { getRandomDetector } from '../../../redux/reducers/__tests__/utils'; -import { UNITS, Detector, FeatureAttributes } from '../../../models/interfaces'; +import { + UNITS, + Detector, + FeatureAttributes + } from '../../../models/interfaces'; describe('anomalyResultUtils', () => { - let randomDetector_20_min: Detector; - let randomDetector_20_sec: Detector; - let feature_id = 'deny_max'; - beforeAll(() => { - randomDetector_20_min = { - ...getRandomDetector(true), - detectionInterval: { - period: { - interval: 1, - unit: UNITS.MINUTES, - }, - }, - windowDelay: { - period: { - interval: 20, - unit: UNITS.MINUTES, - }, - }, - featureAttributes: [ - { - featureId: feature_id, - featureName: feature_id, - featureEnabled: true, - }, - ] as FeatureAttributes[], - }; - - randomDetector_20_sec = { - ...getRandomDetector(true), - detectionInterval: { - period: { - interval: 1, - unit: UNITS.MINUTES, - }, - }, - windowDelay: { - period: { - interval: 20, - unit: UNITS.SECONDS, - }, - }, - featureAttributes: [ - { - featureId: feature_id, - featureName: feature_id, - featureEnabled: true, - }, - ] as FeatureAttributes[], - }; - }); - describe('getFeatureDataPointsForDetector', () => { - test('returns no missing data with 20 minute window delay', () => { - expect( - getFeatureDataPointsForDetector( - randomDetector_20_min, - { - deny_max: [ - { - startTime: 1655253197686, - endTime: 1655253257686, - plotTime: 1655253257686, - data: 13451, - }, - { - startTime: 1655253137689, - endTime: 1655253197689, - plotTime: 1655253197689, - data: 10973, - }, - { - startTime: 1655253077698, - endTime: 1655253137698, - plotTime: 1655253137698, - data: 11777, - }, - { - startTime: 1655253017690, - endTime: 1655253077690, - plotTime: 1655253077690, - data: 21588, - }, - ], - }, - randomDetector_20_min.detectionInterval.period.interval, - { - startDate: 1655252995079, - endDate: 1655253295079, - }, - true - ) - ).toEqual({ - deny_max: [ - { - isMissing: false, - plotTime: 1655253060000, - startTime: 1655253000000, - endTime: 1655253060000, - }, - { - isMissing: false, - plotTime: 1655253120000, - startTime: 1655253060000, - endTime: 1655253120000, - }, - { - isMissing: false, - plotTime: 1655253180000, - startTime: 1655253120000, - endTime: 1655253180000, - }, - ], - }); - }); - test('returns missing data with 20 minute window delay', () => { - expect( - getFeatureDataPointsForDetector( - randomDetector_20_min, - { - feature_id: [], - }, - randomDetector_20_min.detectionInterval.period.interval, - { - startDate: 1655252995079, - endDate: 1655253295079, - }, - true - ) - ).toEqual({ - deny_max: [ - { - isMissing: true, - plotTime: 1655253060000, - startTime: 1655253000000, - endTime: 1655253060000, - }, - { - isMissing: true, - plotTime: 1655253120000, - startTime: 1655253060000, - endTime: 1655253120000, - }, - { - isMissing: true, - plotTime: 1655253180000, - startTime: 1655253120000, - endTime: 1655253180000, - }, - ], - }); - }); - test('returns missing data with 20 seconds window delay', () => { - expect( - getFeatureDataPointsForDetector( - randomDetector_20_sec, - { - deny_max: [ - { - startTime: 1655245177235, - endTime: 1655245237235, - plotTime: 1655245237235, - data: 14719, + let randomDetector_20_min : Detector; + let randomDetector_20_sec : Detector; + let feature_id = 'deny_max'; + beforeAll(() => { + randomDetector_20_min = { + ...getRandomDetector(true), + detectionInterval: { + period: { + interval: 1, + unit: UNITS.MINUTES, + }, }, - { - startTime: 1655245117232, - endTime: 1655245177232, - plotTime: 1655245177232, - data: 14476, + windowDelay: { + period: { + interval: 20, + unit: UNITS.MINUTES, + }, }, - ], - }, - randomDetector_20_sec.detectionInterval.period.interval, - { - startDate: 1655244944254, - endDate: 1655245244254, - }, - true - ) - ).toEqual({ - deny_max: [ - { - isMissing: true, - plotTime: 1655245020000, - startTime: 1655244960000, - endTime: 1655245020000, - }, - { - isMissing: true, - plotTime: 1655245080000, - startTime: 1655245020000, - endTime: 1655245080000, - }, - { - isMissing: true, - plotTime: 1655245140000, - startTime: 1655245080000, - endTime: 1655245140000, - }, - ], - }); - }); - test('returns partially missing data with 20 seconds window delay', () => { - expect( - getFeatureDataPointsForDetector( - randomDetector_20_sec, - { - deny_max: [ - { - startTime: 1655245357224, - endTime: 1655245417224, - plotTime: 1655245417224, - data: 8675, - }, - { - startTime: 1655245297232, - endTime: 1655245357232, - plotTime: 1655245357232, - data: 9397, - }, - { - startTime: 1655245237231, - endTime: 1655245297231, - plotTime: 1655245297231, - data: 12102, + featureAttributes: [ + { + featureId: feature_id, + featureName: feature_id, + featureEnabled: true, + }, + ] as FeatureAttributes[], + }; + + randomDetector_20_sec= { + ...getRandomDetector(true), + detectionInterval: { + period: { + interval: 1, + unit: UNITS.MINUTES, + }, }, - { - startTime: 1655245177235, - endTime: 1655245237235, - plotTime: 1655245237235, - data: 14719, + windowDelay: { + period: { + interval: 20, + unit: UNITS.SECONDS, + }, }, - ], - }, - randomDetector_20_sec.detectionInterval.period.interval, - { - startDate: 1655245124258, - endDate: 1655245424258, - }, - true - ) - ).toEqual({ - deny_max: [ - { - isMissing: true, - plotTime: 1655245200000, - startTime: 1655245140000, - endTime: 1655245200000, - }, - { - isMissing: false, - plotTime: 1655245260000, - startTime: 1655245200000, - endTime: 1655245260000, - }, - { - isMissing: false, - plotTime: 1655245320000, - startTime: 1655245260000, - endTime: 1655245320000, - }, - ], - }); + featureAttributes: [ + { + featureId: feature_id, + featureName: feature_id, + featureEnabled: true, + }, + ] as FeatureAttributes[], + }; }); - }); - describe('getFeatureMissingDataAnnotations', () => { - test('returns missing data annotation with 20 seconds window delay', () => { - expect( - getFeatureMissingDataAnnotations( - [ - { - startTime: 1654731937236, - endTime: 1654731997236, - plotTime: 1654731997236, - data: 9998, - }, - { - startTime: 1654731877250, - endTime: 1654731937250, - plotTime: 1654731937250, - data: 14841, - }, - { - startTime: 1654731817236, - endTime: 1654731877236, - plotTime: 1654731877236, - data: 6777, - }, - { - startTime: 1654731757234, - endTime: 1654731817234, - plotTime: 1654731817234, - data: 15443, - }, - { - startTime: 1654731697230, - endTime: 1654731757230, - plotTime: 1654731757230, - data: 9612, - }, - { - startTime: 1654731637234, - endTime: 1654731697234, - plotTime: 1654731697234, - data: 13992, - }, - { - startTime: 1654731577232, - endTime: 1654731637232, - plotTime: 1654731637232, - data: 10522, - }, - { - startTime: 1654731517232, - endTime: 1654731577232, - plotTime: 1654731577232, - data: 10945, - }, - ], - randomDetector_20_sec.detectionInterval.period.interval, - randomDetector_20_sec.windowDelay.period, - { - startDate: 1654731477228, - endDate: 1654731697232, - }, - { - startDate: 1654731477228, - endDate: 1654731697232, - }, - false - ) - ).toEqual([ - // our tests use UTC time zone. But in real application, it is local time. - { - dataValue: 1654731540000, - details: - 'There is feature data point missing between 06/08/22 11:38 PM and 06/08/22 11:39 PM', - header: '06/08/22 11:39:00 PM', - }, - ]); + describe('getFeatureDataPointsForDetector', () => { + test('returns no missing data with 20 minute window delay', () => { + expect( + getFeatureDataPointsForDetector( + randomDetector_20_min, + { + "deny_max": + [ + {"startTime":1655253197686,"endTime":1655253257686,"plotTime":1655253257686,"data":13451}, + {"startTime":1655253137689,"endTime":1655253197689,"plotTime":1655253197689,"data":10973}, + {"startTime":1655253077698,"endTime":1655253137698,"plotTime":1655253137698,"data":11777}, + {"startTime":1655253017690,"endTime":1655253077690,"plotTime":1655253077690,"data":21588} + ] + }, + randomDetector_20_min.detectionInterval.period.interval, + { + startDate: 1655252995079, + endDate: 1655253295079 + }, + true + ) + ).toEqual( + { + "deny_max": + [ + {"isMissing":false,"plotTime":1655253060000,"startTime":1655253000000,"endTime":1655253060000}, + {"isMissing":false,"plotTime":1655253120000,"startTime":1655253060000,"endTime":1655253120000}, + {"isMissing":false,"plotTime":1655253180000,"startTime":1655253120000,"endTime":1655253180000} + ] + }); + }); + test('returns missing data with 20 minute window delay', () => { + expect( + getFeatureDataPointsForDetector( + randomDetector_20_min, + { + feature_id:[ + ] + }, + randomDetector_20_min.detectionInterval.period.interval, + { + startDate: 1655252995079, + endDate: 1655253295079 + }, + true + ) + ).toEqual( + { + "deny_max": + [ + {"isMissing":true,"plotTime":1655253060000,"startTime":1655253000000,"endTime":1655253060000}, + {"isMissing":true,"plotTime":1655253120000,"startTime":1655253060000,"endTime":1655253120000}, + {"isMissing":true,"plotTime":1655253180000,"startTime":1655253120000,"endTime":1655253180000} + ] + }); + }); + test('returns missing data with 20 seconds window delay', () => { + expect( + getFeatureDataPointsForDetector( + randomDetector_20_sec, + { + "deny_max":[ + {"startTime":1655245177235,"endTime":1655245237235,"plotTime":1655245237235,"data":14719}, + {"startTime":1655245117232,"endTime":1655245177232,"plotTime":1655245177232,"data":14476}] + }, + randomDetector_20_sec.detectionInterval.period.interval, + { + startDate: 1655244944254, + endDate: 1655245244254 + }, + true + ) + ).toEqual( + { + "deny_max":[ + {"isMissing":true,"plotTime":1655245020000,"startTime":1655244960000,"endTime":1655245020000}, + {"isMissing":true,"plotTime":1655245080000,"startTime":1655245020000,"endTime":1655245080000}, + {"isMissing":true,"plotTime":1655245140000,"startTime":1655245080000,"endTime":1655245140000} + ] + } + ); + }); + test('returns partially missing data with 20 seconds window delay', () => { + expect( + getFeatureDataPointsForDetector( + randomDetector_20_sec, + { + "deny_max":[ + {"startTime":1655245357224,"endTime":1655245417224,"plotTime":1655245417224,"data":8675}, + {"startTime":1655245297232,"endTime":1655245357232,"plotTime":1655245357232,"data":9397}, + {"startTime":1655245237231,"endTime":1655245297231,"plotTime":1655245297231,"data":12102}, + {"startTime":1655245177235,"endTime":1655245237235,"plotTime":1655245237235,"data":14719}] + }, + randomDetector_20_sec.detectionInterval.period.interval, + { + startDate: 1655245124258, + endDate: 1655245424258 + }, + true + ) + ).toEqual( + { + "deny_max":[ + {"isMissing":true,"plotTime":1655245200000,"startTime":1655245140000,"endTime":1655245200000}, + {"isMissing":false,"plotTime":1655245260000,"startTime":1655245200000,"endTime":1655245260000}, + {"isMissing":false,"plotTime":1655245320000,"startTime":1655245260000,"endTime":1655245320000}] + } + ); + }); }); - test('returns no missing data annotation with 20 seconds window delay', () => { - expect( - getFeatureMissingDataAnnotations( - [ - { - startTime: 1655249917234, - endTime: 1655249977234, - plotTime: 1655249977234, - data: 8326, - }, - { - startTime: 1655249857233, - endTime: 1655249917233, - plotTime: 1655249917233, - data: 10953, - }, - { - startTime: 1655249797235, - endTime: 1655249857235, - plotTime: 1655249857235, - data: 14106, - }, - { - startTime: 1655249737234, - endTime: 1655249797234, - plotTime: 1655249797234, - data: 15453, - }, - { - startTime: 1655249677234, - endTime: 1655249737234, - plotTime: 1655249737234, - data: 8721, - }, - { - startTime: 1655249617233, - endTime: 1655249677233, - plotTime: 1655249677233, - data: 8606, - }, - { - startTime: 1655249557233, - endTime: 1655249617233, - plotTime: 1655249617233, - data: 8996, - }, - { - startTime: 1655249497232, - endTime: 1655249557232, - plotTime: 1655249557232, - data: 10809, - }, - { - startTime: 1655249437230, - endTime: 1655249497230, - plotTime: 1655249497230, - data: 5445, - }, - ], - randomDetector_20_sec.detectionInterval.period.interval, - randomDetector_20_sec.windowDelay.period, - { - startDate: 1655249857234, - endDate: 1655250031633, - }, - { - startDate: 1655249857234, - endDate: 1655250031633, - }, - false - ) - ).toEqual([]); + describe('getFeatureMissingDataAnnotations', () => { + test('returns missing data annotation with 20 seconds window delay', () => { + expect( + getFeatureMissingDataAnnotations( + [ + {"startTime":1654731937236,"endTime":1654731997236,"plotTime":1654731997236,"data":9998}, + {"startTime":1654731877250,"endTime":1654731937250,"plotTime":1654731937250,"data":14841}, + {"startTime":1654731817236,"endTime":1654731877236,"plotTime":1654731877236,"data":6777}, + {"startTime":1654731757234,"endTime":1654731817234,"plotTime":1654731817234,"data":15443}, + {"startTime":1654731697230,"endTime":1654731757230,"plotTime":1654731757230,"data":9612}, + {"startTime":1654731637234,"endTime":1654731697234,"plotTime":1654731697234,"data":13992}, + {"startTime":1654731577232,"endTime":1654731637232,"plotTime":1654731637232,"data":10522}, + {"startTime":1654731517232,"endTime":1654731577232,"plotTime":1654731577232,"data":10945} + ], + randomDetector_20_sec.detectionInterval.period.interval, + randomDetector_20_sec.windowDelay.period, + { + startDate: 1654731477228, + endDate: 1654731697232 + }, + { + startDate: 1654731477228, + endDate: 1654731697232 + }, + false + ) + ).toEqual( + [ + // our tests use UTC time zone. But in real application, it is local time. + { + "dataValue":1654731540000, + "details":"There is feature data point missing between 06/08/22 11:38 PM and 06/08/22 11:39 PM", + "header":"06/08/22 11:39:00 PM" + } + ] + ); + }); + test('returns no missing data annotation with 20 seconds window delay', () => { + expect( + getFeatureMissingDataAnnotations( + [ + {"startTime":1655249917234,"endTime":1655249977234,"plotTime":1655249977234,"data":8326}, + {"startTime":1655249857233,"endTime":1655249917233,"plotTime":1655249917233,"data":10953}, + {"startTime":1655249797235,"endTime":1655249857235,"plotTime":1655249857235,"data":14106}, + {"startTime":1655249737234,"endTime":1655249797234,"plotTime":1655249797234,"data":15453}, + {"startTime":1655249677234,"endTime":1655249737234,"plotTime":1655249737234,"data":8721}, + {"startTime":1655249617233,"endTime":1655249677233,"plotTime":1655249677233,"data":8606}, + {"startTime":1655249557233,"endTime":1655249617233,"plotTime":1655249617233,"data":8996}, + {"startTime":1655249497232,"endTime":1655249557232,"plotTime":1655249557232,"data":10809}, + {"startTime":1655249437230,"endTime":1655249497230,"plotTime":1655249497230,"data":5445} + ], + randomDetector_20_sec.detectionInterval.period.interval, + randomDetector_20_sec.windowDelay.period, + { + startDate: 1655249857234, + endDate: 1655250031633 + }, + { + startDate: 1655249857234, + endDate: 1655250031633 + }, + false + ) + ).toEqual( + [] + ); + }); + test('returns missing data annotation with 20 minutes window delay', () => { + expect( + getFeatureMissingDataAnnotations( + // timestamps in descending order + [ + {"startTime":1654652417693,"endTime":1654652477693,"plotTime":1654652477693,"data":9050}, + {"startTime":1654652357688,"endTime":1654652417688,"plotTime":1654652417688,"data":13895}, + {"startTime":1654652297691,"endTime":1654652357691,"plotTime":1654652357691,"data":11362}, + {"startTime":1654652237690,"endTime":1654652297690,"plotTime":1654652297690,"data":13253}, + {"startTime":1654652177690,"endTime":1654652237690,"plotTime":1654652237690,"data":15658}, + {"startTime":1654652117689,"endTime":1654652177689,"plotTime":1654652177689,"data":10015}, + {"startTime":1654652057688,"endTime":1654652117688,"plotTime":1654652117688,"data":12291} + ], + randomDetector_20_min.detectionInterval.period.interval, + randomDetector_20_min.windowDelay.period, + { + startDate: 1654651997688, + endDate: 1654653617693 + }, + { + startDate: 1654651997688, + endDate: 1654653617693 + }, + false + ) + ).toEqual( + // our tests use UTC time zone. But in real application, it is local time. + [ + { + "dataValue":1654652040000, + "details":"There is feature data point missing between 06/08/22 1:33 AM and 06/08/22 1:34 AM", + "header":"06/08/22 01:34:00 AM" + } + ] + ); + }); + test('returns no missing data annotation with 20 minutes window delay', () => { + expect( + getFeatureMissingDataAnnotations( + [ + {"startTime":1655250437690,"endTime":1655250497690,"plotTime":1655250497690,"data":13888}, + {"startTime":1655250377688,"endTime":1655250437688,"plotTime":1655250437688,"data":8246}, + {"startTime":1655250317687,"endTime":1655250377687,"plotTime":1655250377687,"data":16812}, + {"startTime":1655250257691,"endTime":1655250317691,"plotTime":1655250317691,"data":9834}, + {"startTime":1655250197688,"endTime":1655250257688,"plotTime":1655250257688,"data":12409}, + {"startTime":1655250137686,"endTime":1655250197686,"plotTime":1655250197686,"data":14615}, + {"startTime":1655250077703,"endTime":1655250137703,"plotTime":1655250137703,"data":8377} + ], + randomDetector_20_min.detectionInterval.period.interval, + randomDetector_20_min.windowDelay.period, + { + startDate: 1655250377690, + endDate: 1655251724454 + }, + { + startDate: 1655250377690, + endDate: 1655251724454 + }, + false + ) + ).toEqual( + [] + ); + }); }); - test('returns missing data annotation with 20 minutes window delay', () => { - expect( - getFeatureMissingDataAnnotations( - // timestamps in descending order - [ - { - startTime: 1654652417693, - endTime: 1654652477693, - plotTime: 1654652477693, - data: 9050, - }, - { - startTime: 1654652357688, - endTime: 1654652417688, - plotTime: 1654652417688, - data: 13895, - }, - { - startTime: 1654652297691, - endTime: 1654652357691, - plotTime: 1654652357691, - data: 11362, - }, - { - startTime: 1654652237690, - endTime: 1654652297690, - plotTime: 1654652297690, - data: 13253, - }, - { - startTime: 1654652177690, - endTime: 1654652237690, - plotTime: 1654652237690, - data: 15658, - }, - { - startTime: 1654652117689, - endTime: 1654652177689, - plotTime: 1654652177689, - data: 10015, - }, - { - startTime: 1654652057688, - endTime: 1654652117688, - plotTime: 1654652117688, - data: 12291, - }, - ], - randomDetector_20_min.detectionInterval.period.interval, - randomDetector_20_min.windowDelay.period, - { - startDate: 1654651997688, - endDate: 1654653617693, - }, - { - startDate: 1654651997688, - endDate: 1654653617693, - }, - false - ) - ).toEqual( - // our tests use UTC time zone. But in real application, it is local time. - [ - { - dataValue: 1654652040000, - details: - 'There is feature data point missing between 06/08/22 1:33 AM and 06/08/22 1:34 AM', - header: '06/08/22 01:34:00 AM', - }, - ] - ); - }); - test('returns no missing data annotation with 20 minutes window delay', () => { - expect( - getFeatureMissingDataAnnotations( - [ - { - startTime: 1655250437690, - endTime: 1655250497690, - plotTime: 1655250497690, - data: 13888, - }, - { - startTime: 1655250377688, - endTime: 1655250437688, - plotTime: 1655250437688, - data: 8246, - }, - { - startTime: 1655250317687, - endTime: 1655250377687, - plotTime: 1655250377687, - data: 16812, - }, - { - startTime: 1655250257691, - endTime: 1655250317691, - plotTime: 1655250317691, - data: 9834, - }, - { - startTime: 1655250197688, - endTime: 1655250257688, - plotTime: 1655250257688, - data: 12409, - }, - { - startTime: 1655250137686, - endTime: 1655250197686, - plotTime: 1655250197686, - data: 14615, - }, - { - startTime: 1655250077703, - endTime: 1655250137703, - plotTime: 1655250137703, - data: 8377, - }, - ], - randomDetector_20_min.detectionInterval.period.interval, - randomDetector_20_min.windowDelay.period, - { - startDate: 1655250377690, - endDate: 1655251724454, - }, - { - startDate: 1655250377690, - endDate: 1655251724454, - }, - false - ) - ).toEqual([]); - }); - }); + + }); \ No newline at end of file diff --git a/test/jest.config.js b/test/jest.config.js index 70c3a53f..b58f8f5c 100644 --- a/test/jest.config.js +++ b/test/jest.config.js @@ -43,5 +43,5 @@ module.exports = { modulePathIgnorePatterns: ['/offline-module-cache/'], testPathIgnorePatterns: ['/build/', '/node_modules/'], transformIgnorePatterns: ['/node_modules'], - globalSetup: '/global-setup.js', + globalSetup: "/global-setup.js", }; \ No newline at end of file From d5c2def98b733bb1e2406f74abce269cb28e8bdd Mon Sep 17 00:00:00 2001 From: Jackie Han Date: Mon, 10 Jul 2023 17:57:18 -0700 Subject: [PATCH 43/43] Feature anywhere (#540) * Add branch constants in CI workflow (#345) Signed-off-by: Tyler Ohlsen * Bump decode-uri-component (#359) Signed-off-by: Tyler Ohlsen * removed duplicate popout icon and ran prettier (#382) Signed-off-by: Amit Galitzky * Change detector out of time range modal warning into a callout warning (#384) Signed-off-by: Jackie Han Signed-off-by: Jackie Han * Fix undefined entity list when heatmap is empty (#383) Signed-off-by: Tyler Ohlsen * Updated MAINTAINERS.md to match recommended opensearch-project format. (#388) Signed-off-by: dblock * Add windows env to integration test workflow (#390) Signed-off-by: Tyler Ohlsen * Bump json5 to 2.2.3 (#393) Signed-off-by: Tyler Ohlsen * Add 2.5 release notes (#395) Signed-off-by: Tyler Ohlsen * Update cold start message (#398) Signed-off-by: Kaituo Li * upgrade filter bug (#402) Previously, we didn't actually add filter type when loading old detector. This PR fixed that. Testing done: 1. added a unit tes 2. verified e2et Signed-off-by: Kaituo Li * Changed required minimum intervals in cold start message (#411) Required minimum intervals to finish cold start is related to shingle size. This PR adds the shingle size in the computation and puts the result in the cold start message. Testing done: 1. verified changing shingle size will change the message. Signed-off-by: Kaituo Li * Bump @sideway/formula to 3.0.1 (#418) Signed-off-by: Tyler Ohlsen * Remove auto_expand_replicas override in sample data indices (#423) Signed-off-by: Tyler Ohlsen * Created untriaged issue workflow. (#422) Signed-off-by: dblock * Add 2.6 release notes (#429) Signed-off-by: Tyler Ohlsen * Fix Node.js and Yarn installation in CI (#433) Signed-off-by: Miki * run prettier command against all files (#444) Signed-off-by: Jackie Han * Add 2.7 release notes (#456) Signed-off-by: Jackie Han * updating maintainers and code owners (#476) Signed-off-by: Amit Galitzky * fixing test to pass with node 18 (#491) Signed-off-by: Amit Galitzky * add 2.8.0 release notes (#500) Signed-off-by: Kaituo Li * Remove invalid link (#470) Signed-off-by: Peter Nied Signed-off-by: Peter Nied * Remove global OUI style override (#511) Signed-off-by: Matt Provost --------- Signed-off-by: Tyler Ohlsen Signed-off-by: Amit Galitzky Signed-off-by: Jackie Han Signed-off-by: dblock Signed-off-by: Kaituo Li Signed-off-by: Miki Signed-off-by: Peter Nied Signed-off-by: Peter Nied Signed-off-by: Matt Provost Co-authored-by: Tyler Ohlsen Co-authored-by: Amit Galitzky Co-authored-by: Daniel (dB.) Doubrovkine Co-authored-by: Kaituo Li Co-authored-by: Miki Co-authored-by: Peter Nied Co-authored-by: Matt Provost --- .../__tests__/anomalyResultUtils.test.ts | 839 ++++++++++++------ test/jest.config.js | 4 +- 2 files changed, 547 insertions(+), 296 deletions(-) diff --git a/public/pages/utils/__tests__/anomalyResultUtils.test.ts b/public/pages/utils/__tests__/anomalyResultUtils.test.ts index d83cfccf..65ebc8d6 100644 --- a/public/pages/utils/__tests__/anomalyResultUtils.test.ts +++ b/public/pages/utils/__tests__/anomalyResultUtils.test.ts @@ -9,307 +9,558 @@ * GitHub history for details. */ -import { getFeatureMissingDataAnnotations, getFeatureDataPointsForDetector } from '../anomalyResultUtils'; -import { getRandomDetector } from '../../../redux/reducers/__tests__/utils'; import { - UNITS, - Detector, - FeatureAttributes - } from '../../../models/interfaces'; + getFeatureMissingDataAnnotations, + getFeatureDataPointsForDetector, +} from '../anomalyResultUtils'; +import { getRandomDetector } from '../../../redux/reducers/__tests__/utils'; +import { UNITS, Detector, FeatureAttributes } from '../../../models/interfaces'; describe('anomalyResultUtils', () => { - let randomDetector_20_min : Detector; - let randomDetector_20_sec : Detector; - let feature_id = 'deny_max'; - beforeAll(() => { - randomDetector_20_min = { - ...getRandomDetector(true), - detectionInterval: { - period: { - interval: 1, - unit: UNITS.MINUTES, - }, + let randomDetector_20_min: Detector; + let randomDetector_20_sec: Detector; + let feature_id = 'deny_max'; + beforeAll(() => { + randomDetector_20_min = { + ...getRandomDetector(true), + detectionInterval: { + period: { + interval: 1, + unit: UNITS.MINUTES, + }, + }, + windowDelay: { + period: { + interval: 20, + unit: UNITS.MINUTES, + }, + }, + featureAttributes: [ + { + featureId: feature_id, + featureName: feature_id, + featureEnabled: true, + }, + ] as FeatureAttributes[], + }; + + randomDetector_20_sec = { + ...getRandomDetector(true), + detectionInterval: { + period: { + interval: 1, + unit: UNITS.MINUTES, + }, + }, + windowDelay: { + period: { + interval: 20, + unit: UNITS.SECONDS, + }, + }, + featureAttributes: [ + { + featureId: feature_id, + featureName: feature_id, + featureEnabled: true, + }, + ] as FeatureAttributes[], + }; + }); + describe('getFeatureDataPointsForDetector', () => { + test('returns no missing data with 20 minute window delay', () => { + expect( + getFeatureDataPointsForDetector( + randomDetector_20_min, + { + deny_max: [ + { + startTime: 1655253197686, + endTime: 1655253257686, + plotTime: 1655253257686, + data: 13451, }, - windowDelay: { - period: { - interval: 20, - unit: UNITS.MINUTES, - }, + { + startTime: 1655253137689, + endTime: 1655253197689, + plotTime: 1655253197689, + data: 10973, }, - featureAttributes: [ - { - featureId: feature_id, - featureName: feature_id, - featureEnabled: true, - }, - ] as FeatureAttributes[], - }; - - randomDetector_20_sec= { - ...getRandomDetector(true), - detectionInterval: { - period: { - interval: 1, - unit: UNITS.MINUTES, - }, + { + startTime: 1655253077698, + endTime: 1655253137698, + plotTime: 1655253137698, + data: 11777, }, - windowDelay: { - period: { - interval: 20, - unit: UNITS.SECONDS, - }, + { + startTime: 1655253017690, + endTime: 1655253077690, + plotTime: 1655253077690, + data: 21588, }, - featureAttributes: [ - { - featureId: feature_id, - featureName: feature_id, - featureEnabled: true, - }, - ] as FeatureAttributes[], - }; + ], + }, + randomDetector_20_min.detectionInterval.period.interval, + { + startDate: 1655252995079, + endDate: 1655253295079, + }, + true + ) + ).toEqual({ + deny_max: [ + { + isMissing: false, + plotTime: 1655253060000, + startTime: 1655253000000, + endTime: 1655253060000, + }, + { + isMissing: false, + plotTime: 1655253120000, + startTime: 1655253060000, + endTime: 1655253120000, + }, + { + isMissing: false, + plotTime: 1655253180000, + startTime: 1655253120000, + endTime: 1655253180000, + }, + ], + }); }); - describe('getFeatureDataPointsForDetector', () => { - test('returns no missing data with 20 minute window delay', () => { - expect( - getFeatureDataPointsForDetector( - randomDetector_20_min, - { - "deny_max": - [ - {"startTime":1655253197686,"endTime":1655253257686,"plotTime":1655253257686,"data":13451}, - {"startTime":1655253137689,"endTime":1655253197689,"plotTime":1655253197689,"data":10973}, - {"startTime":1655253077698,"endTime":1655253137698,"plotTime":1655253137698,"data":11777}, - {"startTime":1655253017690,"endTime":1655253077690,"plotTime":1655253077690,"data":21588} - ] - }, - randomDetector_20_min.detectionInterval.period.interval, - { - startDate: 1655252995079, - endDate: 1655253295079 - }, - true - ) - ).toEqual( - { - "deny_max": - [ - {"isMissing":false,"plotTime":1655253060000,"startTime":1655253000000,"endTime":1655253060000}, - {"isMissing":false,"plotTime":1655253120000,"startTime":1655253060000,"endTime":1655253120000}, - {"isMissing":false,"plotTime":1655253180000,"startTime":1655253120000,"endTime":1655253180000} - ] - }); - }); - test('returns missing data with 20 minute window delay', () => { - expect( - getFeatureDataPointsForDetector( - randomDetector_20_min, - { - feature_id:[ - ] - }, - randomDetector_20_min.detectionInterval.period.interval, - { - startDate: 1655252995079, - endDate: 1655253295079 - }, - true - ) - ).toEqual( - { - "deny_max": - [ - {"isMissing":true,"plotTime":1655253060000,"startTime":1655253000000,"endTime":1655253060000}, - {"isMissing":true,"plotTime":1655253120000,"startTime":1655253060000,"endTime":1655253120000}, - {"isMissing":true,"plotTime":1655253180000,"startTime":1655253120000,"endTime":1655253180000} - ] - }); - }); - test('returns missing data with 20 seconds window delay', () => { - expect( - getFeatureDataPointsForDetector( - randomDetector_20_sec, - { - "deny_max":[ - {"startTime":1655245177235,"endTime":1655245237235,"plotTime":1655245237235,"data":14719}, - {"startTime":1655245117232,"endTime":1655245177232,"plotTime":1655245177232,"data":14476}] - }, - randomDetector_20_sec.detectionInterval.period.interval, - { - startDate: 1655244944254, - endDate: 1655245244254 - }, - true - ) - ).toEqual( - { - "deny_max":[ - {"isMissing":true,"plotTime":1655245020000,"startTime":1655244960000,"endTime":1655245020000}, - {"isMissing":true,"plotTime":1655245080000,"startTime":1655245020000,"endTime":1655245080000}, - {"isMissing":true,"plotTime":1655245140000,"startTime":1655245080000,"endTime":1655245140000} - ] - } - ); - }); - test('returns partially missing data with 20 seconds window delay', () => { - expect( - getFeatureDataPointsForDetector( - randomDetector_20_sec, - { - "deny_max":[ - {"startTime":1655245357224,"endTime":1655245417224,"plotTime":1655245417224,"data":8675}, - {"startTime":1655245297232,"endTime":1655245357232,"plotTime":1655245357232,"data":9397}, - {"startTime":1655245237231,"endTime":1655245297231,"plotTime":1655245297231,"data":12102}, - {"startTime":1655245177235,"endTime":1655245237235,"plotTime":1655245237235,"data":14719}] - }, - randomDetector_20_sec.detectionInterval.period.interval, - { - startDate: 1655245124258, - endDate: 1655245424258 - }, - true - ) - ).toEqual( - { - "deny_max":[ - {"isMissing":true,"plotTime":1655245200000,"startTime":1655245140000,"endTime":1655245200000}, - {"isMissing":false,"plotTime":1655245260000,"startTime":1655245200000,"endTime":1655245260000}, - {"isMissing":false,"plotTime":1655245320000,"startTime":1655245260000,"endTime":1655245320000}] - } - ); - }); + test('returns missing data with 20 minute window delay', () => { + expect( + getFeatureDataPointsForDetector( + randomDetector_20_min, + { + feature_id: [], + }, + randomDetector_20_min.detectionInterval.period.interval, + { + startDate: 1655252995079, + endDate: 1655253295079, + }, + true + ) + ).toEqual({ + deny_max: [ + { + isMissing: true, + plotTime: 1655253060000, + startTime: 1655253000000, + endTime: 1655253060000, + }, + { + isMissing: true, + plotTime: 1655253120000, + startTime: 1655253060000, + endTime: 1655253120000, + }, + { + isMissing: true, + plotTime: 1655253180000, + startTime: 1655253120000, + endTime: 1655253180000, + }, + ], + }); }); - describe('getFeatureMissingDataAnnotations', () => { - test('returns missing data annotation with 20 seconds window delay', () => { - expect( - getFeatureMissingDataAnnotations( - [ - {"startTime":1654731937236,"endTime":1654731997236,"plotTime":1654731997236,"data":9998}, - {"startTime":1654731877250,"endTime":1654731937250,"plotTime":1654731937250,"data":14841}, - {"startTime":1654731817236,"endTime":1654731877236,"plotTime":1654731877236,"data":6777}, - {"startTime":1654731757234,"endTime":1654731817234,"plotTime":1654731817234,"data":15443}, - {"startTime":1654731697230,"endTime":1654731757230,"plotTime":1654731757230,"data":9612}, - {"startTime":1654731637234,"endTime":1654731697234,"plotTime":1654731697234,"data":13992}, - {"startTime":1654731577232,"endTime":1654731637232,"plotTime":1654731637232,"data":10522}, - {"startTime":1654731517232,"endTime":1654731577232,"plotTime":1654731577232,"data":10945} - ], - randomDetector_20_sec.detectionInterval.period.interval, - randomDetector_20_sec.windowDelay.period, - { - startDate: 1654731477228, - endDate: 1654731697232 - }, - { - startDate: 1654731477228, - endDate: 1654731697232 - }, - false - ) - ).toEqual( - [ - // our tests use UTC time zone. But in real application, it is local time. - { - "dataValue":1654731540000, - "details":"There is feature data point missing between 06/08/22 11:38 PM and 06/08/22 11:39 PM", - "header":"06/08/22 11:39:00 PM" - } - ] - ); - }); - test('returns no missing data annotation with 20 seconds window delay', () => { - expect( - getFeatureMissingDataAnnotations( - [ - {"startTime":1655249917234,"endTime":1655249977234,"plotTime":1655249977234,"data":8326}, - {"startTime":1655249857233,"endTime":1655249917233,"plotTime":1655249917233,"data":10953}, - {"startTime":1655249797235,"endTime":1655249857235,"plotTime":1655249857235,"data":14106}, - {"startTime":1655249737234,"endTime":1655249797234,"plotTime":1655249797234,"data":15453}, - {"startTime":1655249677234,"endTime":1655249737234,"plotTime":1655249737234,"data":8721}, - {"startTime":1655249617233,"endTime":1655249677233,"plotTime":1655249677233,"data":8606}, - {"startTime":1655249557233,"endTime":1655249617233,"plotTime":1655249617233,"data":8996}, - {"startTime":1655249497232,"endTime":1655249557232,"plotTime":1655249557232,"data":10809}, - {"startTime":1655249437230,"endTime":1655249497230,"plotTime":1655249497230,"data":5445} - ], - randomDetector_20_sec.detectionInterval.period.interval, - randomDetector_20_sec.windowDelay.period, - { - startDate: 1655249857234, - endDate: 1655250031633 - }, - { - startDate: 1655249857234, - endDate: 1655250031633 - }, - false - ) - ).toEqual( - [] - ); - }); - test('returns missing data annotation with 20 minutes window delay', () => { - expect( - getFeatureMissingDataAnnotations( - // timestamps in descending order - [ - {"startTime":1654652417693,"endTime":1654652477693,"plotTime":1654652477693,"data":9050}, - {"startTime":1654652357688,"endTime":1654652417688,"plotTime":1654652417688,"data":13895}, - {"startTime":1654652297691,"endTime":1654652357691,"plotTime":1654652357691,"data":11362}, - {"startTime":1654652237690,"endTime":1654652297690,"plotTime":1654652297690,"data":13253}, - {"startTime":1654652177690,"endTime":1654652237690,"plotTime":1654652237690,"data":15658}, - {"startTime":1654652117689,"endTime":1654652177689,"plotTime":1654652177689,"data":10015}, - {"startTime":1654652057688,"endTime":1654652117688,"plotTime":1654652117688,"data":12291} - ], - randomDetector_20_min.detectionInterval.period.interval, - randomDetector_20_min.windowDelay.period, - { - startDate: 1654651997688, - endDate: 1654653617693 - }, - { - startDate: 1654651997688, - endDate: 1654653617693 - }, - false - ) - ).toEqual( - // our tests use UTC time zone. But in real application, it is local time. - [ - { - "dataValue":1654652040000, - "details":"There is feature data point missing between 06/08/22 1:33 AM and 06/08/22 1:34 AM", - "header":"06/08/22 01:34:00 AM" - } - ] - ); - }); - test('returns no missing data annotation with 20 minutes window delay', () => { - expect( - getFeatureMissingDataAnnotations( - [ - {"startTime":1655250437690,"endTime":1655250497690,"plotTime":1655250497690,"data":13888}, - {"startTime":1655250377688,"endTime":1655250437688,"plotTime":1655250437688,"data":8246}, - {"startTime":1655250317687,"endTime":1655250377687,"plotTime":1655250377687,"data":16812}, - {"startTime":1655250257691,"endTime":1655250317691,"plotTime":1655250317691,"data":9834}, - {"startTime":1655250197688,"endTime":1655250257688,"plotTime":1655250257688,"data":12409}, - {"startTime":1655250137686,"endTime":1655250197686,"plotTime":1655250197686,"data":14615}, - {"startTime":1655250077703,"endTime":1655250137703,"plotTime":1655250137703,"data":8377} - ], - randomDetector_20_min.detectionInterval.period.interval, - randomDetector_20_min.windowDelay.period, - { - startDate: 1655250377690, - endDate: 1655251724454 - }, - { - startDate: 1655250377690, - endDate: 1655251724454 - }, - false - ) - ).toEqual( - [] - ); - }); + test('returns missing data with 20 seconds window delay', () => { + expect( + getFeatureDataPointsForDetector( + randomDetector_20_sec, + { + deny_max: [ + { + startTime: 1655245177235, + endTime: 1655245237235, + plotTime: 1655245237235, + data: 14719, + }, + { + startTime: 1655245117232, + endTime: 1655245177232, + plotTime: 1655245177232, + data: 14476, + }, + ], + }, + randomDetector_20_sec.detectionInterval.period.interval, + { + startDate: 1655244944254, + endDate: 1655245244254, + }, + true + ) + ).toEqual({ + deny_max: [ + { + isMissing: true, + plotTime: 1655245020000, + startTime: 1655244960000, + endTime: 1655245020000, + }, + { + isMissing: true, + plotTime: 1655245080000, + startTime: 1655245020000, + endTime: 1655245080000, + }, + { + isMissing: true, + plotTime: 1655245140000, + startTime: 1655245080000, + endTime: 1655245140000, + }, + ], + }); }); - - -}); \ No newline at end of file + test('returns partially missing data with 20 seconds window delay', () => { + expect( + getFeatureDataPointsForDetector( + randomDetector_20_sec, + { + deny_max: [ + { + startTime: 1655245357224, + endTime: 1655245417224, + plotTime: 1655245417224, + data: 8675, + }, + { + startTime: 1655245297232, + endTime: 1655245357232, + plotTime: 1655245357232, + data: 9397, + }, + { + startTime: 1655245237231, + endTime: 1655245297231, + plotTime: 1655245297231, + data: 12102, + }, + { + startTime: 1655245177235, + endTime: 1655245237235, + plotTime: 1655245237235, + data: 14719, + }, + ], + }, + randomDetector_20_sec.detectionInterval.period.interval, + { + startDate: 1655245124258, + endDate: 1655245424258, + }, + true + ) + ).toEqual({ + deny_max: [ + { + isMissing: true, + plotTime: 1655245200000, + startTime: 1655245140000, + endTime: 1655245200000, + }, + { + isMissing: false, + plotTime: 1655245260000, + startTime: 1655245200000, + endTime: 1655245260000, + }, + { + isMissing: false, + plotTime: 1655245320000, + startTime: 1655245260000, + endTime: 1655245320000, + }, + ], + }); + }); + }); + describe('getFeatureMissingDataAnnotations', () => { + test('returns missing data annotation with 20 seconds window delay', () => { + expect( + getFeatureMissingDataAnnotations( + [ + { + startTime: 1654731937236, + endTime: 1654731997236, + plotTime: 1654731997236, + data: 9998, + }, + { + startTime: 1654731877250, + endTime: 1654731937250, + plotTime: 1654731937250, + data: 14841, + }, + { + startTime: 1654731817236, + endTime: 1654731877236, + plotTime: 1654731877236, + data: 6777, + }, + { + startTime: 1654731757234, + endTime: 1654731817234, + plotTime: 1654731817234, + data: 15443, + }, + { + startTime: 1654731697230, + endTime: 1654731757230, + plotTime: 1654731757230, + data: 9612, + }, + { + startTime: 1654731637234, + endTime: 1654731697234, + plotTime: 1654731697234, + data: 13992, + }, + { + startTime: 1654731577232, + endTime: 1654731637232, + plotTime: 1654731637232, + data: 10522, + }, + { + startTime: 1654731517232, + endTime: 1654731577232, + plotTime: 1654731577232, + data: 10945, + }, + ], + randomDetector_20_sec.detectionInterval.period.interval, + randomDetector_20_sec.windowDelay.period, + { + startDate: 1654731477228, + endDate: 1654731697232, + }, + { + startDate: 1654731477228, + endDate: 1654731697232, + }, + false + ) + ).toEqual([ + // our tests use UTC time zone. But in real application, it is local time. + { + dataValue: 1654731540000, + details: + 'There is feature data point missing between 06/08/22 11:38 PM and 06/08/22 11:39 PM', + header: '06/08/22 11:39:00 PM', + }, + ]); + }); + test('returns no missing data annotation with 20 seconds window delay', () => { + expect( + getFeatureMissingDataAnnotations( + [ + { + startTime: 1655249917234, + endTime: 1655249977234, + plotTime: 1655249977234, + data: 8326, + }, + { + startTime: 1655249857233, + endTime: 1655249917233, + plotTime: 1655249917233, + data: 10953, + }, + { + startTime: 1655249797235, + endTime: 1655249857235, + plotTime: 1655249857235, + data: 14106, + }, + { + startTime: 1655249737234, + endTime: 1655249797234, + plotTime: 1655249797234, + data: 15453, + }, + { + startTime: 1655249677234, + endTime: 1655249737234, + plotTime: 1655249737234, + data: 8721, + }, + { + startTime: 1655249617233, + endTime: 1655249677233, + plotTime: 1655249677233, + data: 8606, + }, + { + startTime: 1655249557233, + endTime: 1655249617233, + plotTime: 1655249617233, + data: 8996, + }, + { + startTime: 1655249497232, + endTime: 1655249557232, + plotTime: 1655249557232, + data: 10809, + }, + { + startTime: 1655249437230, + endTime: 1655249497230, + plotTime: 1655249497230, + data: 5445, + }, + ], + randomDetector_20_sec.detectionInterval.period.interval, + randomDetector_20_sec.windowDelay.period, + { + startDate: 1655249857234, + endDate: 1655250031633, + }, + { + startDate: 1655249857234, + endDate: 1655250031633, + }, + false + ) + ).toEqual([]); + }); + test('returns missing data annotation with 20 minutes window delay', () => { + expect( + getFeatureMissingDataAnnotations( + // timestamps in descending order + [ + { + startTime: 1654652417693, + endTime: 1654652477693, + plotTime: 1654652477693, + data: 9050, + }, + { + startTime: 1654652357688, + endTime: 1654652417688, + plotTime: 1654652417688, + data: 13895, + }, + { + startTime: 1654652297691, + endTime: 1654652357691, + plotTime: 1654652357691, + data: 11362, + }, + { + startTime: 1654652237690, + endTime: 1654652297690, + plotTime: 1654652297690, + data: 13253, + }, + { + startTime: 1654652177690, + endTime: 1654652237690, + plotTime: 1654652237690, + data: 15658, + }, + { + startTime: 1654652117689, + endTime: 1654652177689, + plotTime: 1654652177689, + data: 10015, + }, + { + startTime: 1654652057688, + endTime: 1654652117688, + plotTime: 1654652117688, + data: 12291, + }, + ], + randomDetector_20_min.detectionInterval.period.interval, + randomDetector_20_min.windowDelay.period, + { + startDate: 1654651997688, + endDate: 1654653617693, + }, + { + startDate: 1654651997688, + endDate: 1654653617693, + }, + false + ) + ).toEqual( + // our tests use UTC time zone. But in real application, it is local time. + [ + { + dataValue: 1654652040000, + details: + 'There is feature data point missing between 06/08/22 1:33 AM and 06/08/22 1:34 AM', + header: '06/08/22 01:34:00 AM', + }, + ] + ); + }); + test('returns no missing data annotation with 20 minutes window delay', () => { + expect( + getFeatureMissingDataAnnotations( + [ + { + startTime: 1655250437690, + endTime: 1655250497690, + plotTime: 1655250497690, + data: 13888, + }, + { + startTime: 1655250377688, + endTime: 1655250437688, + plotTime: 1655250437688, + data: 8246, + }, + { + startTime: 1655250317687, + endTime: 1655250377687, + plotTime: 1655250377687, + data: 16812, + }, + { + startTime: 1655250257691, + endTime: 1655250317691, + plotTime: 1655250317691, + data: 9834, + }, + { + startTime: 1655250197688, + endTime: 1655250257688, + plotTime: 1655250257688, + data: 12409, + }, + { + startTime: 1655250137686, + endTime: 1655250197686, + plotTime: 1655250197686, + data: 14615, + }, + { + startTime: 1655250077703, + endTime: 1655250137703, + plotTime: 1655250137703, + data: 8377, + }, + ], + randomDetector_20_min.detectionInterval.period.interval, + randomDetector_20_min.windowDelay.period, + { + startDate: 1655250377690, + endDate: 1655251724454, + }, + { + startDate: 1655250377690, + endDate: 1655251724454, + }, + false + ) + ).toEqual([]); + }); + }); +}); diff --git a/test/jest.config.js b/test/jest.config.js index b58f8f5c..c726f920 100644 --- a/test/jest.config.js +++ b/test/jest.config.js @@ -43,5 +43,5 @@ module.exports = { modulePathIgnorePatterns: ['/offline-module-cache/'], testPathIgnorePatterns: ['/build/', '/node_modules/'], transformIgnorePatterns: ['/node_modules'], - globalSetup: "/global-setup.js", -}; \ No newline at end of file + globalSetup: '/global-setup.js', +};