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

[7.x] [Enterprise Search] Convert our public_url route to config_data and collect initialAppData (#75616) #75667

Merged
merged 1 commit into from
Aug 21, 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
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