From 600d85c5321443ca988e235e50ec8fbdfe0a5f08 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Thu, 26 Mar 2020 20:54:22 -0400 Subject: [PATCH] [Endpoint] Common PageView and LinkToApp components (#60819) (#61555) * a common PageView component * Policy List cleanup + improvements to PageView * Policy details refactored to use PageView layout * Fix bug: header nav - ensure section sub-routes set nav to `isSelected` * Added useNavigateToAppEventHandler hook * Remove use of `appBasePath` and use `history` for initializing router - For details - see https://github.com/elastic/kibana/pull/56705 * Removed `hello-world` API * Added `LinkToApp` component + refactor policy list to use it * Add icon to plugin registration --- .../__snapshots__/link_to_app.test.tsx.snap | 40 ++ .../__snapshots__/page_view.test.tsx.snap | 646 ++++++++++++++++++ .../endpoint/components/header_nav.tsx | 56 +- .../endpoint/components/link_to_app.test.tsx | 144 ++++ .../endpoint/components/link_to_app.tsx | 35 + .../endpoint/components/page_view.test.tsx | 63 ++ .../endpoint/components/page_view.tsx | 96 +++ .../use_navigate_to_app_event_handler.ts | 75 ++ .../public/applications/endpoint/index.tsx | 19 +- .../endpoint/view/policy/policy_details.tsx | 90 +-- .../endpoint/view/policy/policy_list.tsx | 92 +-- x-pack/plugins/endpoint/public/plugin.ts | 1 + x-pack/plugins/endpoint/server/plugin.ts | 2 - .../plugins/endpoint/server/routes/index.ts | 26 - .../feature_controls/endpoint_spaces.ts | 4 +- .../functional/apps/endpoint/header_nav.ts | 2 +- 16 files changed, 1209 insertions(+), 182 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/components/__snapshots__/link_to_app.test.tsx.snap create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/components/__snapshots__/page_view.test.tsx.snap create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/components/link_to_app.test.tsx create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/components/link_to_app.tsx create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/components/page_view.test.tsx create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/components/page_view.tsx create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/hooks/use_navigate_to_app_event_handler.ts delete mode 100644 x-pack/plugins/endpoint/server/routes/index.ts diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/components/__snapshots__/link_to_app.test.tsx.snap b/x-pack/plugins/endpoint/public/applications/endpoint/components/__snapshots__/link_to_app.test.tsx.snap new file mode 100644 index 0000000000000..6838b673b90d8 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/components/__snapshots__/link_to_app.test.tsx.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LinkToApp component should render with href 1`] = ` + + + + link + + + +`; + +exports[`LinkToApp component should render with minimum input 1`] = ` + + + + + +`; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/components/__snapshots__/page_view.test.tsx.snap b/x-pack/plugins/endpoint/public/applications/endpoint/components/__snapshots__/page_view.test.tsx.snap new file mode 100644 index 0000000000000..34420e653049c --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/components/__snapshots__/page_view.test.tsx.snap @@ -0,0 +1,646 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PageView component should display body header custom element 1`] = ` +.c0 { + padding: 0; +} + +.c0 .endpoint-header { + padding: 24px; +} + +.c0 .endpoint-page-content { + border-left: none; + border-right: none; +} + + + body header +

+ } +> + + +
+ +
+ + +
+ +
+ +
+

+ body header +

+
+
+
+
+ +
+ body content +
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`PageView component should display body header wrapped in EuiTitle 1`] = ` +.c0 { + padding: 0; +} + +.c0 .endpoint-header { + padding: 24px; +} + +.c0 .endpoint-page-content { + border-left: none; + border-right: none; +} + + + + +
+ +
+ + +
+ +
+ +
+ +

+ body header +

+
+
+
+
+
+ +
+ body content +
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`PageView component should display header left and right 1`] = ` +.c0 { + padding: 0; +} + +.c0 .endpoint-header { + padding: 24px; +} + +.c0 .endpoint-page-content { + border-left: none; + border-right: none; +} + + + + +
+ +
+ +
+ +
+ +

+ page title +

