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 index b110d32442c2c4..858dac864b58aa 100644 --- 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 @@ -12,7 +12,7 @@ import { useNavigateToAppEventHandler } from '../hooks/use_navigate_to_app_event 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) */ + /** Any app specific path (route) */ appPath?: string; appState?: any; onClick?: MouseEventHandler; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/mocks/app_context_render.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/mocks/app_context_render.tsx index af34205e2310f3..7cb1031ef9a09a 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/mocks/app_context_render.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/mocks/app_context_render.tsx @@ -18,7 +18,7 @@ type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResul /** * Mocked app root context renderer */ -interface AppContextTestRender { +export interface AppContextTestRender { store: ReturnType; history: ReturnType; coreStart: ReturnType; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/mock_host_result_list.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/mock_host_result_list.ts index d4c2602e34387e..20aa973ffc93de 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/mock_host_result_list.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/mock_host_result_list.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostResultList, HostStatus } from '../../../../../common/types'; +import { HostInfo, HostResultList, HostStatus } from '../../../../../common/types'; import { EndpointDocGenerator } from '../../../../../common/generate_data'; export const mockHostResultList: (options?: { @@ -40,3 +40,14 @@ export const mockHostResultList: (options?: { }; return mock; }; + +/** + * returns a mocked API response for retrieving a single host metadata + */ +export const mockHostDetailsApiResult = (): HostInfo => { + const generator = new EndpointDocGenerator('seed'); + return { + metadata: generator.generateHostMetadata(), + host_status: HostStatus.ERROR, + }; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details.tsx index 37080e8568350e..90829f7ad4cbe3 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details.tsx @@ -28,6 +28,7 @@ import { useHostListSelector } from './hooks'; import { urlFromQueryParams } from './url_from_query_params'; import { FormattedDateAndTime } from '../formatted_date_time'; import { uiQueryParams, detailsData, detailsError } from './../../store/hosts/selectors'; +import { LinkToApp } from '../../components/link_to_app'; const HostIds = styled(EuiListGroupItem)` margin-top: 0; @@ -37,6 +38,7 @@ const HostIds = styled(EuiListGroupItem)` `; const HostDetails = memo(({ details }: { details: HostMetadata }) => { + const { appId, appPath, url } = useHostLogsUrl(details.host.id); const detailsResultsUpper = useMemo(() => { return [ { @@ -113,6 +115,20 @@ const HostDetails = memo(({ details }: { details: HostMetadata }) => { listItems={detailsResultsLower} data-test-subj="hostDetailsLowerList" /> + +

+ + + +

); }); @@ -170,3 +186,15 @@ export const HostDetailsFlyout = () => { ); }; + +const useHostLogsUrl = (hostId: string): { url: string; appId: string; appPath: string } => { + const { services } = useKibana(); + return useMemo(() => { + const appPath = `/stream?logFilter=(expression:'host.id:${hostId}',kind:kuery)`; + return { + url: `${services.application.getUrlForApp('logs')}${appPath}`, + appId: 'logs', + appPath, + }; + }, [hostId, services.application]); +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx index f6dfae99c1b11b..c3ff41268e3db1 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx @@ -6,40 +6,26 @@ import React from 'react'; import * as reactTestingLibrary from '@testing-library/react'; -import { Provider } from 'react-redux'; -import { I18nProvider } from '@kbn/i18n/react'; -import { EuiThemeProvider } from '../../../../../../../legacy/common/eui_styled_components'; -import { appStoreFactory } from '../../store'; -import { RouteCapture } from '../route_capture'; -import { createMemoryHistory, MemoryHistory } from 'history'; -import { Router } from 'react-router-dom'; +import { fireEvent } from '@testing-library/react'; import { AppAction } from '../../types'; import { HostList } from './index'; -import { mockHostResultList } from '../../store/hosts/mock_host_result_list'; +import { + mockHostDetailsApiResult, + mockHostResultList, +} from '../../store/hosts/mock_host_result_list'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../mocks'; +import { HostInfo } from '../../../../../common/types'; describe('when on the hosts page', () => { - let render: () => reactTestingLibrary.RenderResult; - let history: MemoryHistory; - let store: ReturnType; + let render: () => ReturnType; + let history: AppContextTestRender['history']; + let store: AppContextTestRender['store']; + let coreStart: AppContextTestRender['coreStart']; beforeEach(async () => { - history = createMemoryHistory(); - store = appStoreFactory(); - render = () => { - return reactTestingLibrary.render( - - - - - - - - - - - - ); - }; + const mockedContext = createAppRootMockRenderer(); + ({ history, store, coreStart } = mockedContext); + render = () => mockedContext.render(); }); it('should show a table', async () => { @@ -56,7 +42,7 @@ describe('when on the hosts page', () => { expect(e).not.toBeNull(); }); }); - describe('when data loads', () => { + describe('when list data loads', () => { beforeEach(() => { reactTestingLibrary.act(() => { const action: AppAction = { @@ -76,6 +62,16 @@ describe('when on the hosts page', () => { describe('when the user clicks the hostname in the table', () => { let renderResult: reactTestingLibrary.RenderResult; beforeEach(async () => { + const hostDetailsApiResponse = mockHostDetailsApiResult(); + + coreStart.http.get.mockReturnValue(Promise.resolve(hostDetailsApiResponse)); + reactTestingLibrary.act(() => { + store.dispatch({ + type: 'serverReturnedHostDetails', + payload: hostDetailsApiResponse, + }); + }); + renderResult = render(); const detailsLink = await renderResult.findByTestId('hostnameCellLink'); if (detailsLink) { @@ -93,19 +89,71 @@ describe('when on the hosts page', () => { }); describe('when there is a selected host in the url', () => { + let hostDetails: HostInfo; beforeEach(() => { + const { + host_status, + metadata: { host, ...details }, + } = mockHostDetailsApiResult(); + hostDetails = { + host_status, + metadata: { + ...details, + host: { + ...host, + id: '1', + }, + }, + }; + + coreStart.http.get.mockReturnValue(Promise.resolve(hostDetails)); + coreStart.application.getUrlForApp.mockReturnValue('/app/logs'); + reactTestingLibrary.act(() => { history.push({ ...history.location, search: '?selected_host=1', }); }); + reactTestingLibrary.act(() => { + store.dispatch({ + type: 'serverReturnedHostDetails', + payload: hostDetails, + }); + }); }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('should show the flyout', () => { const renderResult = render(); return renderResult.findByTestId('hostDetailsFlyout').then(flyout => { expect(flyout).not.toBeNull(); }); }); + it('should include the link to logs', async () => { + const renderResult = render(); + const linkToLogs = await renderResult.findByTestId('hostDetailsLinkToLogs'); + expect(linkToLogs).not.toBeNull(); + expect(linkToLogs.textContent).toEqual('Endpoint Logs'); + expect(linkToLogs.getAttribute('href')).toEqual( + "/app/logs/stream?logFilter=(expression:'host.id:1',kind:kuery)" + ); + }); + describe('when link to logs is clicked', () => { + beforeEach(async () => { + const renderResult = render(); + const linkToLogs = await renderResult.findByTestId('hostDetailsLinkToLogs'); + reactTestingLibrary.act(() => { + fireEvent.click(linkToLogs); + }); + }); + + it('should navigate to logs without full page refresh', async () => { + // FIXME: this is not working :( + expect(coreStart.application.navigateToApp.mock.calls).toHaveLength(1); + }); + }); }); });