Skip to content

Commit 7b60fed

Browse files
authored
Do not show suggestAD action if the data source has no AI agent (#901)
Signed-off-by: gaobinlong <gbinlong@amazon.com>
1 parent 482849d commit 7b60fed

File tree

3 files changed

+163
-65
lines changed

3 files changed

+163
-65
lines changed

public/components/DiscoverAction/SuggestAnomalyDetector.test.tsx

+90-46
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import SuggestAnomalyDetector from './SuggestAnomalyDetector';
2020
import userEvent from '@testing-library/user-event';
2121
import { HttpFetchOptionsWithPath } from '../../../../../src/core/public';
2222
import { getAssistantClient, getQueryService, getUsageCollection } from '../../services';
23+
import { getMappings } from '../../redux/reducers/opensearch';
2324

2425
const notifications = {
2526
toasts: {
@@ -131,6 +132,23 @@ describe('GenerateAnomalyDetector spec', () => {
131132
(getAssistantClient().agentConfigExists as jest.Mock).mockResolvedValueOnce({
132133
exists: true
133134
});
135+
136+
httpClientMock.get = jest.fn().mockResolvedValue({
137+
ok: true,
138+
response: {
139+
mappings: {
140+
test: {
141+
mappings: {
142+
properties: {
143+
field: {
144+
type: 'date',
145+
}
146+
}
147+
}
148+
}
149+
}
150+
},
151+
});
134152
});
135153

136154
it('renders with empty generated parameters', async () => {
@@ -176,7 +194,7 @@ describe('GenerateAnomalyDetector spec', () => {
176194
await waitFor(() => {
177195
expect(getNotifications().toasts.addDanger).toHaveBeenCalledTimes(1);
178196
expect(getNotifications().toasts.addDanger).toHaveBeenCalledWith(
179-
'Generate parameters for creating anomaly detector failed, reason: Error: Cannot find aggregation field, aggregation method or data fields!'
197+
'Generate parameters for creating anomaly detector failed, reason: Error: Cannot find aggregation field, aggregation method or date fields!'
180198
);
181199
});
182200
});
@@ -255,41 +273,11 @@ describe('GenerateAnomalyDetector spec', () => {
255273
});
256274
});
257275

258-
describe('Test agent not configured', () => {
259-
beforeEach(() => {
260-
jest.clearAllMocks();
261-
const queryService = getQueryService();
262-
queryService.queryString.getQuery.mockReturnValue({
263-
dataset: {
264-
id: 'test-pattern',
265-
title: 'test-pattern',
266-
type: 'INDEX_PATTERN',
267-
timeFieldName: '@timestamp',
268-
},
269-
});
270-
});
271-
272-
it('renders with empty generated parameters', async () => {
273-
(getAssistantClient().agentConfigExists as jest.Mock).mockResolvedValueOnce({
274-
exists: false
275-
});
276-
277-
const { queryByText } = renderWithRouter();
278-
expect(queryByText('Suggested anomaly detector')).not.toBeNull();
279-
280-
await waitFor(() => {
281-
expect(getNotifications().toasts.addDanger).toHaveBeenCalledTimes(1);
282-
expect(getNotifications().toasts.addDanger).toHaveBeenCalledWith(
283-
'Generate parameters for creating anomaly detector failed, reason: Error: Agent for suggest anomaly detector not found, please configure an agent firstly!'
284-
);
285-
});
286-
});
287-
});
288-
289276
describe('Test feedback', () => {
290277
let reportUiStatsMock: any;
291278

292279
beforeEach(() => {
280+
jest.clearAllMocks();
293281
const queryService = getQueryService();
294282
queryService.queryString.getQuery.mockReturnValue({
295283
dataset: {
@@ -404,6 +392,40 @@ describe('GenerateAnomalyDetector spec', () => {
404392
(getAssistantClient().agentConfigExists as jest.Mock).mockResolvedValueOnce({
405393
exists: true
406394
});
395+
396+
httpClientMock.get = jest.fn((pathOrOptions: string | HttpFetchOptionsWithPath) => {
397+
const url = typeof pathOrOptions === 'string' ? pathOrOptions : pathOrOptions.path;
398+
switch (url) {
399+
case '/api/anomaly_detectors/_mappings':
400+
return Promise.resolve({
401+
ok: true,
402+
response: {
403+
mappings: {
404+
test: {
405+
mappings: {
406+
properties: {
407+
field: {
408+
type: 'date',
409+
}
410+
}
411+
}
412+
}
413+
}
414+
},
415+
});
416+
case '/api/anomaly_detectors/detectors/_count':
417+
return Promise.resolve({
418+
ok: true,
419+
response: {
420+
count: 0
421+
},
422+
});
423+
default:
424+
return Promise.resolve({
425+
ok: true
426+
});
427+
}
428+
});
407429
});
408430

409431
it('All API calls execute successfully', async () => {
@@ -494,13 +516,6 @@ describe('GenerateAnomalyDetector spec', () => {
494516
}
495517
});
496518

497-
httpClientMock.get = jest.fn().mockResolvedValue({
498-
ok: true,
499-
response: {
500-
count: 0
501-
},
502-
});
503-
504519
const { queryByText, getByTestId } = renderWithRouter();
505520
expect(queryByText('Suggested anomaly detector')).not.toBeNull();
506521

@@ -546,13 +561,6 @@ describe('GenerateAnomalyDetector spec', () => {
546561
}
547562
});
548563

549-
httpClientMock.get = jest.fn().mockResolvedValue({
550-
ok: true,
551-
response: {
552-
count: 0
553-
},
554-
});
555-
556564
(getAssistantClient().executeAgentByConfigName as jest.Mock).mockResolvedValueOnce({
557565
body: {
558566
inference_results: [
@@ -587,4 +595,40 @@ describe('GenerateAnomalyDetector spec', () => {
587595
});
588596
});
589597
});
598+
599+
describe('Test getting index mapping failed', () => {
600+
beforeEach(() => {
601+
jest.clearAllMocks();
602+
const queryService = getQueryService();
603+
queryService.queryString.getQuery.mockReturnValue({
604+
dataset: {
605+
id: 'test-pattern',
606+
title: 'test-pattern',
607+
type: 'INDEX_PATTERN',
608+
timeFieldName: '@timestamp',
609+
},
610+
});
611+
});
612+
613+
afterEach(() => {
614+
jest.clearAllMocks();
615+
});
616+
617+
it('renders with getting index mapping failed', async () => {
618+
httpClientMock.get = jest.fn().mockResolvedValue({
619+
ok: false,
620+
error: 'failed to get index mapping'
621+
});
622+
623+
const { queryByText } = renderWithRouter();
624+
expect(queryByText('Suggested anomaly detector')).not.toBeNull();
625+
626+
await waitFor(() => {
627+
expect(getNotifications().toasts.addDanger).toHaveBeenCalledTimes(1);
628+
expect(getNotifications().toasts.addDanger).toHaveBeenCalledWith(
629+
'failed to get index mapping'
630+
);
631+
});
632+
});
633+
});
590634
});

public/components/DiscoverAction/SuggestAnomalyDetector.tsx

+48-12
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ import { formikToDetectorName } from '../FeatureAnywhereContextMenu/CreateAnomal
7777
import { DEFAULT_DATA } from '../../../../../src/plugins/data/common';
7878
import { AppState } from '../../redux/reducers';
7979
import { v4 as uuidv4 } from 'uuid';
80+
import { getPathsPerDataType } from '../../redux/reducers/mapper';
8081

8182
export interface GeneratedParameters {
8283
categoryField: string;
@@ -107,8 +108,7 @@ function SuggestAnomalyDetector({
107108
const indexPatternId = dataset.id;
108109
// indexName could be a index pattern or a concrete index
109110
const indexName = dataset.title;
110-
const timeFieldName = dataset.timeFieldName;
111-
if (!indexPatternId || !indexName || !timeFieldName) {
111+
if (!indexPatternId || !indexName) {
112112
notifications.toasts.addDanger(
113113
'Cannot extract complete index info from the context'
114114
);
@@ -135,11 +135,12 @@ function SuggestAnomalyDetector({
135135
);
136136
const categoricalFields = getCategoryFields(indexDataTypes);
137137

138-
const dateFields = get(indexDataTypes, 'date', []) as string[];
139-
const dateNanoFields = get(indexDataTypes, 'date_nanos', []) as string[];
140-
const allDateFields = dateFields.concat(dateNanoFields);
141-
138+
// const dateFields = get(indexDataTypes, 'date', []) as string[];
139+
// const dateNanoFields = get(indexDataTypes, 'date_nanos', []) as string[];
140+
// const allDateFields = dateFields.concat(dateNanoFields);
141+
const [allDateFields, setAllDateFields] = useState<string[]>([]);
142142
const [feedbackResult, setFeedbackResult] = useState<boolean | undefined>(undefined);
143+
const [timeFieldName, setTimeFieldName] = useState(dataset.timeFieldName || '');
143144

144145
// let LLM to generate parameters for creating anomaly detector
145146
async function getParameters() {
@@ -166,6 +167,16 @@ function SuggestAnomalyDetector({
166167
initialDetectorValue.categoryFieldEnabled = !!generatedParameters.categoryField;
167168
initialDetectorValue.categoryField = initialDetectorValue.categoryFieldEnabled ? [generatedParameters.categoryField] : [];
168169

170+
// if the dataset has no time field, then we find a root level field from the mapping, or we use the first one as the default time field
171+
if (!timeFieldName) {
172+
if (generatedParameters.dateFields.length == 0) {
173+
throw new Error('Cannot find any date type fields!');
174+
}
175+
const defaultTimeField = generatedParameters.dateFields.find(dateField => !dateField.includes('.')) || generatedParameters.dateFields[0];
176+
setTimeFieldName(defaultTimeField);
177+
initialDetectorValue.timeField = defaultTimeField;
178+
}
179+
169180
setIsLoading(false);
170181
setButtonName('Create detector');
171182
setCategoryFieldEnabled(!!generatedParameters.categoryField);
@@ -183,7 +194,7 @@ function SuggestAnomalyDetector({
183194
const rawAggregationMethods = rawGeneratedParameters['aggregationMethod'];
184195
const rawDataFields = rawGeneratedParameters['dateFields'];
185196
if (!rawAggregationFields || !rawAggregationMethods || !rawDataFields) {
186-
throw new Error('Cannot find aggregation field, aggregation method or data fields!');
197+
throw new Error('Cannot find aggregation field, aggregation method or date fields!');
187198
}
188199
const aggregationFields =
189200
rawAggregationFields.split(',');
@@ -196,19 +207,21 @@ function SuggestAnomalyDetector({
196207
}
197208

198209
const featureList = aggregationFields.map((field: string, index: number) => {
199-
const method = aggregationMethods[index];
210+
let method = aggregationMethods[index];
200211
if (!field || !method) {
201212
throw new Error('The generated aggregation field or aggregation method is empty!');
202213
}
214+
// for the count aggregation method, display name and actual name are different, need to convert the display name to actual name
215+
method = method.replace('count', 'value_count');
203216
const aggregationOption = {
204217
label: field,
205218
};
206219
const feature: FeaturesFormikValues = {
207-
featureName: `feature_${field}`,
220+
featureName: `feature_${field}`.substring(0, 64),
208221
featureType: FEATURE_TYPE.SIMPLE,
209222
featureEnabled: true,
210223
aggregationQuery: '',
211-
aggregationBy: aggregationMethods[index],
224+
aggregationBy: method,
212225
aggregationOf: [aggregationOption],
213226
};
214227
return feature;
@@ -223,8 +236,31 @@ function SuggestAnomalyDetector({
223236

224237
useEffect(() => {
225238
async function fetchData() {
226-
await dispatch(getMappings(indexName, dataSourceId));
227-
await getParameters();
239+
await dispatch(getMappings(indexName, dataSourceId))
240+
.then(async (result: any) => {
241+
const indexDataTypes = getPathsPerDataType(result.response.mappings);
242+
const dateFields = get(indexDataTypes, 'date', []) as string[];
243+
const dateNanoFields = get(indexDataTypes, 'date_nanos', []) as string[];
244+
const allDateFields = dateFields.concat(dateNanoFields);
245+
setAllDateFields(allDateFields);
246+
if (allDateFields.length == 0) {
247+
notifications.toasts.addDanger(
248+
'Cannot find any date type fields!'
249+
);
250+
} else {
251+
await getParameters();
252+
}
253+
})
254+
.catch((err: any) => {
255+
notifications.toasts.addDanger(
256+
prettifyErrorMessage(
257+
getErrorMessage(
258+
err,
259+
'There was a problem getting the index mapping'
260+
)
261+
)
262+
);
263+
});
228264
}
229265
fetchData();
230266
}, []);

public/utils/contextMenu/getActions.tsx

+25-7
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,24 @@ import React from 'react';
77
import { i18n } from '@osd/i18n';
88
import { EuiIconType } from '@elastic/eui';
99
import { toMountPoint } from '../../../../../src/plugins/opensearch_dashboards_react/public';
10-
import { Action, createAction } from '../../../../../src/plugins/ui_actions/public';
10+
import {
11+
Action,
12+
createAction,
13+
} from '../../../../../src/plugins/ui_actions/public';
1114
import { createADAction } from '../../action/ad_dashboard_action';
1215
import AnywhereParentFlyout from '../../components/FeatureAnywhereContextMenu/AnywhereParentFlyout';
1316
import { Provider } from 'react-redux';
1417
import configureStore from '../../redux/configureStore';
1518
import DocumentationTitle from '../../components/FeatureAnywhereContextMenu/DocumentationTitle/containers/DocumentationTitle';
1619
import { AD_FEATURE_ANYWHERE_LINK, ANOMALY_DETECTION_ICON } from '../constants';
17-
import { getClient, getOverlays } from '../../../public/services';
20+
import {
21+
getAssistantClient,
22+
getClient,
23+
getOverlays,
24+
} from '../../../public/services';
1825
import { FLYOUT_MODES } from '../../../public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/constants';
1926
import SuggestAnomalyDetector from '../../../public/components/DiscoverAction/SuggestAnomalyDetector';
27+
import { SUGGEST_ANOMALY_DETECTOR_CONFIG_ID } from '../../../server/utils/constants';
2028

2129
export const ACTION_SUGGEST_AD = 'suggestAnomalyDetector';
2230

@@ -99,22 +107,32 @@ export const getSuggestAnomalyDetectorAction = () => {
99107
const overlay = openFlyout(
100108
toMountPoint(
101109
<Provider store={store}>
102-
<SuggestAnomalyDetector
103-
closeFlyout={() => overlay.close()}
104-
/>
110+
<SuggestAnomalyDetector closeFlyout={() => overlay.close()} />
105111
</Provider>
106112
)
107113
);
108-
}
114+
};
109115

110116
return createAction({
111117
id: 'suggestAnomalyDetector',
112118
order: 100,
113119
type: ACTION_SUGGEST_AD,
114120
getDisplayName: () => 'Suggest anomaly detector',
115121
getIconType: () => ANOMALY_DETECTION_ICON,
122+
// suggestAD is only compatible with data sources that have certain agents configured
123+
isCompatible: async (context) => {
124+
if (context.datasetId) {
125+
const assistantClient = getAssistantClient();
126+
const res = await assistantClient.agentConfigExists(
127+
SUGGEST_ANOMALY_DETECTOR_CONFIG_ID,
128+
{ dataSourceId: context.dataSourceId }
129+
);
130+
return res.exists;
131+
}
132+
return false;
133+
},
116134
execute: async () => {
117135
onClick();
118136
},
119137
});
120-
}
138+
};

0 commit comments

Comments
 (0)