[Enterprise Search] Add callouts on product selector for license and trial status (#99122)

* Add isTrial selector to LicensingLogic

* Add LicenseCallout component

In order to match the existing design, I opted to use an extra EuiFlexItem for the gap between the button, instead of adding a stylesheet with padding, like the legacy version had. Verified it looks good on mobile as well.

* Add TrialCallout component

* Wire up new callouts

- Only render when the host is set, otherwise, fall back to the Setup Guide callout
- Add some extra padding with a larger spacer to beter match legacy UI

* Refactor for a better test

* Use isEmptyRender API instead of checking for length

Co-authored-by: Constance <constancecchen@users.noreply.github.com>

* DRY out callout heading

* Remove grow prop to align button to right

* Update button copy

* Replace EuiButton with EuiButtonTo

* Remove EuiText wrapper on link text

* Better test organization

* Remove unnecessarydivs

Co-authored-by: Constance <constancecchen@users.noreply.github.com>

* Refactor i18n for link

Also changes link style to underline

* Center-align trial callout

* Rename i18n ID

Co-authored-by: Constance <constancecchen@users.noreply.github.com>

* Add back and rename translations

Co-authored-by: Constance <constancecchen@users.noreply.github.com>
This commit is contained in:
Scotty Bollinger 2021-05-04 17:05:44 -05:00 committed by GitHub
parent d6b41ff7bf
commit 3e54390293
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 274 additions and 9 deletions

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
export const LICENSE_CALLOUT_BODY = i18n.translate('xpack.enterpriseSearch.licenseCalloutBody', {
defaultMessage:
'Enterprise authentication via SAML, document-level permission and authorization support, custom search experiences and more are available with a valid Platinum license.',
});
export const LICENSE_CALLOUT_BUTTON = i18n.translate(
'xpack.enterpriseSearch.licenseCalloutButton',
{
defaultMessage: 'Manage your license',
}
);

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { LicenseCallout } from './license_callout';

View file

@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { setMockValues } from '../../../__mocks__';
import React from 'react';
import { shallow } from 'enzyme';
import { EuiPanel, EuiText } from '@elastic/eui';
import { EuiButtonTo } from '../../../shared/react_router_helpers';
import { LicenseCallout } from './';
describe('LicenseCallout', () => {
it('renders when non-platinum or on trial', () => {
setMockValues({
hasPlatinumLicense: false,
isTrial: true,
});
const wrapper = shallow(<LicenseCallout />);
expect(wrapper.find(EuiPanel)).toHaveLength(1);
expect(wrapper.find(EuiText)).toHaveLength(2);
expect(wrapper.find(EuiButtonTo).prop('to')).toEqual(
'/app/management/stack/license_management'
);
});
it('does not render for platinum', () => {
setMockValues({
hasPlatinumLicense: true,
isTrial: false,
});
const wrapper = shallow(<LicenseCallout />);
expect(wrapper.isEmptyRender()).toBe(true);
});
});

View file

@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { useValues } from 'kea';
import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { LicensingLogic } from '../../../shared/licensing';
import { EuiButtonTo } from '../../../shared/react_router_helpers';
import { PRODUCT_SELECTOR_CALLOUT_HEADING } from '../../constants';
import { LICENSE_CALLOUT_BODY, LICENSE_CALLOUT_BUTTON } from './constants';
export const LicenseCallout: React.FC = () => {
const { hasPlatinumLicense, isTrial } = useValues(LicensingLogic);
if (hasPlatinumLicense && !isTrial) return null;
return (
<EuiPanel hasShadow={false} hasBorder className="productCard" paddingSize="l">
<EuiFlexGroup gutterSize="s" alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={7}>
<EuiText>
<h3>{PRODUCT_SELECTOR_CALLOUT_HEADING}</h3>
</EuiText>
<EuiText size="s">{LICENSE_CALLOUT_BODY}</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={1} />
<EuiFlexItem grow={false}>
<EuiButtonTo to="/app/management/stack/license_management" shouldNotCreateHref>
{LICENSE_CALLOUT_BUTTON}
</EuiButtonTo>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
};

View file

@ -13,8 +13,10 @@ import { shallow } from 'enzyme';
import { EuiPage } from '@elastic/eui';
import { LicenseCallout } from '../license_callout';
import { ProductCard } from '../product_card';
import { SetupGuideCta } from '../setup_guide';
import { TrialCallout } from '../trial_callout';
import { ProductSelector } from './';
@ -26,6 +28,15 @@ describe('ProductSelector', () => {
expect(wrapper.find(EuiPage).hasClass('enterpriseSearchOverview')).toBe(true);
expect(wrapper.find(ProductCard)).toHaveLength(2);
expect(wrapper.find(SetupGuideCta)).toHaveLength(1);
expect(wrapper.find(LicenseCallout)).toHaveLength(0);
});
it('renders the license and trial callouts', () => {
setMockValues({ config: { host: 'localhost' } });
const wrapper = shallow(<ProductSelector access={{}} />);
expect(wrapper.find(TrialCallout)).toHaveLength(1);
expect(wrapper.find(LicenseCallout)).toHaveLength(1);
});
describe('access checks when host is set', () => {

View file

@ -29,8 +29,10 @@ import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/
import AppSearchImage from '../../assets/app_search.png';
import WorkplaceSearchImage from '../../assets/workplace_search.png';
import { LicenseCallout } from '../license_callout';
import { ProductCard } from '../product_card';
import { SetupGuideCta } from '../setup_guide';
import { TrialCallout } from '../trial_callout';
interface ProductSelectorProps {
access: {
@ -53,6 +55,7 @@ export const ProductSelector: React.FC<ProductSelectorProps> = ({ access }) => {
<SendTelemetry action="viewed" metric="overview" />
<EuiPageBody>
<TrialCallout />
<EuiPageHeader>
<EuiPageHeaderSection className="enterpriseSearchOverview__header">
<EuiTitle size="l">
@ -88,8 +91,8 @@ export const ProductSelector: React.FC<ProductSelectorProps> = ({ access }) => {
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiSpacer />
{!config.host && <SetupGuideCta />}
<EuiSpacer size="xxl" />
{config.host ? <LicenseCallout /> : <SetupGuideCta />}
</EuiPageContentBody>
</EuiPageBody>
</EuiPage>

View file

@ -11,6 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EuiPanelTo } from '../../../shared/react_router_helpers';
import { PRODUCT_SELECTOR_CALLOUT_HEADING } from '../../constants';
import CtaImage from './assets/getting_started.png';
import './setup_guide_cta.scss';
@ -20,11 +21,7 @@ export const SetupGuideCta: React.FC = () => (
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem className="enterpriseSearchSetupCta__text">
<EuiTitle size="s">
<h2>
{i18n.translate('xpack.enterpriseSearch.overview.setupCta.title', {
defaultMessage: 'Enterprise-grade functionality for teams big and small',
})}
</h2>
<h2>{PRODUCT_SELECTOR_CALLOUT_HEADING}</h2>
</EuiTitle>
<EuiText size="s" color="subdued">
{i18n.translate('xpack.enterpriseSearch.overview.setupCta.description', {

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { TrialCallout } from './trial_callout';

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { setMockValues } from '../../../__mocks__';
import React from 'react';
import { shallow } from 'enzyme';
import { EuiCallOut } from '@elastic/eui';
import { TrialCallout } from './';
describe('TrialCallout', () => {
it('renders when non-platinum or on trial', () => {
setMockValues({ isTrial: true });
const wrapper = shallow(<TrialCallout />);
expect(wrapper.find(EuiCallOut)).toHaveLength(1);
});
it('does not render when not on trial', () => {
setMockValues({ isTrial: false });
const wrapper = shallow(<TrialCallout />);
expect(wrapper.find(EuiCallOut)).toHaveLength(0);
});
});

View file

@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { useValues } from 'kea';
import moment from 'moment';
import { EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { LicensingLogic } from '../../../shared/licensing';
export const TrialCallout: React.FC = () => {
const { license, isTrial } = useValues(LicensingLogic);
if (!isTrial) return null;
const title = (
<>
<FormattedMessage
id="xpack.enterpriseSearch.trialCalloutTitle"
defaultMessage="Your Elastic Stack Trial license, which enables Platinum features, expires in {days, plural, one {# day} other {# days}}."
values={{
days: moment(license?.expiryDateInMillis).diff(moment({ hours: 0 }), 'days'),
}}
/>{' '}
<EuiLink href="https://www.elastic.co/subscriptions" target="_blank">
<u>
<FormattedMessage
id="xpack.enterpriseSearch.trialCalloutLink"
defaultMessage="Learn more about Elastic Stack licenses."
/>
</u>
</EuiLink>
</>
);
return (
<>
<EuiCallOut size="s" title={title} iconType="iInCircle" style={{ margin: '0 auto' }} />
<EuiSpacer size="xxl" />
</>
);
};

View file

@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
export const PRODUCT_SELECTOR_CALLOUT_HEADING = i18n.translate(
'xpack.enterpriseSearch.productSelectorCalloutTitle',
{
defaultMessage: 'Enterprise-grade functionality for teams big and small',
}
);

View file

@ -158,5 +158,34 @@ describe('LicensingLogic', () => {
expect(LicensingLogic.values.hasGoldLicense).toEqual(false);
});
});
describe('isTrial', () => {
it('is true for active trial license', () => {
updateLicense({ status: 'active', type: 'trial' });
expect(LicensingLogic.values.isTrial).toEqual(true);
});
it('is false if the trial license is expired', () => {
updateLicense({ status: 'expired', type: 'trial' });
expect(LicensingLogic.values.isTrial).toEqual(false);
});
it('is false for all non-trial licenses', () => {
updateLicense({ status: 'active', type: 'basic' });
expect(LicensingLogic.values.isTrial).toEqual(false);
updateLicense({ status: 'active', type: 'standard' });
expect(LicensingLogic.values.isTrial).toEqual(false);
updateLicense({ status: 'active', type: 'gold' });
expect(LicensingLogic.values.isTrial).toEqual(false);
updateLicense({ status: 'active', type: 'platinum' });
expect(LicensingLogic.values.isTrial).toEqual(false);
updateLicense({ status: 'active', type: 'enterprise' });
expect(LicensingLogic.values.isTrial).toEqual(false);
});
});
});
});

View file

@ -15,6 +15,7 @@ interface LicensingValues {
licenseSubscription: Subscription | null;
hasPlatinumLicense: boolean;
hasGoldLicense: boolean;
isTrial: boolean;
}
interface LicensingActions {
setLicense(license: ILicense): ILicense;
@ -56,6 +57,10 @@ export const LicensingLogic = kea<MakeLogicType<LicensingValues, LicensingAction
return license?.isActive && qualifyingLicenses.includes(license?.type);
},
],
isTrial: [
(selectors) => [selectors.license],
(license) => license?.isActive && license?.type === 'trial',
],
},
events: ({ props, actions, values }) => ({
afterMount: () => {

View file

@ -7614,10 +7614,10 @@
"xpack.enterpriseSearch.overview.productCard.launchButton": "{productName}の起動",
"xpack.enterpriseSearch.overview.productCard.setupButton": "{productName} のセットアップ",
"xpack.enterpriseSearch.overview.setupCta.description": "Elastic App Search および Workplace Search を使用して、アプリまたは社内組織に検索を追加できます。検索が簡単になるとどのような利点があるのかについては、動画をご覧ください。",
"xpack.enterpriseSearch.overview.setupCta.title": "あらゆる規模のチームに対応するエンタープライズ級の機能",
"xpack.enterpriseSearch.overview.setupHeading": "セットアップする製品を選択し、開始してください。",
"xpack.enterpriseSearch.overview.subheading": "開始する製品を選択します。",
"xpack.enterpriseSearch.productName": "エンタープライズサーチ",
"xpack.enterpriseSearch.productSelectorCalloutTitle": "あらゆる規模のチームに対応するエンタープライズ級の機能",
"xpack.enterpriseSearch.readOnlyMode.warning": "エンタープライズ サーチは読み取り専用モードです。作成、編集、削除などの変更を実行できません。",
"xpack.enterpriseSearch.schema.addFieldModal.addFieldButtonLabel": "フィールドの追加",
"xpack.enterpriseSearch.schema.addFieldModal.description": "追加すると、フィールドはスキーマから削除されます。",

View file

@ -7684,10 +7684,10 @@
"xpack.enterpriseSearch.overview.productCard.launchButton": "推出 {productName}",
"xpack.enterpriseSearch.overview.productCard.setupButton": "设置 {productName}",
"xpack.enterpriseSearch.overview.setupCta.description": "通过 Elastic App Search 和 Workplace Search将搜索添加到您的应用或内部组织中。观看视频了解方便易用的搜索功能可以帮您做些什么。",
"xpack.enterpriseSearch.overview.setupCta.title": "适用于大型和小型团队的企业级功能",
"xpack.enterpriseSearch.overview.setupHeading": "选择产品进行设置并开始使用。",
"xpack.enterpriseSearch.overview.subheading": "选择产品开始使用。",
"xpack.enterpriseSearch.productName": "企业搜索",
"xpack.enterpriseSearch.productSelectorCalloutTitle": "适用于大型和小型团队的企业级功能",
"xpack.enterpriseSearch.readOnlyMode.warning": "企业搜索处于只读模式。您将无法执行更改,例如创建、编辑或删除。",
"xpack.enterpriseSearch.schema.addFieldModal.addFieldButtonLabel": "添加字段",
"xpack.enterpriseSearch.schema.addFieldModal.description": "字段添加后,将无法从架构中删除。",