Skip to content

Commit

Permalink
[Enterprise Search] Set up initial KibanaPageTemplate (#102170) (#102431
Browse files Browse the repository at this point in the history
)

* Set up shared EnterpriseSearchPageTemplate component

* Set up product-specific page templates + setPageChrome

+ misc tech debt - create AS components/layout/index.ts for imports

* Set up navigation helpers for EuiSideNav usage

- Update react_router_helpers to pass back props as a plain JS obj instead of only working with React components (+ update react components to use new simpler helper)

- Convert SideNavLink active logic to a plain JS helper

* Set up top-level product navigations

NYI: sub navigations (future separate PRs)

* Set up test_helpers for inspecting pageHeaders

- primarily useful for rightSideItems, which often contain conditional logic

* Initial example: Convert RoleMappings views to new page template

Minor refactors:
+ remove unnecessary type union
+ fix un-i18n'ed product names
+ add full stop to documentation sentence
+ add semantic HTML tags around various page landmarks (header, section)

* EUI feedback: add empty root parent section

* Revert Role Mappings union type removal

- but shenanigans it a bit to take our i18n'd shared product names (requires as const assertion)

- done to reduce merge conflicts for Scotty / make his life (hopefully) a bit easier between ent-search and Kibana

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Constance <constancecchen@users.noreply.github.com>
  • Loading branch information
kibanamachine and Constance committed Jun 17, 2021
1 parent 2ad78b2 commit b3c9c18
Show file tree
Hide file tree
Showing 33 changed files with 1,181 additions and 190 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 @@ -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,107 @@
/*
* 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', () => {
it('always generates a default engines nav item', () => {
setMockValues({ myRole: {} });

expect(useAppSearchNav()).toEqual([
{
id: '',
name: '',
items: [
{
id: 'engines',
name: 'Engines',
href: '/engines',
items: [],
},
],
},
]);
});

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

expect(useAppSearchNav()).toEqual([
{
id: '',
name: '',
items: [
{
id: 'engines',
name: 'Engines',
href: '/engines',
items: [],
},
{
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([
{
id: '',
name: '',
items: [
{
id: 'engines',
name: 'Engines',
href: '/engines',
items: [],
},
{
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([
{
id: '',
name: '',
items: [
{
id: 'engines',
name: 'Engines',
href: '/engines',
items: [],
},
{
id: 'usersRoles',
name: 'Users & roles',
href: '/role_mappings',
},
],
},
]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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
},
];

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

// Root level items are meant to be section headers, but the AS nav (currently)
// isn't organized this way. So we create a fake empty parent item here
// to cause all our navItems to properly render as nav links.
return [{ id: '', name: '', items: 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 @@
/*
* 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>
);
};
Loading

0 comments on commit b3c9c18

Please sign in to comment.