Skip to content

Commit

Permalink
[Endpoint] Add link to Logs UI to the Host Details view (#62852) (#63144
Browse files Browse the repository at this point in the history
)

* Add LinktoApp to host details for logs

* initial setup for testing link on details

* Export interface AppContextTestRender for reference in tests

* Refactor hosts tests to use AppContextTestRender

* Render full details and validate link to logs

* one more test to ensure we navigate to app (not full page refresh)

* Fixes post master merge
  • Loading branch information
paul-tavares authored Apr 10, 2020
1 parent e636d98 commit 712c4e0
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLButtonElement | HTMLAnchorElement>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResul
/**
* Mocked app root context renderer
*/
interface AppContextTestRender {
export interface AppContextTestRender {
store: ReturnType<typeof appStoreFactory>;
history: ReturnType<typeof createMemoryHistory>;
coreStart: ReturnType<typeof coreMock.createStart>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?: {
Expand Down Expand Up @@ -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,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 [
{
Expand Down Expand Up @@ -113,6 +115,20 @@ const HostDetails = memo(({ details }: { details: HostMetadata }) => {
listItems={detailsResultsLower}
data-test-subj="hostDetailsLowerList"
/>
<EuiHorizontalRule margin="s" />
<p>
<LinkToApp
appId={appId}
appPath={appPath}
href={url}
data-test-subj="hostDetailsLinkToLogs"
>
<FormattedMessage
id="xpack.endpoint.host.details.linkToLogsTitle"
defaultMessage="Endpoint Logs"
/>
</LinkToApp>
</p>
</>
);
});
Expand Down Expand Up @@ -170,3 +186,15 @@ export const HostDetailsFlyout = () => {
</EuiFlyout>
);
};

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]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,54 +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<never>;
let store: ReturnType<typeof appStoreFactory>;

let queryByTestSubjId: (
renderResult: reactTestingLibrary.RenderResult,
testSubjId: string
) => Promise<Element | null>;
let render: () => ReturnType<AppContextTestRender['render']>;
let history: AppContextTestRender['history'];
let store: AppContextTestRender['store'];
let coreStart: AppContextTestRender['coreStart'];

beforeEach(async () => {
history = createMemoryHistory<never>();
store = appStoreFactory();
render = () => {
return reactTestingLibrary.render(
<Provider store={store}>
<I18nProvider>
<EuiThemeProvider>
<Router history={history}>
<RouteCapture>
<HostList />
</RouteCapture>
</Router>
</EuiThemeProvider>
</I18nProvider>
</Provider>
);
};

queryByTestSubjId = async (renderResult, testSubjId) => {
return await reactTestingLibrary.waitForElement(
() => document.body.querySelector(`[data-test-subj="${testSubjId}"]`),
{
container: renderResult.container,
}
);
};
const mockedContext = createAppRootMockRenderer();
({ history, store, coreStart } = mockedContext);
render = () => mockedContext.render(<HostList />);
});

it('should show a table', async () => {
Expand All @@ -70,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 = {
Expand All @@ -90,8 +62,18 @@ 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 queryByTestSubjId(renderResult, 'hostnameCellLink');
const detailsLink = await renderResult.findByTestId('hostnameCellLink');
if (detailsLink) {
reactTestingLibrary.fireEvent.click(detailsLink);
}
Expand All @@ -107,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);
});
});
});
});

0 comments on commit 712c4e0

Please sign in to comment.