Skip to content

Commit 95fab68

Browse files
committed
Add Suggest parameters button + move operational settings to Configure Model
This PR centralizes operational settings in the model step as we can call suggest API. Suggest API works better when we know features and categorical fields. This PR also adds an assisted setup via suggestions, and corrects documentation links. Specifically - UI - Add SuggestParametersDialog to recommend detection interval, frequency, history, and window delay; apply suggestions to the form. - Introduce Settings panel in Configure Model with Interval, Frequency, Window delay, and History fields. - Validate frequency as a multiple of interval; add validateMultipleOf util. - Move interval/window delay out of Define Detector (remove DefineDetector/Settings and tests). - Show read-only OperationalSettings in Review & Create; remove interval/window delay display from DetectorDefinitionFields. - Replace BASE_DOCS_LINK usages with AD_DOCS_LINK; add ALERTING_DOCS_LINK. - Replace BASE_DOCS_LINK usages with AD_DOCS_LINK; add ALERTING_DOCS_LINK. - Add error in detector status if any. Otherwise, if a detector is stopped, users won't know why unless they know how to run get detector API with required parameter. - Server/Redux - Add POST /detectors/_suggest/{suggestType}[/{dataSourceId}] route. - Add cluster client method ad.suggestDetector. - Add suggestDetector Redux action. - Models/Helpers - Extend Detector with optional frequency and history. - Map interval/windowDelay/frequency/history in formikToDetector and model helpers (conditionally persisted). - Pass operational settings through preview/sample anomalies flow. - Misc - Update snapshots and tests due to UI changes. - Minor lint/typing cleanups (commas, docs links). Testing done: 1. fixed broken IT and UT. Will publish IT fix in functional test repo. 2. manual test. Signed-off-by: kaituo <[email protected]>
1 parent d179675 commit 95fab68

File tree

70 files changed

+4871
-2441
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+4871
-2441
lines changed

