Skip to content
Merged
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
Copy link
Copy Markdown
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
Copy Markdown
Contributor 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 }),
});
}

// 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 @@
/*
Copy link
Copy Markdown
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
Copy Markdown
Contributor 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>
);
};
Loading