Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7.x] [Endpoint] Add link to Logs UI to the Host Details view (#62852) #63144

Merged
merged 1 commit into from
Apr 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});
});
});
});