diff --git a/x-pack/plugins/enterprise_search/kibana.json b/x-pack/plugins/enterprise_search/kibana.json index d0c4c9733da2a..3121d6bd470b0 100644 --- a/x-pack/plugins/enterprise_search/kibana.json +++ b/x-pack/plugins/enterprise_search/kibana.json @@ -2,7 +2,7 @@ "id": "enterpriseSearch", "version": "1.0.0", "kibanaVersion": "kibana", - "requiredPlugins": ["home"], + "requiredPlugins": ["home", "licensing"], "configPath": ["enterpriseSearch"], "optionalPlugins": ["usageCollection"], "server": true, diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts index cfe5a1e4c4ee2..5b19055115fde 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts @@ -6,6 +6,7 @@ export { mockHistory } from './react_router_history.mock'; export { mockKibanaContext } from './kibana_context.mock'; +export { mockLicenseContext } from './license_context.mock'; export { mountWithKibanaContext } from './mount_with_context.mock'; // Note: shallow_usecontext must be imported directly as a file diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts index fcfa1b0a21f13..3363c031da8ae 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts @@ -14,4 +14,5 @@ export const mockKibanaContext = { http: httpServiceMock.createSetupContract(), setBreadcrumbs: jest.fn(), enterpriseSearchUrl: 'http://localhost:3002', + license$: jest.fn(), }; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/license_context.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/license_context.mock.ts new file mode 100644 index 0000000000000..7c37ecc7cde1b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/license_context.mock.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { licensingMock } from '../../../../licensing/public/mocks'; + +export const mockLicenseContext = { + license: licensingMock.createLicense(), +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts index 5193a0cd299f8..20add45e16b58 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts @@ -5,14 +5,15 @@ */ /** - * NOTE: This variable name MUST start with 'mock*' in order for + * NOTE: These variable names MUST start with 'mock*' in order for * Jest to accept its use within a jest.mock() */ import { mockKibanaContext } from './kibana_context.mock'; +import { mockLicenseContext } from './license_context.mock'; jest.mock('react', () => ({ ...jest.requireActual('react'), - useContext: jest.fn(() => mockKibanaContext), + useContext: jest.fn(() => ({ ...mockKibanaContext, ...mockLicenseContext })), })); /** diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx index 2b712721a7fa5..5d029d6c4ba8a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx @@ -11,6 +11,7 @@ import { act } from 'react-dom/test-utils'; import { render } from 'enzyme'; import { KibanaContext } from '../../../'; +import { LicenseContext } from '../../../shared/licensing'; import { mountWithKibanaContext, mockKibanaContext } from '../../../__mocks__'; import { EmptyState, ErrorState, NoUserState } from '../empty_states'; @@ -24,7 +25,9 @@ describe('EngineOverview', () => { // We use render() instead of mount() here to not trigger lifecycle methods (i.e., useEffect) const wrapper = render( - + + + ); @@ -85,7 +88,7 @@ describe('EngineOverview', () => { }); it('renders', () => { - expect(wrapper.find(EngineTable)).toHaveLength(2); + expect(wrapper.find(EngineTable)).toHaveLength(1); }); it('calls the engines API', () => { @@ -95,12 +98,6 @@ describe('EngineOverview', () => { pageIndex: 1, }, }); - expect(mockApi).toHaveBeenNthCalledWith(2, '/api/app_search/engines', { - query: { - type: 'meta', - pageIndex: 1, - }, - }); }); describe('pagination', () => { @@ -130,13 +127,36 @@ describe('EngineOverview', () => { expect(getTablePagination().pageIndex).toEqual(4); }); }); + + describe('when on a platinum license', () => { + beforeAll(async () => { + mockApi.mockClear(); + wrapper = await mountWithApiMock({ + license: { type: 'platinum', isActive: true }, + get: mockApi, + }); + }); + + it('renders a 2nd meta engines table', () => { + expect(wrapper.find(EngineTable)).toHaveLength(2); + }); + + it('makes a 2nd call to the engines API with type meta', () => { + expect(mockApi).toHaveBeenNthCalledWith(2, '/api/app_search/engines', { + query: { + type: 'meta', + pageIndex: 1, + }, + }); + }); + }); }); /** * Test helpers */ - const mountWithApiMock = async ({ get }) => { + const mountWithApiMock = async ({ get, license }) => { let wrapper; const httpMock = { ...mockKibanaContext.http, get }; @@ -144,7 +164,12 @@ describe('EngineOverview', () => { // TBH, I don't fully understand why since Enzyme's mount is supposed to // have act() baked in - could be because of the wrapping context provider? await act(async () => { - wrapper = mountWithKibanaContext(, { http: httpMock }); + wrapper = mountWithKibanaContext( + + + , + { http: httpMock } + ); }); wrapper.update(); // This seems to be required for the DOM to actually update diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx index d87c36cd9b9d6..1e1a583b5bcdb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx @@ -17,6 +17,7 @@ import { import { SetAppSearchBreadcrumbs as SetBreadcrumbs } from '../../../shared/kibana_breadcrumbs'; import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; +import { LicenseContext, ILicenseContext, hasPlatinumLicense } from '../../../shared/licensing'; import { KibanaContext, IKibanaContext } from '../../../index'; import EnginesIcon from '../../assets/engine.svg'; @@ -30,6 +31,7 @@ import './engine_overview.scss'; export const EngineOverview: ReactFC<> = () => { const { http } = useContext(KibanaContext) as IKibanaContext; + const { license } = useContext(LicenseContext) as ILicenseContext; const [isLoading, setIsLoading] = useState(true); const [hasNoAccount, setHasNoAccount] = useState(false); @@ -72,11 +74,13 @@ export const EngineOverview: ReactFC<> = () => { }, [enginesPage]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { - const params = { type: 'meta', pageIndex: metaEnginesPage }; - const callbacks = { setResults: setMetaEngines, setResultsTotal: setMetaEnginesTotal }; + if (hasPlatinumLicense(license)) { + const params = { type: 'meta', pageIndex: metaEnginesPage }; + const callbacks = { setResults: setMetaEngines, setResultsTotal: setMetaEnginesTotal }; - setEnginesData(params, callbacks); - }, [metaEnginesPage]); // eslint-disable-line react-hooks/exhaustive-deps + setEnginesData(params, callbacks); + } + }, [license, metaEnginesPage]); // eslint-disable-line react-hooks/exhaustive-deps if (hasErrorConnecting) return ; if (hasNoAccount) return ; diff --git a/x-pack/plugins/enterprise_search/public/applications/index.test.ts b/x-pack/plugins/enterprise_search/public/applications/index.test.ts index 7ece7e153c154..7ea5b97feac6c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/index.test.ts @@ -5,14 +5,20 @@ */ import { coreMock } from 'src/core/public/mocks'; -import { renderApp } from '../applications'; +import { licensingMock } from '../../../licensing/public/mocks'; + +import { renderApp } from './'; describe('renderApp', () => { it('mounts and unmounts UI', () => { const params = coreMock.createAppMountParamters(); const core = coreMock.createStart(); + const config = {}; + const plugins = { + licensing: licensingMock.createSetup(), + }; - const unmount = renderApp(core, params, {}); + const unmount = renderApp(core, params, config, plugins); expect(params.element.querySelector('.setup-guide')).not.toBeNull(); unmount(); expect(params.element.innerHTML).toEqual(''); diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 473d395c1e604..0fb18a8b627bb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -9,8 +9,10 @@ import ReactDOM from 'react-dom'; import { BrowserRouter, Route, Redirect } from 'react-router-dom'; import { CoreStart, AppMountParams, HttpHandler } from 'src/core/public'; -import { ClientConfigType } from '../plugin'; +import { ClientConfigType, PluginsSetup } from '../plugin'; import { TSetBreadcrumbs } from './shared/kibana_breadcrumbs'; +import { ILicense } from '../../../../licensing/public'; +import { LicenseProvider } from './shared/licensing'; import { AppSearch } from './app_search'; @@ -18,29 +20,38 @@ export interface IKibanaContext { enterpriseSearchUrl?: string; http(): HttpHandler; setBreadCrumbs(): TSetBreadcrumbs; + license$: Observable; } export const KibanaContext = React.createContext(); -export const renderApp = (core: CoreStart, params: AppMountParams, config: ClientConfigType) => { +export const renderApp = ( + core: CoreStart, + params: AppMountParams, + config: ClientConfigType, + plugins: PluginsSetup +) => { ReactDOM.render( - - - {/* This will eventually contain an Enterprise Search landing page, - and we'll also actually have a /workplace_search route */} - - - - - - + + + + {/* This will eventually contain an Enterprise Search landing page, + and we'll also actually have a /workplace_search route */} + + + + + + + , params.element ); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts new file mode 100644 index 0000000000000..9c8c1417d48db --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +export { LicenseContext, LicenseProvider, ILicenseContext } from './license_context'; +export { hasPlatinumLicense } from './license_checks'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts new file mode 100644 index 0000000000000..e21bf004b39a2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { hasPlatinumLicense } from './license_checks'; + +describe('hasPlatinumLicense', () => { + it('is true for platinum licenses', () => { + expect(hasPlatinumLicense({ isActive: true, type: 'platinum' })).toEqual(true); + }); + + it('is true for enterprise licenses', () => { + expect(hasPlatinumLicense({ isActive: true, type: 'enterprise' })).toEqual(true); + }); + + it('is true for trial licenses', () => { + expect(hasPlatinumLicense({ isActive: true, type: 'platinum' })).toEqual(true); + }); + + it('is false if the current license is expired', () => { + expect(hasPlatinumLicense({ isActive: false, type: 'platinum' })).toEqual(false); + expect(hasPlatinumLicense({ isActive: false, type: 'enterprise' })).toEqual(false); + expect(hasPlatinumLicense({ isActive: false, type: 'trial' })).toEqual(false); + }); + + it('is false for licenses below platinum', () => { + expect(hasPlatinumLicense({ isActive: true, type: 'basic' })).toEqual(false); + expect(hasPlatinumLicense({ isActive: false, type: 'standard' })).toEqual(false); + expect(hasPlatinumLicense({ isActive: true, type: 'gold' })).toEqual(false); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts new file mode 100644 index 0000000000000..7d0de8a093b31 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ILicense } from '../../../../../../licensing/public'; + +export const hasPlatinumLicense = (license: ILicenseContext) => { + return license?.isActive && ['platinum', 'enterprise', 'trial'].includes(license?.type); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.test.tsx new file mode 100644 index 0000000000000..3385f79d3d075 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.test.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useContext } from 'react'; + +import { mountWithKibanaContext } from '../../__mocks__'; +import { LicenseContext, ILicenseContext } from './'; + +describe('LicenseProvider', () => { + const MockComponent: React.FC<> = () => { + const { license } = useContext(LicenseContext) as ILicenseContext; + return
{license.type}
; + }; + + it('renders children', () => { + const wrapper = mountWithKibanaContext( + + + + ); + + expect(wrapper.find('.license-test')).toHaveLength(1); + expect(wrapper.text()).toEqual('basic'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.tsx new file mode 100644 index 0000000000000..c8295196aedb5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useContext } from 'react'; +import useObservable from 'react-use/lib/useObservable'; + +import { KibanaContext, IKibanaContext } from '../../'; + +import { ILicense } from '../../../../licensing/public'; + +export interface ILicenseContext { + license?: ILicense; +} + +export const LicenseContext = React.createContext(); + +export const LicenseProvider: React.FC<> = ({ children }) => { + // Listen for changes to license subscription + const { license$ } = useContext(KibanaContext) as IKibanaContext; + const license = useObservable(license$); + + // Render rest of application and pass down license via context + return ; +}; diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 5331eb1e8f51f..cf495b6a6f9de 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -17,6 +17,7 @@ import { HomePublicPluginSetup, } from '../../../../src/plugins/home/public'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; +import { LicensingPluginSetup } from '../../licensing/public'; import AppSearchLogo from './applications/app_search/assets/logo.svg'; @@ -24,7 +25,8 @@ export interface ClientConfigType { host?: string; } export interface PluginsSetup { - home?: HomePublicPluginSetup; + home: HomePublicPluginSetup; + licensing: LicensingPluginSetup; } export class EnterpriseSearchPlugin implements Plugin { @@ -43,11 +45,12 @@ export class EnterpriseSearchPlugin implements Plugin { euiIconType: AppSearchLogo, // TODO: Temporary - App Search will likely no longer need an icon once the nav structure changes. category: DEFAULT_APP_CATEGORIES.management, // TODO - This is likely not final/correct order: 10, // TODO - This will also likely not be needed once new nav structure changes land - async mount(params: AppMountParameters) { + mount: async (params: AppMountParameters) => { const [coreStart] = await core.getStartServices(); + const { renderApp } = await import('./applications'); - return renderApp(coreStart, params, config); + return renderApp(coreStart, params, config, plugins); }, });