Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add platinum licensing check to Meta Engines table/call #11

Merged
merged 4 commits into from
May 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>,
Comment on lines +167 to +170
Copy link
Owner Author

@cee-chen cee-chen May 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on how often we use licensing context in the future, I might as a tech debt for some day just rename mountWithKibanaContext to mountWithContext and nest a <LicenseContext.Provider> in there by default, so we don't have to do it by hand

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point.

{ 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>
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the approach I used was heavily borrowed from the APM plugin's approach - I looked at 2-3 other plugins and how they handled licensing but ended up liking theirs as the most simple/cleanest/easiest to test.

I also did briefly play around with munging LicenseProvider into a single KibanaProvider declared within this file, but eventually opted to copy their setup - a separate context file is easier to test and notice the observable subscription in, and it's additionally very possible we might just continue to add contexts over time (although it would also be nice if we could handle that w/ Kea...)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, I like that you followed the approach the other plugin's use. That seems to make the most sense to me as well.

We don't have Kea yet, right? This could definitely be ported over at some point whenever we get there. It would probably be cleaner than mixing Contexts + Kea stores.

<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');
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have liked to potentially add a test that mocks the observable changing data and the basic text changing to 'trial' or some such, but I'm not sure how to do so. Open to ideas if anyone has them!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No ideas off of the top of my head.

});
});
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