Skip to content

Commit d320bb9

Browse files
scottybollingerJasonStoltzConstance
authored
[Enterprise Search] Update Product Selector and add Setup Guide (#78233) (#78381)
* Add conditional button text - Only shows error connectign if host is set - Removes conditional rendering of cards - Changes the action text from “Launch” to “Setup” * Add setup guide * Extract ProductSelector to component * Update index and add routes * Change setup guide text * Fix imports * Add missing mock * Update x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx Co-authored-by: Jason Stoltzfus <[email protected]> * Remove Literals Co-authored-by: Constance <[email protected]> * Remove Literals II - The Force Awakens Co-authored-by: Constance <[email protected]> * Add back access checks * Remove hard-coded props 🤦🏼‍♂️ * Remove data-test-subj attr * Reafactor access check variables * Remove unused beforeEach Co-authored-by: Constance <[email protected]> * Add newline Co-authored-by: Constance <[email protected]> * Update image to compressed * Remove unused things * Update to new way of using lodash things 🤷🏽‍♀️ Co-authored-by: Jason Stoltzfus <[email protected]> Co-authored-by: Constance <[email protected]> Co-authored-by: Jason Stoltzfus <[email protected]> Co-authored-by: Constance <[email protected]>
1 parent 5aed1a1 commit d320bb9

File tree

12 files changed

+330
-110
lines changed

12 files changed

+330
-110
lines changed

x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
*/
66

77
import '../../../__mocks__/kea.mock';
8+
import '../../../__mocks__/shallow_usecontext.mock';
89

9-
import React from 'react';
10+
import React, { useContext } from 'react';
1011
import { shallow } from 'enzyme';
1112

1213
import { EuiCard } from '@elastic/eui';
@@ -26,6 +27,7 @@ describe('ProductCard', () => {
2627
});
2728

2829
it('renders an App Search card', () => {
30+
(useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'localhost' } }));
2931
const wrapper = shallow(<ProductCard product={APP_SEARCH_PLUGIN} image="as.jpg" />);
3032
const card = wrapper.find(EuiCard).dive().shallow();
3133

@@ -34,13 +36,14 @@ describe('ProductCard', () => {
3436

3537
const button = card.find(EuiButton);
3638
expect(button.prop('to')).toEqual('/app/enterprise_search/app_search');
37-
expect(button.prop('data-test-subj')).toEqual('LaunchAppSearchButton');
39+
expect(button.prop('children')).toEqual('Launch App Search');
3840

3941
button.simulate('click');
4042
expect(sendTelemetry).toHaveBeenCalledWith(expect.objectContaining({ metric: 'app_search' }));
4143
});
4244

4345
it('renders a Workplace Search card', () => {
46+
(useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'localhost' } }));
4447
const wrapper = shallow(<ProductCard product={WORKPLACE_SEARCH_PLUGIN} image="ws.jpg" />);
4548
const card = wrapper.find(EuiCard).dive().shallow();
4649

@@ -49,11 +52,21 @@ describe('ProductCard', () => {
4952

5053
const button = card.find(EuiButton);
5154
expect(button.prop('to')).toEqual('/app/enterprise_search/workplace_search');
52-
expect(button.prop('data-test-subj')).toEqual('LaunchWorkplaceSearchButton');
55+
expect(button.prop('children')).toEqual('Launch Workplace Search');
5356

5457
button.simulate('click');
5558
expect(sendTelemetry).toHaveBeenCalledWith(
5659
expect.objectContaining({ metric: 'workplace_search' })
5760
);
5861
});
62+
63+
it('renders correct button text when host not present', () => {
64+
(useContext as jest.Mock).mockImplementation(() => ({ config: { host: '' } }));
65+
66+
const wrapper = shallow(<ProductCard product={WORKPLACE_SEARCH_PLUGIN} image="ws.jpg" />);
67+
const card = wrapper.find(EuiCard).dive().shallow();
68+
const button = card.find(EuiButton);
69+
70+
expect(button.prop('children')).toEqual('Setup Workplace Search');
71+
});
5972
});

