Skip to content

Commit

Permalink
[Enterprise Search] Convert our public_url route to config_data a…
Browse files Browse the repository at this point in the history
…nd collect initialAppData (#75616)

* [Setup] DRY out stripTrailingSlash helper

- DRYs out repeated code
- This will be used by an upcoming server/ endpoint change, hence why it's in common

* [Setup] DRY out initial app data types to common/types

- In preparation for upcoming server logic that will need to reuse these types
+ DRY out and clean up workplace_search types
  - remove unused supportEligible
  - remove currentUser - unneeded in Kibana

* Update callEnterpriseSearchConfigAPI to parse and fetch new expected data

* Remove /public_url API for /config_data

* Remove getPublicUrl in favor of directly calling the new /config_data API from public/plugin

+ set returned initialData in this.data

* Set up product apps to be passed initial data as props

* Fix for Kea/redux state not resetting between AS<->WS nav

- resetContext at the top level only gets called once total on first plugin load and never after, causing navigating between WS and AS to crash when both have Kea - this fixes the issue

- moves redux Provider to top level app as well

* Add very basic Kea logic file to App Search

* Finish AppSearchConfigured tests & set up kea+useEffect mocks

* [Cleanup] DRY out repeated mock initialAppData to a reusable defaults constant
  • Loading branch information
Constance authored and cee-chen committed Aug 21, 2020
1 parent 65293aa commit 6bb1409
Show file tree
Hide file tree
Showing 31 changed files with 573 additions and 211 deletions.
7 changes: 7 additions & 0 deletions x-pack/plugins/enterprise_search/common/__mocks__/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* 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 * from './initial_app_data';
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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 const DEFAULT_INITIAL_APP_DATA = {
readOnlyMode: false,
ilmEnabled: true,
configuredLimits: {
maxDocumentByteSize: 102400,
maxEnginesPerMetaEngine: 15,
},
appSearch: {
accountId: 'some-id-string',
onBoardingComplete: true,
role: {
id: 'account_id:somestring|user_oid:somestring',
roleType: 'owner',
ability: {
accessAllEngines: true,
destroy: ['session'],
manage: ['account_credentials', 'account_engines'], // etc
edit: ['LocoMoco::Account'], // etc
view: ['Engine'], // etc
credentialTypes: ['admin', 'private', 'search'],
availableRoleTypes: ['owner', 'admin'],
},
},
},
workplaceSearch: {
organization: {
name: 'ACME Donuts',
defaultOrgName: 'My Organization',
},
fpAccount: {
id: 'some-id-string',
groups: ['Default', 'Cats'],
isAdmin: true,
canCreatePersonalSources: true,
isCurated: false,
viewedOnboardingPage: true,
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* 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 { stripTrailingSlash } from './';

describe('Strip Trailing Slash helper', () => {
it('strips trailing slashes', async () => {
expect(stripTrailingSlash('http://trailing.slash/')).toEqual('http://trailing.slash');
});

it('does nothing is there is no trailing slash', async () => {
expect(stripTrailingSlash('http://ok.url')).toEqual('http://ok.url');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* 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.
*/

/**
* Small helper for stripping trailing slashes from URLs or paths
* (usually ones that come in from React Router or API endpoints)
*/
export const stripTrailingSlash = (url: string): string => {
return url && url.endsWith('/') ? url.slice(0, -1) : url;
};
25 changes: 25 additions & 0 deletions x-pack/plugins/enterprise_search/common/types/app_search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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 interface IAccount {
accountId: string;
onBoardingComplete: boolean;
role: IRole;
}

export interface IRole {
id: string;
roleType: string;
ability: {
accessAllEngines: boolean;
destroy: string[];
manage: string[];
edit: string[];
view: string[];
credentialTypes: string[];
availableRoleTypes: string[];
};
}
24 changes: 24 additions & 0 deletions x-pack/plugins/enterprise_search/common/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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 { IAccount as IAppSearchAccount } from './app_search';
import { IAccount as IWorkplaceSearchAccount, IOrganization } from './workplace_search';

export interface IInitialAppData {
readOnlyMode?: boolean;
ilmEnabled?: boolean;
configuredLimits?: IConfiguredLimits;
appSearch?: IAppSearchAccount;
workplaceSearch?: {
organization: IOrganization;
fpAccount: IWorkplaceSearchAccount;
};
}

export interface IConfiguredLimits {
maxDocumentByteSize: number;
maxEnginesPerMetaEngine: number;
}
19 changes: 19 additions & 0 deletions x-pack/plugins/enterprise_search/common/types/workplace_search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* 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 interface IAccount {
id: string;
groups: string[];
isAdmin: boolean;
isCurated: boolean;
canCreatePersonalSources: boolean;
viewedOnboardingPage: boolean;
}

export interface IOrganization {
name: string;
defaultOrgName: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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.
*/

jest.mock('kea', () => ({
...(jest.requireActual('kea') as object),
useValues: jest.fn(() => ({})),
useActions: jest.fn(() => ({})),
}));

/**
* Example usage within a component test:
*
* import '../../../__mocks__/kea'; // Must come before kea's import, adjust relative path as needed
*
* import { useActions, useValues } from 'kea';
*
* it('some test', () => {
* (useValues as jest.Mock).mockImplementationOnce(() => ({ someValue: 'hello' }));
* (useActions as jest.Mock).mockImplementationOnce(() => ({ someAction: () => 'world' }));
* });
*/
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { mockLicenseContext } from './license_context.mock';
jest.mock('react', () => ({
...(jest.requireActual('react') as object),
useContext: jest.fn(() => ({ ...mockKibanaContext, ...mockLicenseContext })),
useEffect: jest.fn((fn) => fn()), // Calls on mount/every update - use mount for more complex behavior
}));

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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 { resetContext } from 'kea';

import { DEFAULT_INITIAL_APP_DATA } from '../../../common/__mocks__';
import { AppLogic } from './app_logic';

describe('AppLogic', () => {
beforeEach(() => {
resetContext({});
AppLogic.mount();
});

const DEFAULT_VALUES = {
hasInitialized: false,
};

it('has expected default values', () => {
expect(AppLogic.values).toEqual(DEFAULT_VALUES);
});

describe('initializeAppData()', () => {
it('sets values based on passed props', () => {
AppLogic.actions.initializeAppData(DEFAULT_INITIAL_APP_DATA);

expect(AppLogic.values).toEqual({
hasInitialized: true,
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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 { kea } from 'kea';

import { IInitialAppData } from '../../../common/types';
import { IKeaLogic } from '../shared/types';

export interface IAppLogicValues {
hasInitialized: boolean;
}
export interface IAppLogicActions {
initializeAppData(props: IInitialAppData): void;
}

export const AppLogic = kea({
actions: (): IAppLogicActions => ({
initializeAppData: (props) => props,
}),
reducers: () => ({
hasInitialized: [
false,
{
initializeAppData: () => true,
},
],
}),
}) as IKeaLogic<IAppLogicValues, IAppLogicActions>;
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,68 @@
*/

import '../__mocks__/shallow_usecontext.mock';
import '../__mocks__/kea.mock';

import React, { useContext } from 'react';
import { Redirect } from 'react-router-dom';
import { shallow } from 'enzyme';
import { useValues, useActions } from 'kea';

import { SetupGuide } from './components/setup_guide';
import { Layout, SideNav, SideNavLink } from '../shared/layout';
import { AppSearch, AppSearchNav } from './';
import { AppSearch, AppSearchUnconfigured, AppSearchConfigured, AppSearchNav } from './';

describe('AppSearch', () => {
it('renders', () => {
it('renders AppSearchUnconfigured when config.host is not set', () => {
(useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } }));
const wrapper = shallow(<AppSearch />);

expect(wrapper.find(Layout)).toHaveLength(1);
expect(wrapper.find(AppSearchUnconfigured)).toHaveLength(1);
});

it('redirects to Setup Guide when config.host is not set', () => {
(useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } }));
it('renders AppSearchConfigured when config.host set', () => {
(useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'some.url' } }));
const wrapper = shallow(<AppSearch />);

expect(wrapper.find(AppSearchConfigured)).toHaveLength(1);
});
});

describe('AppSearchUnconfigured', () => {
it('renders the Setup Guide and redirects to the Setup Guide', () => {
const wrapper = shallow(<AppSearchUnconfigured />);

expect(wrapper.find(SetupGuide)).toHaveLength(1);
expect(wrapper.find(Redirect)).toHaveLength(1);
expect(wrapper.find(Layout)).toHaveLength(0);
});
});

describe('AppSearchConfigured', () => {
it('renders with layout', () => {
(useActions as jest.Mock).mockImplementation(() => ({ initializeAppData: () => {} }));

const wrapper = shallow(<AppSearchConfigured />);

expect(wrapper.find(Layout)).toHaveLength(1);
});

it('initializes app data with passed props', () => {
const initializeAppData = jest.fn();
(useActions as jest.Mock).mockImplementation(() => ({ initializeAppData }));

shallow(<AppSearchConfigured readOnlyMode={true} />);

expect(initializeAppData).toHaveBeenCalledWith({ readOnlyMode: true });
});

it('does not re-initialize app data', () => {
const initializeAppData = jest.fn();
(useActions as jest.Mock).mockImplementation(() => ({ initializeAppData }));
(useValues as jest.Mock).mockImplementationOnce(() => ({ hasInitialized: true }));

shallow(<AppSearchConfigured />);

expect(initializeAppData).not.toHaveBeenCalled();
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useContext } from 'react';
import React, { useContext, useEffect } from 'react';
import { Route, Redirect, Switch } from 'react-router-dom';
import { useActions, useValues } from 'kea';

import { i18n } from '@kbn/i18n';

import { APP_SEARCH_PLUGIN } from '../../../common/constants';
import { KibanaContext, IKibanaContext } from '../index';
import { AppLogic, IAppLogicActions, IAppLogicValues } from './app_logic';
import { IInitialAppData } from '../../../common/types';

import { APP_SEARCH_PLUGIN } from '../../../common/constants';
import { Layout, SideNav, SideNavLink } from '../shared/layout';

import {
Expand All @@ -25,20 +29,29 @@ import {
import { SetupGuide } from './components/setup_guide';
import { EngineOverview } from './components/engine_overview';

export const AppSearch: React.FC = () => {
export const AppSearch: React.FC<IInitialAppData> = (props) => {
const { config } = useContext(KibanaContext) as IKibanaContext;
return !config.host ? <AppSearchUnconfigured /> : <AppSearchConfigured {...props} />;
};

export const AppSearchUnconfigured: React.FC = () => (
<Switch>
<Route exact path={SETUP_GUIDE_PATH}>
<SetupGuide />
</Route>
<Route>
<Redirect to={SETUP_GUIDE_PATH} />
</Route>
</Switch>
);

export const AppSearchConfigured: React.FC<IInitialAppData> = (props) => {
const { hasInitialized } = useValues(AppLogic) as IAppLogicValues;
const { initializeAppData } = useActions(AppLogic) as IAppLogicActions;

if (!config.host)
return (
<Switch>
<Route exact path={SETUP_GUIDE_PATH}>
<SetupGuide />
</Route>
<Route>
<Redirect to={SETUP_GUIDE_PATH} />
</Route>
</Switch>
);
useEffect(() => {
if (!hasInitialized) initializeAppData(props);
}, [hasInitialized]);

return (
<Switch>
Expand Down
Loading

0 comments on commit 6bb1409

Please sign in to comment.