+
+
+
+ +
+ right side actions +
+
+
+
+ + +
+ +
+ body content +
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`PageView component should display only body if not header props used 1`] = ` +.c0 { + padding: 0; +} + +.c0 .endpoint-header { + padding: 24px; +} + +.c0 .endpoint-page-content { + border-left: none; + border-right: none; +} + + + + +
+ +
+ + +
+ +
+ body content +
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`PageView component should display only header left 1`] = ` +.c0 { + padding: 0; +} + +.c0 .endpoint-header { + padding: 24px; +} + +.c0 .endpoint-page-content { + border-left: none; + border-right: none; +} + + + + +
+ +
+ +
+ +
+ +

+ page title +

+
+
+
+
+
+ + +
+ +
+ body content +
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`PageView component should display only header right but include an empty left side 1`] = ` +.c0 { + padding: 0; +} + +.c0 .endpoint-header { + padding: 24px; +} + +.c0 .endpoint-page-content { + border-left: none; + border-right: none; +} + + + + +
+ +
+ +
+ +
+ + +
+ right side actions +
+
+
+ + + +
+ +
+ body content +
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`PageView component should pass through EuiPage props 1`] = ` +.c0 { + padding: 0; +} + +.c0 .endpoint-header { + padding: 24px; +} + +.c0 .endpoint-page-content { + border-left: none; + border-right: none; +} + + + + +
+ +
+ + +
+ +
+ body content +
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`PageView component should use custom element for header left and not wrap in EuiTitle 1`] = ` +.c0 { + padding: 0; +} + +.c0 .endpoint-header { + padding: 24px; +} + +.c0 .endpoint-page-content { + border-left: none; + border-right: none; +} + + + title here +

+ } +> + + +
+ +
+ +
+ +
+

+ title here +

+
+
+
+
+ + +
+ +
+ body content +
+
+
+
+
+
+
+
+
+
+
+`; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/components/header_nav.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/components/header_nav.tsx index 1bafcbec93f5f..3fb06d6b4a56e 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/components/header_nav.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/components/header_nav.tsx @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { MouseEvent } from 'react'; +import React, { MouseEvent, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiTabs, EuiTab } from '@elastic/eui'; import { useHistory, useLocation } from 'react-router-dom'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; export interface NavTabs { name: string; @@ -46,30 +47,33 @@ export const navTabs: NavTabs[] = [ }, ]; -export const HeaderNavigation: React.FunctionComponent<{ basename: string }> = React.memo( - ({ basename }) => { - const history = useHistory(); - const location = useLocation(); +export const HeaderNavigation: React.FunctionComponent = React.memo(() => { + const history = useHistory(); + const location = useLocation(); + const { services } = useKibana(); + const BASE_PATH = services.application.getUrlForApp('endpoint'); - function renderNavTabs(tabs: NavTabs[]) { - return tabs.map((tab, index) => { - return ( - { - event.preventDefault(); - history.push(tab.href); - }} - isSelected={tab.href === location.pathname} - > - {tab.name} - - ); - }); - } + const tabList = useMemo(() => { + return navTabs.map((tab, index) => { + return ( + { + event.preventDefault(); + history.push(tab.href); + }} + isSelected={ + tab.href === location.pathname || + (tab.href !== '/' && location.pathname.startsWith(tab.href)) + } + > + {tab.name} + + ); + }); + }, [BASE_PATH, history, location.pathname]); - return {renderNavTabs(navTabs)}; - } -); + return {tabList}; +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/components/link_to_app.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/components/link_to_app.test.tsx new file mode 100644 index 0000000000000..902c215434aac --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/components/link_to_app.test.tsx @@ -0,0 +1,144 @@ +/* + * 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 React from 'react'; +import { mount } from 'enzyme'; +import { LinkToApp } from './link_to_app'; +import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { CoreStart } from 'kibana/public'; + +describe('LinkToApp component', () => { + let fakeCoreStart: jest.Mocked; + const render = (ui: Parameters[0]) => + mount(ui, { + wrappingComponent: KibanaContextProvider, + wrappingComponentProps: { + services: { application: fakeCoreStart.application }, + }, + }); + + beforeEach(() => { + fakeCoreStart = coreMock.createStart(); + }); + + it('should render with minimum input', () => { + expect(render(link)).toMatchSnapshot(); + }); + it('should render with href', () => { + expect( + render( + + link + + ) + ).toMatchSnapshot(); + }); + it('should support onClick prop', () => { + const spyOnClickHandler = jest.fn(); + const renderResult = render( + + link + + ); + + renderResult.find('EuiLink').simulate('click', { button: 0 }); + const clickEventArg = spyOnClickHandler.mock.calls[0][0]; + + expect(spyOnClickHandler).toHaveBeenCalled(); + expect(clickEventArg.preventDefault).toBeInstanceOf(Function); + expect(clickEventArg.isDefaultPrevented()).toBe(true); + expect(fakeCoreStart.application.navigateToApp).toHaveBeenCalledWith('ingestManager', { + path: undefined, + state: undefined, + }); + }); + it('should navigate to App with specific path', () => { + const renderResult = render( + + link + + ); + renderResult.find('EuiLink').simulate('click', { button: 0 }); + expect(fakeCoreStart.application.navigateToApp).toHaveBeenCalledWith('ingestManager', { + path: '/some/path', + state: undefined, + }); + }); + it('should passes through EuiLinkProps', () => { + const renderResult = render( + + link + + ); + expect(renderResult.find('EuiLink').props()).toEqual({ + children: 'link', + className: 'my-class', + color: 'primary', + 'data-test-subj': 'my-test-subject', + href: '/app/ingest', + onClick: expect.any(Function), + }); + }); + it('should still preventDefault if onClick callback throws', () => { + const spyOnClickHandler = jest.fn(ev => { + throw new Error('test'); + }); + const renderResult = render( + + link + + ); + expect(() => renderResult.find('EuiLink').simulate('click')).toThrow(); + const clickEventArg = spyOnClickHandler.mock.calls[0][0]; + expect(clickEventArg.isDefaultPrevented()).toBe(true); + }); + it('should not navigate if onClick callback prevents defalut', () => { + const spyOnClickHandler = jest.fn(ev => { + ev.preventDefault(); + }); + const renderResult = render( + + link + + ); + renderResult.find('EuiLink').simulate('click', { button: 0 }); + expect(fakeCoreStart.application.navigateToApp).not.toHaveBeenCalled(); + }); + it('should not to navigate if it was not left click', () => { + const renderResult = render(link); + renderResult.find('EuiLink').simulate('click', { button: 1 }); + expect(fakeCoreStart.application.navigateToApp).not.toHaveBeenCalled(); + }); + it('should not to navigate if it includes an anchor target', () => { + const renderResult = render( + + link + + ); + renderResult.find('EuiLink').simulate('click', { button: 0 }); + expect(fakeCoreStart.application.navigateToApp).not.toHaveBeenCalled(); + }); + it('should not to navigate if if meta|alt|ctrl|shift keys are pressed', () => { + const renderResult = render( + + link + + ); + const euiLink = renderResult.find('EuiLink'); + ['meta', 'alt', 'ctrl', 'shift'].forEach(key => { + euiLink.simulate('click', { button: 0, [`${key}Key`]: true }); + expect(fakeCoreStart.application.navigateToApp).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/components/link_to_app.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/components/link_to_app.tsx new file mode 100644 index 0000000000000..b110d32442c2c --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/components/link_to_app.tsx @@ -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 React, { memo, MouseEventHandler } from 'react'; +import { EuiLink } from '@elastic/eui'; +import { EuiLinkProps } from '@elastic/eui'; +import { useNavigateToAppEventHandler } from '../hooks/use_navigate_to_app_event_handler'; + +export type LinkToAppProps = EuiLinkProps & { + /** the app id - normally the value of the `id` in that plugin's `kibana.json` */ + appId: string; + /** Any app specic path (route) */ + appPath?: string; + appState?: any; + onClick?: MouseEventHandler; +}; + +/** + * An `EuiLink` that will use Kibana's `.application.navigateToApp()` to redirect the user to the + * a given app without causing a full browser refresh + */ +export const LinkToApp = memo( + ({ appId, appPath: path, appState: state, onClick, children, ...otherProps }) => { + const handleOnClick = useNavigateToAppEventHandler(appId, { path, state, onClick }); + return ( + // eslint-disable-next-line @elastic/eui/href-or-on-click + + {children} + + ); + } +); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/components/page_view.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/components/page_view.test.tsx new file mode 100644 index 0000000000000..867c9101fe79b --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/components/page_view.test.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { PageView } from './page_view'; +import { EuiThemeProvider } from '../../../../../../legacy/common/eui_styled_components'; + +describe('PageView component', () => { + const render = (ui: Parameters[0]) => + mount(ui, { wrappingComponent: EuiThemeProvider }); + + it('should display only body if not header props used', () => { + expect(render(body content)).toMatchSnapshot(); + }); + it('should display header left and right', () => { + expect( + render( + + body content + + ) + ).toMatchSnapshot(); + }); + it('should display only header left', () => { + expect(render(body content)).toMatchSnapshot(); + }); + it('should display only header right but include an empty left side', () => { + expect( + render(body content) + ).toMatchSnapshot(); + }); + it(`should use custom element for header left and not wrap in EuiTitle`, () => { + expect( + render(title here

}>body content
) + ).toMatchSnapshot(); + }); + it('should display body header wrapped in EuiTitle', () => { + expect(render(body content)).toMatchSnapshot(); + }); + it('should display body header custom element', () => { + expect( + render(body header

}>body content
) + ).toMatchSnapshot(); + }); + it('should pass through EuiPage props', () => { + expect( + render( + + body content + + ) + ).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/components/page_view.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/components/page_view.tsx new file mode 100644 index 0000000000000..04401f06d344a --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/components/page_view.tsx @@ -0,0 +1,96 @@ +/* + * 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 { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiPageHeader, + EuiPageHeaderSection, + EuiPageProps, + EuiTitle, +} from '@elastic/eui'; +import React, { memo, ReactNode } from 'react'; +import styled from 'styled-components'; + +const StyledEuiPage = styled(EuiPage)` + padding: 0; + + .endpoint-header { + padding: ${props => props.theme.eui.euiSizeL}; + } + .endpoint-page-content { + border-left: none; + border-right: none; + } +`; + +const isStringOrNumber = /(string|number)/; + +/** + * Page View layout for use in Endpoint + */ +export const PageView = memo< + EuiPageProps & { + /** + * content to be placed on the left side of the header. If a `string` is used, then it will + * be wrapped with `

`, else it will just be used as is. + */ + headerLeft?: ReactNode; + /** Content for the right side of the header */ + headerRight?: ReactNode; + /** + * body (sub-)header section. If a `string` is used, then it will be wrapped with + * `

` + */ + bodyHeader?: ReactNode; + children?: ReactNode; + } +>(({ children, headerLeft, headerRight, bodyHeader, ...otherProps }) => { + return ( + + + {(headerLeft || headerRight) && ( + + + {isStringOrNumber.test(typeof headerLeft) ? ( + +

{headerLeft}

+
+ ) : ( + headerLeft + )} +
+ {headerRight && ( + + {headerRight} + + )} +
+ )} + + {bodyHeader && ( + + + {isStringOrNumber.test(typeof bodyHeader) ? ( + +

{bodyHeader}

+
+ ) : ( + bodyHeader + )} +
+
+ )} + {children} +
+
+
+ ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/hooks/use_navigate_to_app_event_handler.ts b/x-pack/plugins/endpoint/public/applications/endpoint/hooks/use_navigate_to_app_event_handler.ts new file mode 100644 index 0000000000000..5fbfa5e0e58a8 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/hooks/use_navigate_to_app_event_handler.ts @@ -0,0 +1,75 @@ +/* + * 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 { MouseEventHandler, useCallback } from 'react'; +import { ApplicationStart } from 'kibana/public'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; + +type NavigateToAppHandlerProps = Parameters; +type EventHandlerCallback = MouseEventHandler; + +/** + * Provides an event handlers that can be used with (for example) `onClick` to prevent the + * event's default behaviour and instead use Kibana's `navigateToApp()` to send user to a + * different app. Use of `navigateToApp()` prevents a full browser refresh for apps that have + * been converted to the New Platform. + * + * @param appId + * @param [options] + * + * @example + * + * const handleOnClick = useNavigateToAppEventHandler('ingestManager', {path: '#/configs'}) + * return See configs + */ +export const useNavigateToAppEventHandler = ( + /** the app id - normally the value of the `id` in that plugin's `kibana.json` */ + appId: NavigateToAppHandlerProps[0], + + /** Options, some of which are passed along to the app route */ + options?: NavigateToAppHandlerProps[1] & { + onClick?: EventHandlerCallback; + } +): EventHandlerCallback => { + const { services } = useKibana(); + const { path, state, onClick } = options || {}; + return useCallback( + ev => { + try { + if (onClick) { + onClick(ev); + } + } catch (error) { + ev.preventDefault(); + throw error; + } + + if (ev.defaultPrevented) { + return; + } + + if (ev.button !== 0) { + return; + } + + if ( + ev.currentTarget instanceof HTMLAnchorElement && + ev.currentTarget.target !== '' && + ev.currentTarget.target !== '_self' + ) { + return; + } + + if (ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) { + return; + } + + ev.preventDefault(); + services.application.navigateToApp(appId, { path, state }); + }, + [appId, onClick, path, services.application, state] + ); +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx index 884646369b4b1..fa9055e0d9bbd 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx @@ -6,9 +6,9 @@ import * as React from 'react'; import ReactDOM from 'react-dom'; -import { CoreStart, AppMountParameters } from 'kibana/public'; +import { CoreStart, AppMountParameters, ScopedHistory } from 'kibana/public'; import { I18nProvider, FormattedMessage } from '@kbn/i18n/react'; -import { Route, Switch, BrowserRouter } from 'react-router-dom'; +import { Route, Switch, Router } from 'react-router-dom'; import { Provider } from 'react-redux'; import { Store } from 'redux'; import { useObservable } from 'react-use'; @@ -29,12 +29,11 @@ import { EuiThemeProvider } from '../../../../../legacy/common/eui_styled_compon export function renderApp( coreStart: CoreStart, depsStart: EndpointPluginStartDependencies, - { appBasePath, element }: AppMountParameters + { element, history }: AppMountParameters ) { - coreStart.http.get('/api/endpoint/hello-world'); const store = appStoreFactory({ coreStart, depsStart }); ReactDOM.render( - , + , element ); return () => { @@ -43,7 +42,7 @@ export function renderApp( } interface RouterProps { - basename: string; + history: ScopedHistory; store: Store; coreStart: CoreStart; depsStart: EndpointPluginStartDependencies; @@ -51,7 +50,7 @@ interface RouterProps { const AppRoot: React.FunctionComponent = React.memo( ({ - basename, + history, store, coreStart: { http, notifications, uiSettings, application }, depsStart: { data }, @@ -63,9 +62,9 @@ const AppRoot: React.FunctionComponent = React.memo( - + - + = React.memo( /> - + diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx index 7317bd5e03ed9..a64b3293ec6cd 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx @@ -6,11 +6,6 @@ import React from 'react'; import { - EuiTitle, - EuiPage, - EuiPageBody, - EuiPageHeader, - EuiPageHeaderSection, EuiFlexGroup, EuiFlexItem, EuiButton, @@ -19,64 +14,49 @@ import { EuiSpacer, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { usePolicyDetailsSelector } from './policy_hooks'; import { policyDetails } from '../../store/policy_details/selectors'; import { WindowsEventing } from './policy_forms/eventing/windows'; +import { PageView } from '../../components/page_view'; export const PolicyDetails = React.memo(() => { const policyItem = usePolicyDetailsSelector(policyDetails); - function policyName() { - if (policyItem) { - return {policyItem.name}; - } else { - return ( - - - - ); - } - } + const headerLeftContent = + policyItem?.name ?? + i18n.translate('xpack.endpoint.policyDetails.notFound', { + defaultMessage: 'Policy Not Found', + }); + + const headerRightContent = ( + + + + + + + + + + + + + ); return ( - - - - - -

{policyName()}

-
-
- - - - - - - - - - - - -
- -

- -

-
- - -
-
+ + +

+ +

+
+ + +
); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx index f949efa46a2bd..7af302de8576e 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx @@ -4,20 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { SyntheticEvent, useCallback, useEffect, useMemo } from 'react'; -import { - EuiPage, - EuiPageBody, - EuiPageContent, - EuiPageContentBody, - EuiPageContentHeader, - EuiPageContentHeaderSection, - EuiTitle, - EuiBasicTable, - EuiText, - EuiTableFieldDataColumnType, - EuiLink, -} from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo } from 'react'; +import { EuiBasicTable, EuiText, EuiTableFieldDataColumnType, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useDispatch } from 'react-redux'; @@ -35,6 +23,8 @@ import { usePolicyListSelector } from './policy_hooks'; import { PolicyListAction } from '../../store/policy_list'; import { PolicyData } from '../../types'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { PageView } from '../../components/page_view'; +import { LinkToApp } from '../../components/link_to_app'; interface TableChangeCallbackArguments { page: { index: number; size: number }; @@ -145,18 +135,13 @@ export const PolicyList = React.memo(() => { }), render(version: string) { return ( - // eslint-disable-next-line @elastic/eui/href-or-on-click - { - ev.preventDefault(); - services.application.navigateToApp('ingestManager', { - path: `#/configs/${version}`, - }); - }} > {version} - + ); }, }, @@ -165,42 +150,29 @@ export const PolicyList = React.memo(() => { ); return ( - - - - - - -

- -

-
-

- - - -

-
-
- - - -
-
-
+ + + + } + > + + ); }); diff --git a/x-pack/plugins/endpoint/public/plugin.ts b/x-pack/plugins/endpoint/public/plugin.ts index 2759db26bb6c8..ee5bbe71ae8aa 100644 --- a/x-pack/plugins/endpoint/public/plugin.ts +++ b/x-pack/plugins/endpoint/public/plugin.ts @@ -47,6 +47,7 @@ export class EndpointPlugin title: i18n.translate('xpack.endpoint.pluginTitle', { defaultMessage: 'Endpoint', }), + euiIconType: 'securityApp', async mount(params: AppMountParameters) { const [coreStart, depsStart] = await core.getStartServices(); const { renderApp } = await import('./applications/endpoint'); diff --git a/x-pack/plugins/endpoint/server/plugin.ts b/x-pack/plugins/endpoint/server/plugin.ts index 4b4afd8088744..6d2e9e510551a 100644 --- a/x-pack/plugins/endpoint/server/plugin.ts +++ b/x-pack/plugins/endpoint/server/plugin.ts @@ -9,7 +9,6 @@ import { PluginSetupContract as FeaturesPluginSetupContract } from '../../featur import { createConfig$, EndpointConfigType } from './config'; import { EndpointAppContext } from './types'; -import { addRoutes } from './routes'; import { registerEndpointRoutes } from './routes/metadata'; import { registerAlertRoutes } from './routes/alerts'; import { registerResolverRoutes } from './routes/resolver'; @@ -71,7 +70,6 @@ export class EndpointPlugin }, } as EndpointAppContext; const router = core.http.createRouter(); - addRoutes(router); registerEndpointRoutes(router, endpointContext); registerResolverRoutes(router, endpointContext); registerAlertRoutes(router, endpointContext); diff --git a/x-pack/plugins/endpoint/server/routes/index.ts b/x-pack/plugins/endpoint/server/routes/index.ts deleted file mode 100644 index 8b0476ea7b229..0000000000000 --- a/x-pack/plugins/endpoint/server/routes/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 { IRouter } from 'kibana/server'; - -export function addRoutes(router: IRouter) { - router.get( - { - path: '/api/endpoint/hello-world', - validate: false, - options: { - tags: ['access:resolver'], - }, - }, - async function greetingIndex(_context, _request, response) { - return response.ok({ - body: { hello: 'world' }, - headers: { - 'Content-Type': 'application/json', - }, - }); - } - ); -} diff --git a/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts b/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts index bf3d642307d8c..c543046031e9f 100644 --- a/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts +++ b/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts @@ -31,7 +31,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { basePath: '/s/custom_space', }); const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.contain('EEndpoint'); + expect(navLinks).to.contain('Endpoint'); }); it(`endpoint app shows 'Hello World'`, async () => { @@ -69,7 +69,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { basePath: '/s/custom_space', }); const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).not.to.contain('EEndpoint'); + expect(navLinks).not.to.contain('Endpoint'); }); }); }); diff --git a/x-pack/test/functional/apps/endpoint/header_nav.ts b/x-pack/test/functional/apps/endpoint/header_nav.ts index d1fa7311d61e8..c2c4068212484 100644 --- a/x-pack/test/functional/apps/endpoint/header_nav.ts +++ b/x-pack/test/functional/apps/endpoint/header_nav.ts @@ -41,7 +41,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('renders the policy page when Policy tab is selected', async () => { await (await testSubjects.find('policiesEndpointTab')).click(); - await testSubjects.existOrFail('policyViewTitle'); + await testSubjects.existOrFail('policyListPage'); }); it('renders the home page when Home tab is selected after selecting another tab', async () => {