x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import React from 'react';
7+
import React, { useContext } from 'react';
88
import { useValues } from 'kea';
9-
import upperFirst from 'lodash/upperFirst';
10-
import snakeCase from 'lodash/snakeCase';
9+
import { snakeCase } from 'lodash';
1110
import { i18n } from '@kbn/i18n';
1211
import { EuiCard, EuiTextColor } from '@elastic/eui';
1312

13+
import { KibanaContext, IKibanaContext } from '../../../index';
14+
1415
import { EuiButton } from '../../../shared/react_router_helpers';
1516
import { sendTelemetry } from '../../../shared/telemetry';
1617
import { HttpLogic } from '../../../shared/http';
@@ -30,6 +31,25 @@ interface IProductCard {
3031

3132
export const ProductCard: React.FC<IProductCard> = ({ product, image }) => {
3233
const { http } = useValues(HttpLogic);
34+
const {
35+
config: { host },
36+
} = useContext(KibanaContext) as IKibanaContext;
37+
38+
const LAUNCH_BUTTON_TEXT = i18n.translate(
39+
'xpack.enterpriseSearch.overview.productCard.launchButton',
40+
{
41+
defaultMessage: 'Launch {productName}',
42+
values: { productName: product.NAME },
43+
}
44+
);
45+
46+
const SETUP_BUTTON_TEXT = i18n.translate(
47+
'xpack.enterpriseSearch.overview.productCard.setupButton',
48+
{
49+
defaultMessage: 'Setup {productName}',
50+
values: { productName: product.NAME },
51+
}
52+
);
3353

3454
return (
3555
<EuiCard
@@ -59,12 +79,8 @@ export const ProductCard: React.FC<IProductCard> = ({ product, image }) => {
5979
metric: snakeCase(product.ID),
6080
})
6181
}
62-
data-test-subj={`Launch${upperFirst(product.ID)}Button`}
6382
>
64-
{i18n.translate('xpack.enterpriseSearch.overview.productCard.button', {
65-
defaultMessage: `Launch {productName}`,
66-
values: { productName: product.NAME },
67-
})}
83+
{host ? LAUNCH_BUTTON_TEXT : SETUP_BUTTON_TEXT}
6884
</EuiButton>
6985
}
7086
/>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
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+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
export { ProductSelector } from './product_selector';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import '../../../__mocks__/shallow_usecontext.mock';
8+
9+
import React, { useContext } from 'react';
10+
import { shallow } from 'enzyme';
11+
import { EuiPage } from '@elastic/eui';
12+
13+
import { ProductSelector } from './';
14+
import { ProductCard } from '../product_card';
15+
16+
describe('ProductSelector', () => {
17+
it('renders the overview page and product cards with no host set', () => {
18+
(useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } }));
19+
const wrapper = shallow(<ProductSelector access={{}} />);
20+
21+
expect(wrapper.find(EuiPage).hasClass('enterpriseSearchOverview')).toBe(true);
22+
expect(wrapper.find(ProductCard)).toHaveLength(2);
23+
});
24+
25+
describe('access checks when host is set', () => {
26+
beforeEach(() => {
27+
(useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'localhost' } }));
28+
});
29+
30+
it('does not render the App Search card if the user does not have access to AS', () => {
31+
const wrapper = shallow(
32+
<ProductSelector access={{ hasAppSearchAccess: false, hasWorkplaceSearchAccess: true }} />
33+
);
34+
35+
expect(wrapper.find(ProductCard)).toHaveLength(1);
36+
expect(wrapper.find(ProductCard).prop('product').ID).toEqual('workplaceSearch');
37+
});
38+
39+
it('does not render the Workplace Search card if the user does not have access to WS', () => {
40+
const wrapper = shallow(
41+
<ProductSelector access={{ hasAppSearchAccess: true, hasWorkplaceSearchAccess: false }} />
42+
);
43+
44+
expect(wrapper.find(ProductCard)).toHaveLength(1);
45+
expect(wrapper.find(ProductCard).prop('product').ID).toEqual('appSearch');
46+
});
47+
48+
it('does not render any cards if the user does not have access', () => {
49+
const wrapper = shallow(<ProductSelector access={{}} />);
50+
51+
expect(wrapper.find(ProductCard)).toHaveLength(0);
52+
});
53+
});
54+
});
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
/*
7+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
8+
* or more contributor license agreements. Licensed under the Elastic License;
9+
* you may not use this file except in compliance with the Elastic License.
10+
*/
11+
12+
import React, { useContext } from 'react';
13+
14+
import {
15+
EuiPage,
16+
EuiPageBody,
17+
EuiPageHeader,
18+
EuiPageHeaderSection,
19+
EuiPageContentBody,
20+
EuiFlexGroup,
21+
EuiFlexItem,
22+
EuiSpacer,
23+
EuiTitle,
24+
} from '@elastic/eui';
25+
import { i18n } from '@kbn/i18n';
26+
27+
import { KibanaContext, IKibanaContext } from '../../../index';
28+
29+
import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants';
30+
31+
import { SetEnterpriseSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
32+
import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/telemetry';
33+
34+
import { ProductCard } from '../product_card';
35+
36+
import AppSearchImage from '../../assets/app_search.png';
37+
import WorkplaceSearchImage from '../../assets/workplace_search.png';
38+
39+
interface IProductSelectorProps {
40+
access: {
41+
hasAppSearchAccess?: boolean;
42+
hasWorkplaceSearchAccess?: boolean;
43+
};
44+
}
45+
46+
export const ProductSelector: React.FC<IProductSelectorProps> = ({ access }) => {
47+
const { hasAppSearchAccess, hasWorkplaceSearchAccess } = access;
48+
const {
49+
config: { host },
50+
} = useContext(KibanaContext) as IKibanaContext;
51+
52+
const shouldShowAppSearchCard = !host || hasAppSearchAccess;
53+
const shouldShowWorkplaceSearchCard = !host || hasWorkplaceSearchAccess;
54+
55+
return (
56+
<EuiPage restrictWidth className="enterpriseSearchOverview">
57+
<SetPageChrome isRoot />
58+
<SendTelemetry action="viewed" metric="overview" />
59+
60+
<EuiPageBody>
61+
<EuiPageHeader>
62+
<EuiPageHeaderSection className="enterpriseSearchOverview__header">
63+
<EuiTitle size="l">
64+
<h1 className="enterpriseSearchOverview__heading">
65+
{i18n.translate('xpack.enterpriseSearch.overview.heading', {
66+
defaultMessage: 'Welcome to Elastic Enterprise Search',
67+
})}
68+
</h1>
69+
</EuiTitle>
70+
<EuiTitle size="s">
71+
<p className="enterpriseSearchOverview__subheading">
72+
{i18n.translate('xpack.enterpriseSearch.overview.subheading', {
73+
defaultMessage: 'Select a product to get started',
74+
})}
75+
</p>
76+
</EuiTitle>
77+
</EuiPageHeaderSection>
78+
</EuiPageHeader>
79+
<EuiPageContentBody>
80+
<EuiFlexGroup justifyContent="center" gutterSize="xl">
81+
{shouldShowAppSearchCard && (
82+
<EuiFlexItem grow={false} className="enterpriseSearchOverview__card">
83+
<ProductCard product={APP_SEARCH_PLUGIN} image={AppSearchImage} />
84+
</EuiFlexItem>
85+
)}
86+
{shouldShowWorkplaceSearchCard && (
87+
<EuiFlexItem grow={false} className="enterpriseSearchOverview__card">
88+
<ProductCard product={WORKPLACE_SEARCH_PLUGIN} image={WorkplaceSearchImage} />
89+
</EuiFlexItem>
90+
)}
91+
</EuiFlexGroup>
92+
<EuiSpacer />
93+
</EuiPageContentBody>
94+
</EuiPageBody>
95+
</EuiPage>
96+
);
97+
};
190 KB
Loading
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
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+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
export { SetupGuide } from './setup_guide';
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import React from 'react';
8+
import { shallow } from 'enzyme';
9+
10+
import { SetEnterpriseSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
11+
import { SetupGuide as SetupGuideLayout } from '../../../shared/setup_guide';
12+
import { SetupGuide } from './';
13+
14+
describe('SetupGuide', () => {
15+
it('renders', () => {
16+
const wrapper = shallow(<SetupGuide />);
17+
18+
expect(wrapper.find(SetupGuideLayout)).toHaveLength(1);
19+
expect(wrapper.find(SetPageChrome)).toHaveLength(1);
20+
});
21+
});
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import React from 'react';
8+
import { EuiSpacer, EuiTitle, EuiText } from '@elastic/eui';
9+
import { FormattedMessage } from '@kbn/i18n/react';
10+
import { i18n } from '@kbn/i18n';
11+
12+
import { ENTERPRISE_SEARCH_PLUGIN } from '../../../../../common/constants';
13+
import { SetupGuide as SetupGuideLayout } from '../../../shared/setup_guide';
14+
import { SetEnterpriseSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
15+
import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/telemetry';
16+
import GettingStarted from './assets/getting_started.png';
17+
18+
export const SetupGuide: React.FC = () => (
19+
<SetupGuideLayout
20+
productName={ENTERPRISE_SEARCH_PLUGIN.NAME}
21+
productEuiIcon="logoEnterpriseSearch"
22+
standardAuthLink="https://www.elastic.co/guide/en/app-search/current/security-and-users.html#app-search-self-managed-security-and-user-management-standard"
23+
elasticsearchNativeAuthLink="https://www.elastic.co/guide/en/app-search/current/security-and-users.html#app-search-self-managed-security-and-user-management-elasticsearch-native-realm"
24+
>
25+
<SetPageChrome
26+
text={i18n.translate('xpack.enterpriseSearch.setupGuide.title', {
27+
defaultMessage: 'Setup Guide',
28+
})}
29+
/>
30+
<SendTelemetry action="viewed" metric="setup_guide" />
31+
32+
<a href="https://www.elastic.co/enterprise-search" target="_blank" rel="noopener noreferrer">
33+
<img
34+
className="setupGuide__thumbnail"
35+
src={GettingStarted}
36+
alt={i18n.translate('xpack.enterpriseSearch.enterpriseSearch.setupGuide.videoAlt', {
37+
defaultMessage: 'Getting started with Enterprise Search',
38+
})}
39+
width="1280"
40+
height-="720"
41+
/>
42+
</a>
43+
44+
<EuiTitle size="s">
45+
<p>
46+
<FormattedMessage
47+
id="xpack.enterpriseSearch.enterpriseSearch.setupGuide.description"
48+
defaultMessage="Search everything, anywhere. Easily implement powerful, modern search experiences for your busy team. Quickly add pre-tuned search to your website, app, or workplace. Search it all, simply."
49+
/>
50+
</p>
51+
</EuiTitle>
52+
<EuiSpacer size="m" />
53+
<EuiText>
54+
<p>
55+
<FormattedMessage
56+
id="xpack.enterpriseSearch.enterpriseSearch.setupGuide.notConfigured"
57+
defaultMessage="Enterprise Search is not configured in your Kibana instance yet."
58+
/>
59+
</p>
60+
</EuiText>
61+
</SetupGuideLayout>
62+
);

0 commit comments

Comments
 (0)