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

[Enterprise Search] Set up initial KibanaPageTemplate #102170

Merged
merged 10 commits into from
Jun 16, 2021
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 @@ -7,7 +7,7 @@
"optionalPlugins": ["usageCollection", "security", "home", "spaces", "cloud"],
"server": true,
"ui": true,
"requiredBundles": ["home"],
"requiredBundles": ["home", "kibanaReact"],
"owner": {
"name": "Enterprise Search",
"githubTeam": "enterprise-search-frontend"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export { AppSearchPageTemplate } from './page_template';
export { useAppSearchNav } from './nav';
export { KibanaHeaderActions } from './kibana_header_actions';
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { setMockValues } from '../../../__mocks__/kea_logic';

jest.mock('../../../shared/layout', () => ({
generateNavLink: jest.fn(({ to }) => ({ href: to })),
}));

import { useAppSearchNav } from './nav';

describe('useAppSearchNav', () => {
const MOCK_DEFAULT_NAV = [
{
id: 'engines',
name: 'Engines',
href: '/engines',
items: [],
},
];

it('always generates a default engines nav item', () => {
setMockValues({ myRole: {} });

expect(useAppSearchNav()).toEqual(MOCK_DEFAULT_NAV);
});

it('generates a settings nav item if the user can view settings', () => {
setMockValues({ myRole: { canViewSettings: true } });

expect(useAppSearchNav()).toEqual([
...MOCK_DEFAULT_NAV,
{
id: 'settings',
name: 'Settings',
href: '/settings',
},
]);
});

it('generates a credentials nav item if the user can view credentials', () => {
setMockValues({ myRole: { canViewAccountCredentials: true } });

expect(useAppSearchNav()).toEqual([
...MOCK_DEFAULT_NAV,
{
id: 'credentials',
name: 'Credentials',
href: '/credentials',
},
]);
});

it('generates a users & roles nav item if the user can view role mappings', () => {
setMockValues({ myRole: { canViewRoleMappings: true } });

expect(useAppSearchNav()).toEqual([
...MOCK_DEFAULT_NAV,
{
id: 'usersRoles',
name: 'Users & roles',
href: '/role_mappings',
},
]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { useValues } from 'kea';

import { EuiSideNavItemType } from '@elastic/eui';

import { generateNavLink } from '../../../shared/layout';
import { ROLE_MAPPINGS_TITLE } from '../../../shared/role_mapping/constants';

import { AppLogic } from '../../app_logic';
import { ENGINES_PATH, SETTINGS_PATH, CREDENTIALS_PATH, ROLE_MAPPINGS_PATH } from '../../routes';
import { CREDENTIALS_TITLE } from '../credentials';
import { ENGINES_TITLE } from '../engines';
import { SETTINGS_TITLE } from '../settings';

export const useAppSearchNav = () => {
const {
myRole: { canViewSettings, canViewAccountCredentials, canViewRoleMappings },
} = useValues(AppLogic);

const navItems: Array<EuiSideNavItemType<unknown>> = [
{
id: 'engines',
name: ENGINES_TITLE,
...generateNavLink({ to: ENGINES_PATH, isRoot: true }),
items: [], // TODO: Engine nav
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the plan for this TODO?

Copy link
Member Author

Choose a reason for hiding this comment

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

Upcoming PR!

},
];

if (canViewSettings) {
navItems.push({
id: 'settings',
name: SETTINGS_TITLE,
...generateNavLink({ to: SETTINGS_PATH }),
});
}

if (canViewAccountCredentials) {
navItems.push({
id: 'credentials',
name: CREDENTIALS_TITLE,
...generateNavLink({ to: CREDENTIALS_PATH }),
});
}

if (canViewRoleMappings) {
navItems.push({
id: 'usersRoles',
name: ROLE_MAPPINGS_TITLE,
...generateNavLink({ to: ROLE_MAPPINGS_PATH }),
});
}

return navItems;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

jest.mock('./nav', () => ({
useAppSearchNav: () => [],
}));

import React from 'react';

import { shallow } from 'enzyme';

import { SetAppSearchChrome } from '../../../shared/kibana_chrome';
import { EnterpriseSearchPageTemplate } from '../../../shared/layout';
import { SendAppSearchTelemetry } from '../../../shared/telemetry';

import { AppSearchPageTemplate } from './page_template';

describe('AppSearchPageTemplate', () => {
it('renders', () => {
const wrapper = shallow(
<AppSearchPageTemplate>
<div className="hello">world</div>
</AppSearchPageTemplate>
);

expect(wrapper.type()).toEqual(EnterpriseSearchPageTemplate);
expect(wrapper.prop('solutionNav')).toEqual({ name: 'App Search', items: [] });
expect(wrapper.find('.hello').text()).toEqual('world');
});

describe('page chrome', () => {
it('takes a breadcrumb array & renders a product-specific page chrome', () => {
const wrapper = shallow(<AppSearchPageTemplate pageChrome={['Some page']} />);
const setPageChrome = wrapper.find(EnterpriseSearchPageTemplate).prop('setPageChrome') as any;

expect(setPageChrome.type).toEqual(SetAppSearchChrome);
expect(setPageChrome.props.trail).toEqual(['Some page']);
});
});

describe('page telemetry', () => {
it('takes a metric & renders product-specific telemetry viewed event', () => {
const wrapper = shallow(<AppSearchPageTemplate pageViewTelemetry="some_page" />);

expect(wrapper.find(SendAppSearchTelemetry).prop('action')).toEqual('viewed');
expect(wrapper.find(SendAppSearchTelemetry).prop('metric')).toEqual('some_page');
});
});

it('passes down any ...pageTemplateProps that EnterpriseSearchPageTemplate accepts', () => {
const wrapper = shallow(
<AppSearchPageTemplate
pageHeader={{ pageTitle: 'hello world' }}
isLoading={false}
emptyState={<div />}
/>
);

expect(wrapper.find(EnterpriseSearchPageTemplate).prop('pageHeader')!.pageTitle).toEqual(
'hello world'
);
expect(wrapper.find(EnterpriseSearchPageTemplate).prop('isLoading')).toEqual(false);
expect(wrapper.find(EnterpriseSearchPageTemplate).prop('emptyState')).toEqual(<div />);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
Copy link
Contributor

@byronhulcher byronhulcher Jun 15, 2021

Choose a reason for hiding this comment

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

Should the filename of be closer to the exported component's name?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't feel super strongly either way, since this is namespaced (folder-wise... folder-spaced?) to app_search/ I don't think it's too confusing, but I know there's IDE implications to file-finding or showing tab names 🤷

FWIW I was kinda copying Workplace Search's folder architecture pattern here (they already have a layout/nav.tsx and layout/kibana_header_actions.tsx which we also now have our own version of), so I thought layout/page_template.tsx made sense as well 🤔

Definitely doesn't bother me either way though, so happy to revisit later if it's annoying down the road or if we want to circle back and do a vote

* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';

import { APP_SEARCH_PLUGIN } from '../../../../../common/constants';
import { SetAppSearchChrome } from '../../../shared/kibana_chrome';
import { EnterpriseSearchPageTemplate, PageTemplateProps } from '../../../shared/layout';
import { SendAppSearchTelemetry } from '../../../shared/telemetry';

import { useAppSearchNav } from './nav';

export const AppSearchPageTemplate: React.FC<PageTemplateProps> = ({
children,
pageChrome,
pageViewTelemetry,
...pageTemplateProps
}) => {
return (
<EnterpriseSearchPageTemplate
{...pageTemplateProps}
solutionNav={{
name: APP_SEARCH_PLUGIN.NAME,
items: useAppSearchNav(),
}}
setPageChrome={pageChrome && <SetAppSearchChrome trail={pageChrome} />}
>
{pageViewTelemetry && <SendAppSearchTelemetry action="viewed" metric={pageViewTelemetry} />}
{children}
</EnterpriseSearchPageTemplate>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import React from 'react';

import { shallow } from 'enzyme';

import { Loading } from '../../../shared/loading';
import { RoleMappingsTable, RoleMappingsHeading } from '../../../shared/role_mapping';
import { wsRoleMapping } from '../../../shared/role_mapping/__mocks__/roles';

Expand Down Expand Up @@ -44,13 +43,6 @@ describe('RoleMappings', () => {
expect(wrapper.find(RoleMappingsTable)).toHaveLength(1);
});

it('returns Loading when loading', () => {
setMockValues({ ...mockValues, dataLoading: true });
const wrapper = shallow(<RoleMappings />);

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

it('renders RoleMapping flyout', () => {
setMockValues({ ...mockValues, roleMappingFlyoutOpen: true });
const wrapper = shallow(<RoleMappings />);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@ import React, { useEffect } from 'react';

import { useActions, useValues } from 'kea';

import { FlashMessages } from '../../../shared/flash_messages';
import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
import { Loading } from '../../../shared/loading';
import { APP_SEARCH_PLUGIN } from '../../../../../common/constants';
import { RoleMappingsTable, RoleMappingsHeading } from '../../../shared/role_mapping';
import { ROLE_MAPPINGS_TITLE } from '../../../shared/role_mapping/constants';
import { AppSearchPageTemplate } from '../layout';

import { ROLE_MAPPINGS_ENGINE_ACCESS_HEADING } from './constants';
import { RoleMapping } from './role_mapping';
Expand All @@ -38,11 +37,12 @@ export const RoleMappings: React.FC = () => {
return resetState;
}, []);

if (dataLoading) return <Loading />;

const roleMappingsSection = (
<>
<RoleMappingsHeading productName="App Search" onClick={() => initializeRoleMapping()} />
<section>
<RoleMappingsHeading
productName={APP_SEARCH_PLUGIN.NAME}
onClick={() => initializeRoleMapping()}
/>
<RoleMappingsTable
roleMappings={roleMappings}
accessItemKey="engines"
Expand All @@ -51,15 +51,17 @@ export const RoleMappings: React.FC = () => {
shouldShowAuthProvider={multipleAuthProvidersConfig}
handleDeleteMapping={handleDeleteMapping}
/>
</>
</section>
);

return (
<>
<SetPageChrome trail={[ROLE_MAPPINGS_TITLE]} />
<AppSearchPageTemplate
pageChrome={[ROLE_MAPPINGS_TITLE]}
pageHeader={{ pageTitle: ROLE_MAPPINGS_TITLE }}
isLoading={dataLoading}
>
{roleMappingFlyoutOpen && <RoleMapping />}
<FlashMessages />
{roleMappingsSection}
</>
</AppSearchPageTemplate>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { EngineNav, EngineRouter } from './components/engine';
import { EngineCreation } from './components/engine_creation';
import { EnginesOverview, ENGINES_TITLE } from './components/engines';
import { ErrorConnecting } from './components/error_connecting';
import { KibanaHeaderActions } from './components/layout/kibana_header_actions';
import { KibanaHeaderActions } from './components/layout';
import { Library } from './components/library';
import { MetaEngineCreation } from './components/meta_engine_creation';
import { RoleMappings } from './components/role_mappings';
Expand Down Expand Up @@ -92,6 +92,11 @@ export const AppSearchConfigured: React.FC<Required<InitialAppData>> = (props) =
<Library />
</Route>
)}
{canViewRoleMappings && (
<Route path={ROLE_MAPPINGS_PATH}>
<RoleMappings />
</Route>
)}
<Route>
<Layout navigation={<AppSearchNav />} readOnlyMode={readOnlyMode}>
<Switch>
Expand All @@ -110,11 +115,6 @@ export const AppSearchConfigured: React.FC<Required<InitialAppData>> = (props) =
<Route exact path={CREDENTIALS_PATH}>
<Credentials />
</Route>
{canViewRoleMappings && (
<Route path={ROLE_MAPPINGS_PATH}>
<RoleMappings />
</Route>
)}
{canManageEngines && (
<Route exact path={ENGINE_CREATION_PATH}>
<EngineCreation />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,9 @@
* 2.0.
*/

export { EnterpriseSearchPageTemplate, PageTemplateProps } from './page_template';
export { generateNavLink } from './nav_link_helpers';

// TODO: Delete these once KibanaPageTemplate migration is done
export { Layout } from './layout';
export { SideNav, SideNavLink, SideNavItem } from './side_nav';
Loading