public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AssociateExisting/containers/AssociateExisting.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import {
4444
} from '../../../../../../../../src/plugins/vis_augmenter/public';
4545
import { stateToColorMap } from '../../../../../pages/utils/constants';
4646
import {
47-
BASE_DOCS_LINK,
47+
AD_DOCS_LINK,
4848
PLUGIN_NAME,
4949
} from '../../../../../../public/utils/constants';
5050
import { renderTime } from '../../../../../../public/pages/DetectorsList/utils/tableUtils';
@@ -211,7 +211,7 @@ export function AssociateExisting(
211211
View existing anomaly detectors across your system and add the
212212
detector(s) to a dashboard and visualization.{' '}
213213
<a
214-
href={`${BASE_DOCS_LINK}/ad`}
214+
href={`${AD_DOCS_LINK}`}
215215
target="_blank"
216216
style={{ display: 'inline-block' }}
217217
>

public/models/interfaces.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,9 @@ export type Detector = {
197197
flattenCustomResultIndex?: boolean;
198198
filterQuery: { [key: string]: any };
199199
featureAttributes: FeatureAttributes[];
200-
windowDelay: { period: Schedule };
200+
windowDelay?: { period: Schedule };
201+
frequency?: { period: Schedule };
202+
history?: number;
201203
detectionInterval: { period: Schedule };
202204
shingleSize: number;
203205
uiMetadata: UiMetaData;

public/pages/AnomalyCharts/components/AlertsFlyout/AlertsFlyout.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import React from 'react';
2525
import { EuiIcon } from '@elastic/eui';
2626
import { Monitor } from '../../../../models/interfaces';
2727
import { AlertsButton } from '../AlertsButton/AlertsButton';
28-
import { BASE_DOCS_LINK } from '../../../..//utils/constants';
28+
import { ALERTING_DOCS_LINK } from '../../../..//utils/constants';
2929

3030
type AlertsFlyoutProps = {
3131
detectorId: string;
@@ -84,7 +84,7 @@ export const AlertsFlyout = (props: AlertsFlyoutProps) => {
8484
<EuiText>
8585
<p className="alerts_flyout_p">
8686
Anomaly detector alerts are powered by the
87-
<EuiLink href={`${BASE_DOCS_LINK}/alerting`}>
87+
<EuiLink href={`${ALERTING_DOCS_LINK}`}>
8888
{' '}
8989
Alerting plugin
9090
</EuiLink>
@@ -103,7 +103,7 @@ export const AlertsFlyout = (props: AlertsFlyoutProps) => {
103103
<EuiFlexGroup alignItems="center" justifyContent="flexEnd">
104104
<EuiFlexItem grow={true}>
105105
<EuiSmallButton
106-
href={`${BASE_DOCS_LINK}/alerting`}
106+
href={`${ALERTING_DOCS_LINK}`}
107107
target="_blank"
108108
data-test-subj="setUpAlerts"
109109
>

public/pages/ConfigureForecastModel/components/CustomResultIndex/CustomResultIndex.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { Field, FieldProps, FormikProps, useFormikContext } from 'formik';
2727
import React, { useEffect, useState } from 'react';
2828
import ContentPanel from '../../../../components/ContentPanel/ContentPanel';
2929
import { CUSTOM_FORECASTER_RESULT_INDEX_PREFIX } from '../../../../../server/utils/constants';
30-
import { BASE_DOCS_LINK, FORECASTER_DOCS_LINK } from '../../../../utils/constants';
30+
import { FORECASTER_DOCS_LINK } from '../../../../utils/constants';
3131
import {
3232
isInvalid,
3333
getError,

public/pages/ConfigureModel/components/AdvancedSettings/AdvancedSettings.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
import { Field, FieldProps, FieldArray } from 'formik';
2727
import React, { useEffect, useState } from 'react';
2828
import ContentPanel from '../../../../components/ContentPanel/ContentPanel';
29-
import { BASE_DOCS_LINK } from '../../../../utils/constants';
29+
import { AD_DOCS_LINK } from '../../../../utils/constants';
3030
import {
3131
isInvalid,
3232
getError,
@@ -87,7 +87,7 @@ export function AdvancedSettings(props: AdvancedSettingsProps) {
8787
increase recall but also false positives. Larger values
8888
might be useful for ignoring noise in a signal.`,
8989
]}
90-
hintLink={`${BASE_DOCS_LINK}/ad`}
90+
hintLink={`${AD_DOCS_LINK}`}
9191
isInvalid={isInvalid(field.name, form)}
9292
error={getError(field.name, form)}
9393
>
@@ -133,7 +133,7 @@ export function AdvancedSettings(props: AdvancedSettingsProps) {
133133
<FormattedFormRow
134134
title="Sparse data handling"
135135
hint={[`Choose how to handle missing data points.`]}
136-
hintLink={`${BASE_DOCS_LINK}/ad`}
136+
hintLink={`${AD_DOCS_LINK}`}
137137
isInvalid={isInvalid(field.name, form)}
138138
error={getError(field.name, form)}
139139
>

public/pages/ConfigureModel/components/CategoryField/CategoryField.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
} from '@elastic/eui';
2525
import { Field, FieldProps, FormikProps } from 'formik';
2626
import { get, isEmpty } from 'lodash';
27-
import { BASE_DOCS_LINK } from '../../../../utils/constants';
27+
import { AD_DOCS_LINK } from '../../../../utils/constants';
2828
import React, { useState, useEffect } from 'react';
2929
import ContentPanel from '../../../../components/ContentPanel/ContentPanel';
3030
import {
@@ -75,7 +75,7 @@ export function CategoryField(props: CategoryFieldProps) {
7575
>
7676
Split a single time series into multiple time series based on
7777
categorical fields. You can select up to 2.{' '}
78-
<EuiLink href={`${BASE_DOCS_LINK}/ad`} target="_blank">
78+
<EuiLink href={`${AD_DOCS_LINK}`} target="_blank">
7979
Learn more
8080
</EuiLink>
8181
</EuiText>

public/pages/ConfigureModel/components/CategoryField/__tests__/__snapshots__/CategoryField.test.tsx.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ exports[`<CategoryField /> spec renders the component when disabled 1`] = `
4646
4747
<a
4848
class="euiLink euiLink--primary"
49-
href="https://opensearch.org/docs/monitoring-plugins/ad"
49+
href="https://opensearch.org/docs/latest/observing-your-data/ad/index/"
5050
rel="noopener noreferrer"
5151
target="_blank"
5252
>
@@ -174,7 +174,7 @@ exports[`<CategoryField /> spec renders the component when enabled 1`] = `
174174
175175
<a
176176
class="euiLink euiLink--primary"
177-
href="https://opensearch.org/docs/monitoring-plugins/ad"
177+
href="https://opensearch.org/docs/latest/observing-your-data/ad/index/"
178178
rel="noopener noreferrer"
179179
target="_blank"
180180
>

public/pages/ConfigureModel/components/Features/Features.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import React, { Fragment, useEffect } from 'react';
2626
import ContentPanel from '../../../../components/ContentPanel/ContentPanel';
2727
import { Detector } from '../../../../models/interfaces';
2828
import { initialFeatureValue } from '../../utils/helpers';
29-
import { MAX_FEATURE_NUM, BASE_DOCS_LINK } from '../../../../utils/constants';
29+
import { MAX_FEATURE_NUM, AD_DOCS_LINK } from '../../../../utils/constants';
3030
import { FeatureAccordion } from '../FeatureAccordion';
3131

3232
interface FeaturesProps {
@@ -54,7 +54,7 @@ export function Features(props: FeaturesProps) {
5454
>
5555
A feature is the field in your index that you use to check for
5656
anomalies. You can add up to 5 features.{' '}
57-
<EuiLink href={`${BASE_DOCS_LINK}/ad`} target="_blank">
57+
<EuiLink href={`${AD_DOCS_LINK}`} target="_blank">
5858
Learn more
5959
</EuiLink>
6060
</EuiText>
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*
8+
* Modifications Copyright OpenSearch Contributors. See
9+
* GitHub history for details.
10+
*/
11+
12+
import {
13+
EuiCompressedFieldNumber,
14+
EuiFlexGroup,
15+
EuiFlexItem,
16+
EuiText,
17+
} from '@elastic/eui';
18+
import { Field, FieldProps, useFormikContext } from 'formik';
19+
import React, { Fragment } from 'react';
20+
import {
21+
isInvalid,
22+
getError,
23+
validatePositiveInteger,
24+
validateMultipleOf,
25+
validateEmptyOrPositiveInteger,
26+
validateEmptyOrNonNegativeInteger
27+
} from '../../../../utils/utils';
28+
import { AD_DOCS_LINK } from '../../../../utils/constants';
29+
import { FormattedFormRow } from '../../../../components/FormattedFormRow/FormattedFormRow';
30+
31+
export const Settings = () => {
32+
const formik = useFormikContext();
33+
const numberFieldWidth = 140;
34+
35+
// Custom validation function that has access to form values
36+
const validateFrequency = (value: any) => {
37+
return validateMultipleOf(value, formik.values.interval);
38+
};
39+
40+
return (
41+
<Fragment>
42+
<Field name="interval" validate={validatePositiveInteger}>
43+
{({ field, form }: FieldProps) => (
44+
<EuiFlexGroup>
45+
<EuiFlexItem style={{ maxWidth: '70%' }}>
46+
<FormattedFormRow
47+
fullWidth
48+
title="Interval"
49+
hint={[
50+
`Interval sets the time window for summarizing and modeling data (e.g., 5 min to 1 hr), where too small creates noise, higher cost, and overreaction to fluctuations, while too large smooths out anomalies and delays detection.`,
51+
]}
52+
hintLink={`${AD_DOCS_LINK}`}
53+
isInvalid={isInvalid(field.name, form)}
54+
error={getError(field.name, form)}
55+
>
56+
<EuiFlexGroup gutterSize="s" alignItems="center">
57+
<EuiFlexItem grow={false}>
58+
<EuiCompressedFieldNumber
59+
name="detectionInterval"
60+
id="detectionInterval"
61+
placeholder="Interval"
62+
data-test-subj="detectionInterval"
63+
min={1}
64+
style={{ width: numberFieldWidth }}
65+
{...field}
66+
/>
67+
</EuiFlexItem>
68+
<EuiFlexItem>
69+
<EuiText>
70+
<p className="minutes">minutes</p>
71+
</EuiText>
72+
</EuiFlexItem>
73+
</EuiFlexGroup>
74+
</FormattedFormRow>
75+
</EuiFlexItem>
76+
</EuiFlexGroup>
77+
)}
78+
</Field>
79+
80+
<Field
81+
name="frequency"
82+
validate={validateFrequency}
83+
>
84+
{({ field, form }: FieldProps) => (
85+
<EuiFlexGroup style={{ marginTop: '16px' }}>
86+
<EuiFlexItem style={{ maxWidth: '70%' }}>
87+
<FormattedFormRow
88+
fullWidth
89+
title="Frequency"
90+
hint={[
91+
`Frequency defines how often the detector queries data and generates results, which also determines how often alerts may be produced; shorter values provide more real-time updates at higher cost, longer ones reduce load but slow updates. Frequency must be a multiple of interval and defaults to the same value.`,
92+
]}
93+
hintLink={`${AD_DOCS_LINK}`}
94+
isInvalid={isInvalid(field.name, form)}
95+
error={getError(field.name, form)}
96+
>
97+
<EuiFlexGroup gutterSize="s" alignItems="center">
98+
<EuiFlexItem grow={false}>
99+
<EuiCompressedFieldNumber
100+
name="frequency"
101+
id="frequency"
102+
placeholder="Frequency"
103+
data-test-subj="frequency"
104+
min={1}
105+
style={{ width: numberFieldWidth }}
106+
{...field}
107+
/>
108+
</EuiFlexItem>
109+
<EuiFlexItem>
110+
<EuiText>
111+
<p className="minutes">minutes</p>
112+
</EuiText>
113+
</EuiFlexItem>
114+
</EuiFlexGroup>
115+
</FormattedFormRow>
116+
</EuiFlexItem>
117+
</EuiFlexGroup>
118+
)}
119+
</Field>
120+
121+
<Field name="windowDelay" validate={validateEmptyOrNonNegativeInteger}>
122+
{({ field, form }: FieldProps) => (
123+
<FormattedFormRow
124+
fullWidth
125+
title="Window delay"
126+
hint="Specify a window of delay for a detector to fetch data, if you need to account for extra processing time."
127+
hintLink={`${AD_DOCS_LINK}`}
128+
isInvalid={isInvalid(field.name, form)}
129+
error={getError(field.name, form)}
130+
style={{ marginTop: '16px' }}
131+
>
132+
<EuiFlexGroup gutterSize="s" alignItems="center">
133+
<EuiFlexItem grow={false}>
134+
<EuiCompressedFieldNumber
135+
name="windowDelay"
136+
id="windowDelay"
137+
placeholder="Window delay"
138+
data-test-subj="windowDelay"
139+
style={{ width: numberFieldWidth }}
140+
{...field}
141+
/>
142+
</EuiFlexItem>
143+
<EuiFlexItem>
144+
<EuiText>
145+
<p className="minutes">minutes</p>
146+
</EuiText>
147+
</EuiFlexItem>
148+
</EuiFlexGroup>
149+
</FormattedFormRow>
150+
)}
151+
</Field>
152+
153+
<Field name="history" validate={validateEmptyOrPositiveInteger}>
154+
{({ field, form }: FieldProps) => (
155+
<FormattedFormRow
156+
fullWidth
157+
title="History"
158+
hint="How far back the model looks for training data. This determines the amount of historical data used to train the anomaly detection model. Minimum history is 40 intervals, maximum is 10,000 intervals, and the default is 40."
159+
hintLink={`${AD_DOCS_LINK}`}
160+
isInvalid={isInvalid(field.name, form)}
161+
error={getError(field.name, form)}
162+
style={{ marginTop: '16px' }}
163+
>
164+
<EuiFlexGroup gutterSize="s" alignItems="center">
165+
<EuiFlexItem grow={false}>
166+
<EuiCompressedFieldNumber
167+
name="history"
168+
id="history"
169+
placeholder="History"
170+
data-test-subj="history"
171+
min={40}
172+
max={10000}
173+
style={{ width: numberFieldWidth }}
174+
{...field}
175+
/>
176+
</EuiFlexItem>
177+
<EuiFlexItem>
178+
<EuiText>
179+
<p className="minutes">intervals</p>
180+
</EuiText>
181+
</EuiFlexItem>
182+
</EuiFlexGroup>
183+
</FormattedFormRow>
184+
)}
185+
</Field>
186+
</Fragment>
187+
);
188+
};

public/pages/DefineDetector/components/Settings/__tests__/Settings.test.tsx renamed to public/pages/ConfigureModel/components/Settings/__tests__/Settings.test.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ describe('<Settings /> spec', () => {
4040
</Formik>
4141
);
4242
expect(queryByText('Required')).toBeNull();
43-
fireEvent.focus(getByPlaceholderText('Detector interval'));
44-
fireEvent.blur(getByPlaceholderText('Detector interval'));
43+
fireEvent.focus(getByPlaceholderText('Interval'));
44+
fireEvent.blur(getByPlaceholderText('Interval'));
4545
expect(findByText('Required')).not.toBeNull();
4646
});
4747
test('shows error for invalid interval when toggling focus/blur', async () => {
@@ -55,8 +55,8 @@ describe('<Settings /> spec', () => {
5555
</Formik>
5656
);
5757
expect(queryByText('Required')).toBeNull();
58-
userEvent.type(getByPlaceholderText('Detector interval'), '-1');
59-
fireEvent.blur(getByPlaceholderText('Detector interval'));
58+
userEvent.type(getByPlaceholderText('Interval'), '-1');
59+
fireEvent.blur(getByPlaceholderText('Interval'));
6060
expect(findByText('Must be a positive integer')).not.toBeNull();
6161
});
6262
test('shows error for interval of 0 when toggling focus/blur', async () => {
@@ -70,8 +70,8 @@ describe('<Settings /> spec', () => {
7070
</Formik>
7171
);
7272
expect(queryByText('Required')).toBeNull();
73-
userEvent.type(getByPlaceholderText('Detector interval'), '0');
74-
fireEvent.blur(getByPlaceholderText('Detector interval'));
73+
userEvent.type(getByPlaceholderText('Interval'), '0');
74+
fireEvent.blur(getByPlaceholderText('Interval'));
7575
expect(findByText('Must be a positive integer')).not.toBeNull();
7676
});
7777
test('shows error for empty window delay when toggling focus/blur', async () => {

0 commit comments

Comments
 (0)