Skip to content

Commit 1d08141

Browse files
authored
[ML] Entity filter for the Notifications page (#142778)
* do not show callout on loading * unit tests * wip ml entity selector * all options, preserve state wip * support simple selection and wildcards * handle duplicates * update selectedOptions prop * fix hook deps, add error handling * fix empty counts * unit test * move component * rename translation key * add extra assertion * more tests * fix export
1 parent a2f2bc5 commit 1d08141

File tree

17 files changed

+654
-9
lines changed

17 files changed

+654
-9
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
export { MlEntitySelector, type MlEntitySelectorProps } from './ml_entity_selector';
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React from 'react';
9+
import { render, waitFor, screen } from '@testing-library/react';
10+
import { MlEntitySelector } from './ml_entity_selector';
11+
import { useMlApiContext } from '../../contexts/kibana';
12+
import { MlApiServices } from '../../services/ml_api_service';
13+
import { useToastNotificationService } from '../../services/toast_notification_service';
14+
15+
jest.mock('../../contexts/kibana');
16+
jest.mock('../../services/toast_notification_service');
17+
18+
describe('MlEntitySelector', () => {
19+
const getAllJobAndGroupIds = jest.fn(() => {
20+
return Promise.resolve({ jobIds: ['ad_01', 'ad_02'] });
21+
});
22+
23+
const getDataFrameAnalytics = jest.fn(() => {
24+
return Promise.resolve({
25+
count: 2,
26+
data_frame_analytics: [{ id: 'dfa_01' }, { id: 'dfa_02' }],
27+
});
28+
});
29+
30+
const getTrainedModels = jest.fn(() => {
31+
return Promise.resolve([{ model_id: 'model_01' }]);
32+
});
33+
34+
(useMlApiContext as jest.MockedFunction<typeof useMlApiContext>).mockImplementation(() => {
35+
return {
36+
jobs: {
37+
getAllJobAndGroupIds,
38+
},
39+
dataFrameAnalytics: {
40+
getDataFrameAnalytics,
41+
},
42+
trainedModels: {
43+
getTrainedModels,
44+
},
45+
} as unknown as jest.Mocked<MlApiServices>;
46+
});
47+
48+
beforeEach(() => {});
49+
50+
afterEach(() => {
51+
jest.clearAllMocks();
52+
});
53+
54+
test('fetches all available options on mount by default', async () => {
55+
const { getByTestId } = render(<MlEntitySelector />);
56+
57+
await waitFor(() => {
58+
expect(getByTestId('mlEntitySelector_loaded')).toBeInTheDocument();
59+
});
60+
61+
expect(getTrainedModels).toHaveBeenCalledTimes(1);
62+
expect(getAllJobAndGroupIds).toHaveBeenCalledTimes(1);
63+
expect(getDataFrameAnalytics).toHaveBeenCalledTimes(1);
64+
});
65+
66+
test('fetches requested entity types on mount', async () => {
67+
const { getByTestId } = render(<MlEntitySelector entityTypes={{ anomaly_detector: true }} />);
68+
69+
await waitFor(() => {
70+
expect(getByTestId('mlEntitySelector_loaded')).toBeInTheDocument();
71+
});
72+
73+
expect(getTrainedModels).not.toHaveBeenCalled();
74+
expect(getDataFrameAnalytics).not.toHaveBeenCalled();
75+
expect(getAllJobAndGroupIds).toHaveBeenCalledTimes(1);
76+
});
77+
78+
test('marks selected entities', async () => {
79+
const { getByTestId } = render(
80+
<MlEntitySelector selectedOptions={[{ id: 'ad_01', type: 'anomaly_detector' }]} />
81+
);
82+
83+
await waitFor(() => {
84+
expect(getByTestId('mlEntitySelector_loaded')).toBeInTheDocument();
85+
});
86+
87+
// Assert available options to select
88+
const optionsContainer = await screen.findByTestId(
89+
'comboBoxOptionsList mlEntitySelector_loaded-optionsList'
90+
);
91+
const optionElements = optionsContainer.querySelectorAll('[data-test-subj="mlAdJobOption"]');
92+
expect(optionElements).toHaveLength(1);
93+
expect(optionElements[0]).toHaveAttribute('id', 'anomaly_detector:ad_02');
94+
expect(optionsContainer.querySelectorAll('[data-test-subj="mlDfaJobOption"]')).toHaveLength(2);
95+
expect(
96+
optionsContainer.querySelectorAll('[data-test-subj="mlTrainedModelOption"]')
97+
).toHaveLength(1);
98+
99+
// Assert selected
100+
const comboBoxInput = getByTestId('comboBoxInput');
101+
expect(comboBoxInput.querySelectorAll('[data-test-subj="mlAdJobOption"]')).toHaveLength(1);
102+
});
103+
104+
test('provide current selection update on change', async () => {
105+
const onChangeSpy = jest.fn();
106+
107+
const { getByTestId } = render(
108+
<MlEntitySelector
109+
selectedOptions={[{ id: 'ad_01', type: 'anomaly_detector' }]}
110+
onSelectionChange={onChangeSpy}
111+
/>
112+
);
113+
114+
await waitFor(() => {
115+
expect(getByTestId('mlEntitySelector_loaded')).toBeInTheDocument();
116+
});
117+
118+
// Assert available options to select
119+
const optionsContainer = await screen.findByTestId(
120+
'comboBoxOptionsList mlEntitySelector_loaded-optionsList'
121+
);
122+
123+
optionsContainer
124+
.querySelector<HTMLButtonElement>('[id="data_frame_analytics:dfa_01"]')!
125+
.click();
126+
127+
expect(onChangeSpy).toHaveBeenCalledWith([
128+
{ id: 'ad_01', type: 'anomaly_detector' },
129+
{ id: 'dfa_01', type: 'data_frame_analytics' },
130+
]);
131+
});
132+
133+
test('provide current selection update on change with duplicates handling', async () => {
134+
(useMlApiContext as jest.MockedFunction<typeof useMlApiContext>).mockImplementationOnce(() => {
135+
return {
136+
jobs: {
137+
getAllJobAndGroupIds,
138+
},
139+
dataFrameAnalytics: {
140+
getDataFrameAnalytics: jest.fn(() => {
141+
return Promise.resolve({
142+
count: 2,
143+
// same ID as the anomaly detection job
144+
data_frame_analytics: [{ id: 'ad_01' }, { id: 'dfa_02' }],
145+
});
146+
}),
147+
},
148+
trainedModels: {
149+
getTrainedModels,
150+
},
151+
} as unknown as jest.Mocked<MlApiServices>;
152+
});
153+
154+
const onChangeSpy = jest.fn();
155+
156+
const { getByTestId } = render(
157+
<MlEntitySelector
158+
selectedOptions={[{ id: 'ad_01' }, { id: 'keep_it' }]}
159+
onSelectionChange={onChangeSpy}
160+
handleDuplicates={true}
161+
/>
162+
);
163+
164+
await waitFor(() => {
165+
expect(getByTestId('mlEntitySelector_loaded')).toBeInTheDocument();
166+
});
167+
168+
// Assert selected
169+
const comboBoxInput = getByTestId('comboBoxInput');
170+
const selectedOptions = comboBoxInput.querySelectorAll('.euiComboBoxPill');
171+
expect(selectedOptions).toHaveLength(3);
172+
expect(selectedOptions[0]).toHaveAttribute('id', 'anomaly_detector:ad_01');
173+
expect(selectedOptions[1]).toHaveAttribute('id', 'data_frame_analytics:ad_01');
174+
175+
// Assert removal
176+
selectedOptions[0].getElementsByTagName('button')[0].click();
177+
expect(onChangeSpy).toHaveBeenCalledWith([{ id: 'keep_it', type: 'unknown' }]);
178+
});
179+
180+
test('display a toast on error', async () => {
181+
const displayErrorToast = jest.fn();
182+
const sampleError = new Error('try a bit later');
183+
184+
(useMlApiContext as jest.MockedFunction<typeof useMlApiContext>).mockImplementationOnce(() => {
185+
return {
186+
jobs: {
187+
getAllJobAndGroupIds: jest.fn(() => {
188+
throw sampleError;
189+
}),
190+
},
191+
} as unknown as jest.Mocked<MlApiServices>;
192+
});
193+
(
194+
useToastNotificationService as jest.MockedFunction<typeof useToastNotificationService>
195+
).mockImplementationOnce(() => {
196+
return { displayErrorToast } as unknown as ReturnType<typeof useToastNotificationService>;
197+
});
198+
199+
const { getByTestId } = render(
200+
<MlEntitySelector selectedOptions={[{ id: 'ad_01', type: 'anomaly_detector' }]} />
201+
);
202+
203+
await waitFor(() => {
204+
expect(getByTestId('mlEntitySelector_loaded')).toBeInTheDocument();
205+
});
206+
207+
expect(displayErrorToast).toHaveBeenCalledTimes(1);
208+
expect(displayErrorToast).toHaveBeenCalledWith(sampleError, 'Failed to fetch ML entities');
209+
});
210+
});

0 commit comments

Comments
 (0)