Skip to content

Commit e91d0d4

Browse files
[App Search] Add a Sample Engine CTA panel to the engines table when empty (#94647)
* Added new onboarding complete route for App Search * Allow responses without JSON bodies in Enterprise Search * New SampleEngineCreationCtaLogic * New SampleEngineCreationCta component * Add SampleEngineCreationCTA to engines EmptyState * Improve SampleEngineCreationCta * Fix spelling error in Enterprise Search request handler test * Improve SampleEngineCreationCtaLogic * Fix types * Fix tests after origin/master merge * Turns out I 'fixed' my tests by removing this test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
1 parent 5b69278 commit e91d0d4

File tree

13 files changed

+451
-32
lines changed

13 files changed

+451
-32
lines changed

x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.test.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,37 @@ import { shallow, ShallowWrapper } from 'enzyme';
1414

1515
import { EuiEmptyPrompt } from '@elastic/eui';
1616

17+
import { SampleEngineCreationCta } from '../../sample_engine_creation_cta';
18+
1719
import { EmptyState } from './';
1820

1921
describe('EmptyState', () => {
2022
describe('when the user can manage/create engines', () => {
2123
let wrapper: ShallowWrapper;
24+
let prompt: ShallowWrapper;
2225

2326
beforeAll(() => {
2427
setMockValues({ myRole: { canManageEngines: true } });
2528
wrapper = shallow(<EmptyState />);
29+
prompt = wrapper.find(EuiEmptyPrompt).dive();
30+
});
31+
32+
afterAll(() => {
33+
jest.clearAllMocks();
2634
});
2735

2836
it('renders a prompt to create an engine', () => {
2937
expect(wrapper.find('[data-test-subj="AdminEmptyEnginesPrompt"]')).toHaveLength(1);
3038
});
3139

40+
it('contains a sample engine CTA', () => {
41+
expect(prompt.find(SampleEngineCreationCta)).toHaveLength(1);
42+
});
43+
3244
describe('create engine button', () => {
33-
let prompt: ShallowWrapper;
3445
let button: ShallowWrapper;
3546

3647
beforeAll(() => {
37-
prompt = wrapper.find(EuiEmptyPrompt).dive();
3848
button = prompt.find('[data-test-subj="EmptyStateCreateFirstEngineCta"]');
3949
});
4050

@@ -50,13 +60,18 @@ describe('EmptyState', () => {
5060
});
5161

5262
describe('when the user cannot manage/create engines', () => {
63+
let wrapper: ShallowWrapper;
64+
5365
beforeAll(() => {
5466
setMockValues({ myRole: { canManageEngines: false } });
67+
wrapper = shallow(<EmptyState />);
5568
});
5669

57-
it('renders a prompt to contact the App Search admin', () => {
58-
const wrapper = shallow(<EmptyState />);
70+
afterAll(() => {
71+
jest.clearAllMocks();
72+
});
5973

74+
it('renders a prompt to contact the App Search admin', () => {
6075
expect(wrapper.find('[data-test-subj="NonAdminEmptyEnginesPrompt"]')).toHaveLength(1);
6176
});
6277
});

x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import React from 'react';
99

1010
import { useValues, useActions } from 'kea';
1111

12-
import { EuiPageContent, EuiEmptyPrompt } from '@elastic/eui';
12+
import { EuiPageContent, EuiEmptyPrompt, EuiSpacer } from '@elastic/eui';
1313
import { i18n } from '@kbn/i18n';
1414

1515
import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome';
@@ -18,6 +18,8 @@ import { TelemetryLogic } from '../../../../shared/telemetry';
1818
import { AppLogic } from '../../../app_logic';
1919
import { ENGINE_CREATION_PATH } from '../../../routes';
2020

21+
import { SampleEngineCreationCta } from '../../sample_engine_creation_cta/sample_engine_creation_cta';
22+
2123
import { EnginesOverviewHeader } from './header';
2224

2325
import './empty_state.scss';
@@ -55,22 +57,26 @@ export const EmptyState: React.FC = () => {
5557
</p>
5658
}
5759
actions={
58-
<EuiButtonTo
59-
data-test-subj="EmptyStateCreateFirstEngineCta"
60-
fill
61-
to={ENGINE_CREATION_PATH}
62-
onClick={() =>
63-
sendAppSearchTelemetry({
64-
action: 'clicked',
65-
metric: 'create_first_engine_button',
66-
})
67-
}
68-
>
69-
{i18n.translate(
70-
'xpack.enterpriseSearch.appSearch.emptyState.createFirstEngineCta',
71-
{ defaultMessage: 'Create an engine' }
72-
)}
73-
</EuiButtonTo>
60+
<>
61+
<EuiButtonTo
62+
data-test-subj="EmptyStateCreateFirstEngineCta"
63+
fill
64+
to={ENGINE_CREATION_PATH}
65+
onClick={() =>
66+
sendAppSearchTelemetry({
67+
action: 'clicked',
68+
metric: 'create_first_engine_button',
69+
})
70+
}
71+
>
72+
{i18n.translate(
73+
'xpack.enterpriseSearch.appSearch.emptyState.createFirstEngineCta',
74+
{ defaultMessage: 'Create an engine' }
75+
)}
76+
</EuiButtonTo>
77+
<EuiSpacer size="xl" />
78+
<SampleEngineCreationCta />
79+
</>
7480
}
7581
/>
7682
) : (
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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 { i18n } from '@kbn/i18n';
9+
10+
export const SAMPLE_ENGINE_CREATION_CTA_TITLE = i18n.translate(
11+
'xpack.enterpriseSearch.appSearch.sampleEngineCreationCta.title',
12+
{
13+
defaultMessage: 'Just kicking the tires?',
14+
}
15+
);
16+
17+
export const SAMPLE_ENGINE_CREATION_CTA_DESCRIPTION = i18n.translate(
18+
'xpack.enterpriseSearch.appSearch.sampleEngineCreationCta.description',
19+
{
20+
defaultMessage: 'Test an engine with sample data.',
21+
}
22+
);
23+
24+
export const SAMPLE_ENGINE_CREATION_CTA_BUTTON_LABEL = i18n.translate(
25+
'xpack.enterpriseSearch.appSearch.sampleEngineCreationCta.buttonLabel',
26+
{
27+
defaultMessage: 'Try a sample engine',
28+
}
29+
);
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 { SampleEngineCreationCta } from './sample_engine_creation_cta';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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 '../../../__mocks__/enterprise_search_url.mock';
9+
import { setMockActions, setMockValues } from '../../../__mocks__';
10+
11+
import React from 'react';
12+
13+
import { shallow } from 'enzyme';
14+
15+
import { EuiButton } from '@elastic/eui';
16+
17+
import { SampleEngineCreationCta } from './sample_engine_creation_cta';
18+
19+
describe('SampleEngineCTA', () => {
20+
describe('CTA button', () => {
21+
const MOCK_VALUES = {
22+
isLoading: false,
23+
};
24+
25+
const MOCK_ACTIONS = {
26+
createSampleEngine: jest.fn(),
27+
};
28+
29+
beforeEach(() => {
30+
jest.clearAllMocks();
31+
setMockActions(MOCK_ACTIONS);
32+
setMockValues(MOCK_VALUES);
33+
});
34+
35+
it('calls createSampleEngine on click', () => {
36+
const wrapper = shallow(<SampleEngineCreationCta />);
37+
const ctaButton = wrapper.find(EuiButton);
38+
39+
expect(ctaButton.props().onClick).toEqual(MOCK_ACTIONS.createSampleEngine);
40+
});
41+
42+
it('is enabled by default', () => {
43+
const wrapper = shallow(<SampleEngineCreationCta />);
44+
const ctaButton = wrapper.find(EuiButton);
45+
46+
expect(ctaButton.props().isLoading).toEqual(false);
47+
});
48+
49+
it('is disabled while loading', () => {
50+
setMockValues({ ...MOCK_VALUES, isLoading: true });
51+
const wrapper = shallow(<SampleEngineCreationCta />);
52+
const ctaButton = wrapper.find(EuiButton);
53+
54+
expect(ctaButton.props().isLoading).toEqual(true);
55+
});
56+
});
57+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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+
10+
import { useActions, useValues } from 'kea';
11+
12+
import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiTitle, EuiText, EuiButton } from '@elastic/eui';
13+
14+
import {
15+
SAMPLE_ENGINE_CREATION_CTA_TITLE,
16+
SAMPLE_ENGINE_CREATION_CTA_DESCRIPTION,
17+
SAMPLE_ENGINE_CREATION_CTA_BUTTON_LABEL,
18+
} from './i18n';
19+
import { SampleEngineCreationCtaLogic } from './sample_engine_creation_cta_logic';
20+
21+
export const SampleEngineCreationCta: React.FC = () => {
22+
const { isLoading } = useValues(SampleEngineCreationCtaLogic);
23+
const { createSampleEngine } = useActions(SampleEngineCreationCtaLogic);
24+
25+
return (
26+
<EuiPanel>
27+
<EuiFlexGroup alignItems="center">
28+
<EuiFlexItem>
29+
<EuiTitle size="s">
30+
<h3>{SAMPLE_ENGINE_CREATION_CTA_TITLE}</h3>
31+
</EuiTitle>
32+
<EuiText size="s">
33+
<p>{SAMPLE_ENGINE_CREATION_CTA_DESCRIPTION}</p>
34+
</EuiText>
35+
</EuiFlexItem>
36+
<EuiFlexItem grow={false}>
37+
<EuiButton onClick={createSampleEngine} isLoading={isLoading}>
38+
{SAMPLE_ENGINE_CREATION_CTA_BUTTON_LABEL}
39+
</EuiButton>
40+
</EuiFlexItem>
41+
</EuiFlexGroup>
42+
</EuiPanel>
43+
);
44+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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 {
9+
LogicMounter,
10+
mockHttpValues,
11+
mockKibanaValues,
12+
mockFlashMessageHelpers,
13+
} from '../../../__mocks__';
14+
15+
import { nextTick } from '@kbn/test/jest';
16+
17+
import { SampleEngineCreationCtaLogic } from './sample_engine_creation_cta_logic';
18+
19+
describe('SampleEngineCreationCtaLogic', () => {
20+
const { mount } = new LogicMounter(SampleEngineCreationCtaLogic);
21+
const { http } = mockHttpValues;
22+
const { navigateToUrl } = mockKibanaValues;
23+
const { setQueuedSuccessMessage, flashAPIErrors } = mockFlashMessageHelpers;
24+
25+
const DEFAULT_VALUES = {
26+
isLoading: false,
27+
};
28+
29+
beforeEach(() => {
30+
jest.clearAllMocks();
31+
mount();
32+
});
33+
34+
it('has expected default values', () => {
35+
expect(SampleEngineCreationCtaLogic.values).toEqual(DEFAULT_VALUES);
36+
});
37+
38+
describe('actions', () => {
39+
it('onSampleEngineCreationFailure sets isLoading to false', () => {
40+
mount({ isLoading: true });
41+
42+
SampleEngineCreationCtaLogic.actions.onSampleEngineCreationFailure();
43+
44+
expect(SampleEngineCreationCtaLogic.values.isLoading).toEqual(false);
45+
});
46+
});
47+
48+
describe('listeners', () => {
49+
describe('createSampleEngine', () => {
50+
it('POSTS to /api/app_search/engines', () => {
51+
const body = JSON.stringify({
52+
seed_sample_engine: true,
53+
});
54+
SampleEngineCreationCtaLogic.actions.createSampleEngine();
55+
56+
expect(http.post).toHaveBeenCalledWith('/api/app_search/onboarding_complete', { body });
57+
});
58+
59+
it('calls onSampleEngineCreationSuccess on valid submission', async () => {
60+
jest.spyOn(SampleEngineCreationCtaLogic.actions, 'onSampleEngineCreationSuccess');
61+
http.post.mockReturnValueOnce(Promise.resolve({}));
62+
63+
SampleEngineCreationCtaLogic.actions.createSampleEngine();
64+
await nextTick();
65+
66+
expect(
67+
SampleEngineCreationCtaLogic.actions.onSampleEngineCreationSuccess
68+
).toHaveBeenCalledTimes(1);
69+
});
70+
71+
it('calls onSampleEngineCreationFailure and flashAPIErrors on API Error', async () => {
72+
jest.spyOn(SampleEngineCreationCtaLogic.actions, 'onSampleEngineCreationFailure');
73+
http.post.mockReturnValueOnce(Promise.reject());
74+
75+
SampleEngineCreationCtaLogic.actions.createSampleEngine();
76+
await nextTick();
77+
78+
expect(flashAPIErrors).toHaveBeenCalledTimes(1);
79+
expect(
80+
SampleEngineCreationCtaLogic.actions.onSampleEngineCreationFailure
81+
).toHaveBeenCalledTimes(1);
82+
});
83+
});
84+
85+
it('onSampleEngineCreationSuccess should set a success message and navigate the user to the engine page', () => {
86+
SampleEngineCreationCtaLogic.actions.onSampleEngineCreationSuccess();
87+
88+
expect(setQueuedSuccessMessage).toHaveBeenCalledWith('Successfully created engine.');
89+
expect(navigateToUrl).toHaveBeenCalledWith('/engines/national-parks-demo');
90+
});
91+
});
92+
});

0 commit comments

Comments
 (0)