Skip to content

Commit

Permalink
Add platinum licensing check to Meta Engines table/call (elastic#11)
Browse files Browse the repository at this point in the history
* Licensing plugin setup

* Add LicensingContext setup

* Update EngineOverview to not hit meta engines API on platinum license

* Add Jest test helpers for future shallow/context use
  • Loading branch information
Constance authored and cee-chen committed Jun 26, 2020
1 parent 6355395 commit 0bf21c1
Show file tree
Hide file tree
Showing 15 changed files with 204 additions and 34 deletions.
2 changes: 1 addition & 1 deletion x-pack/plugins/enterprise_search/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"id": "enterpriseSearch",
"version": "1.0.0",
"kibanaVersion": "kibana",
"requiredPlugins": ["home"],
"requiredPlugins": ["home", "licensing"],
"configPath": ["enterpriseSearch"],
"optionalPlugins": ["usageCollection"],
"server": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ export const mockKibanaContext = {
http: httpServiceMock.createSetupContract(),
setBreadcrumbs: jest.fn(),
enterpriseSearchUrl: 'http://localhost:3002',
license$: jest.fn(),
};
Original file line number Diff line number Diff line change
@@ -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(),
};
Original file line number Diff line number Diff line change
Expand Up @@ -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 })),
}));

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -24,7 +25,9 @@ describe('EngineOverview', () => {
// We use render() instead of mount() here to not trigger lifecycle methods (i.e., useEffect)
const wrapper = render(
<KibanaContext.Provider value={{ http: {} }}>
<EngineOverview />
<LicenseContext.Provider value={{ license: {} }}>
<EngineOverview />
</LicenseContext.Provider>
</KibanaContext.Provider>
);

Expand Down Expand Up @@ -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', () => {
Expand All @@ -95,12 +98,6 @@ describe('EngineOverview', () => {
pageIndex: 1,
},
});
expect(mockApi).toHaveBeenNthCalledWith(2, '/api/app_search/engines', {
query: {
type: 'meta',
pageIndex: 1,
},
});
});

describe('pagination', () => {
Expand Down Expand Up @@ -130,21 +127,49 @@ 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 };

// We get a lot of act() warning/errors in the terminal without this.
// 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(<EngineOverview />, { http: httpMock });
wrapper = mountWithKibanaContext(
<LicenseContext.Provider value={{ license }}>
<EngineOverview />
</LicenseContext.Provider>,
{ http: httpMock }
);
});
wrapper.update(); // This seems to be required for the DOM to actually update

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand Down Expand Up @@ -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 <ErrorState />;
if (hasNoAccount) return <NoUserState />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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('');
Expand Down
35 changes: 23 additions & 12 deletions x-pack/plugins/enterprise_search/public/applications/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,38 +9,49 @@ 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';

export interface IKibanaContext {
enterpriseSearchUrl?: string;
http(): HttpHandler;
setBreadCrumbs(): TSetBreadcrumbs;
license$: Observable<ILicense>;
}

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(
<KibanaContext.Provider
value={{
http: core.http,
enterpriseSearchUrl: config.host,
setBreadcrumbs: core.chrome.setBreadcrumbs,
license$: plugins.licensing.license$,
}}
>
<BrowserRouter basename={params.appBasePath}>
<Route exact path="/">
{/* This will eventually contain an Enterprise Search landing page,
and we'll also actually have a /workplace_search route */}
<Redirect to="/app_search" />
</Route>
<Route path="/app_search">
<AppSearch />
</Route>
</BrowserRouter>
<LicenseProvider>
<BrowserRouter basename={params.appBasePath}>
<Route exact path="/">
{/* This will eventually contain an Enterprise Search landing page,
and we'll also actually have a /workplace_search route */}
<Redirect to="/app_search" />
</Route>
<Route path="/app_search">
<AppSearch />
</Route>
</BrowserRouter>
</LicenseProvider>
</KibanaContext.Provider>,
params.element
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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);
};
Original file line number Diff line number Diff line change
@@ -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 <div className="license-test">{license.type}</div>;
};

it('renders children', () => {
const wrapper = mountWithKibanaContext(
<LicenseContext.Provider value={{ license: { type: 'basic' } }}>
<MockComponent />
</LicenseContext.Provider>
);

expect(wrapper.find('.license-test')).toHaveLength(1);
expect(wrapper.text()).toEqual('basic');
});
});
Original file line number Diff line number Diff line change
@@ -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 <LicenseContext.Provider value={{ license }} children={children} />;
};
Loading

0 comments on commit 0bf21c1

Please sign in to comment.