mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
d6b41ff7bf
commit
3e54390293
15 changed files with 274 additions and 9 deletions
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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';
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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', () => {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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';
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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" />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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: () => {
|
||||
|
|
|
@ -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": "追加すると、フィールドはスキーマから削除されます。",
|
||||
|
|
|
@ -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": "字段添加后,将无法从架构中删除。",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue