diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/__mocks__/api_log.mock.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/__mocks__/api_log.mock.ts
new file mode 100644
index 000000000000..6106cb049c7a
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/__mocks__/api_log.mock.ts
@@ -0,0 +1,17 @@
+/*
+ * 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 const mockApiLog = {
+ timestamp: '1970-01-01T12:00:00.000Z',
+ http_method: 'POST',
+ status: 200,
+ user_agent: 'Mozilla/5.0',
+ full_request_path: '/api/as/v1/engines/national-parks-demo/search.json',
+ request_body: '{"query":"test search"}',
+ response_body:
+ '{"meta":{"page":{"current":1,"total_pages":0,"total_results":0,"size":20}},"results":[]}',
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.test.tsx
new file mode 100644
index 000000000000..6bebeee80465
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.test.tsx
@@ -0,0 +1,62 @@
+/*
+ * 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, setMockActions } from '../../../../__mocks__';
+import { mockApiLog } from '../__mocks__/api_log.mock';
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { EuiFlyout, EuiBadge } from '@elastic/eui';
+
+import { ApiLogFlyout, ApiLogHeading } from './api_log_flyout';
+
+describe('ApiLogFlyout', () => {
+ const values = {
+ isFlyoutOpen: true,
+ apiLog: mockApiLog,
+ };
+ const actions = {
+ closeFlyout: jest.fn(),
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ setMockValues(values);
+ setMockActions(actions);
+ });
+
+ it('renders', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.find('h2').text()).toEqual('Request details');
+ expect(wrapper.find(ApiLogHeading).last().dive().find('h3').text()).toEqual('Response body');
+ expect(wrapper.find(EuiBadge).prop('children')).toEqual('POST');
+ });
+
+ it('closes the flyout', () => {
+ const wrapper = shallow();
+
+ wrapper.find(EuiFlyout).simulate('close');
+ expect(actions.closeFlyout).toHaveBeenCalled();
+ });
+
+ it('does not render if the flyout is not open', () => {
+ setMockValues({ ...values, isFlyoutOpen: false });
+ const wrapper = shallow();
+
+ expect(wrapper.isEmptyRender()).toBe(true);
+ });
+
+ it('does not render if a current apiLog has not been set', () => {
+ setMockValues({ ...values, apiLog: null });
+ const wrapper = shallow();
+
+ expect(wrapper.isEmptyRender()).toBe(true);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.tsx
new file mode 100644
index 000000000000..dd53e997da0f
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.tsx
@@ -0,0 +1,137 @@
+/*
+ * 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.
+ */
+/*
+ * 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 { useActions, useValues } from 'kea';
+
+import {
+ EuiPortal,
+ EuiFlyout,
+ EuiFlyoutHeader,
+ EuiTitle,
+ EuiFlyoutBody,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSpacer,
+ EuiBadge,
+ EuiHealth,
+ EuiText,
+ EuiCode,
+ EuiCodeBlock,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { getStatusColor, attemptToFormatJson } from '../utils';
+
+import { ApiLogLogic } from './';
+
+export const ApiLogFlyout: React.FC = () => {
+ const { isFlyoutOpen, apiLog } = useValues(ApiLogLogic);
+ const { closeFlyout } = useActions(ApiLogLogic);
+
+ if (!isFlyoutOpen) return null;
+ if (!apiLog) return null;
+
+ return (
+
+
+
+
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.flyout.title', {
+ defaultMessage: 'Request details',
+ })}
+
+
+
+
+
+
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.methodTitle', {
+ defaultMessage: 'Method',
+ })}
+
+
+ {apiLog.http_method}
+
+
+
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.statusTitle', {
+ defaultMessage: 'Status',
+ })}
+
+ {apiLog.status}
+
+
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.timestampTitle', {
+ defaultMessage: 'Timestamp',
+ })}
+
+ {apiLog.timestamp}
+
+
+
+
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.userAgentTitle', {
+ defaultMessage: 'User agent',
+ })}
+
+
+ {apiLog.user_agent}
+
+
+
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.requestPathTitle', {
+ defaultMessage: 'Request path',
+ })}
+
+
+ {apiLog.full_request_path}
+
+
+
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.requestBodyTitle', {
+ defaultMessage: 'Request body',
+ })}
+
+
+ {attemptToFormatJson(apiLog.request_body)}
+
+
+
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.responseBodyTitle', {
+ defaultMessage: 'Response body',
+ })}
+
+
+ {attemptToFormatJson(apiLog.response_body)}
+
+
+
+
+ );
+};
+
+export const ApiLogHeading: React.FC = ({ children }) => (
+
+ {children}
+
+);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.test.tsx
new file mode 100644
index 000000000000..2b7ca7510e8e
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.test.tsx
@@ -0,0 +1,57 @@
+/*
+ * 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 { LogicMounter } from '../../../../__mocks__';
+import { mockApiLog } from '../__mocks__/api_log.mock';
+
+import { ApiLogLogic } from './';
+
+describe('ApiLogLogic', () => {
+ const { mount } = new LogicMounter(ApiLogLogic);
+
+ const DEFAULT_VALUES = {
+ isFlyoutOpen: false,
+ apiLog: null,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('has expected default values', () => {
+ mount();
+ expect(ApiLogLogic.values).toEqual(DEFAULT_VALUES);
+ });
+
+ describe('actions', () => {
+ describe('openFlyout', () => {
+ it('sets isFlyoutOpen to true & sets the current apiLog', () => {
+ mount({ isFlyoutOpen: false, apiLog: null });
+ ApiLogLogic.actions.openFlyout(mockApiLog);
+
+ expect(ApiLogLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ isFlyoutOpen: true,
+ apiLog: mockApiLog,
+ });
+ });
+ });
+
+ describe('closeFlyout', () => {
+ it('sets isFlyoutOpen to false & resets the current apiLog', () => {
+ mount({ isFlyoutOpen: true, apiLog: mockApiLog });
+ ApiLogLogic.actions.closeFlyout();
+
+ expect(ApiLogLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ isFlyoutOpen: false,
+ apiLog: null,
+ });
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.ts
new file mode 100644
index 000000000000..8b7c5f70f605
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.ts
@@ -0,0 +1,44 @@
+/*
+ * 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 { kea, MakeLogicType } from 'kea';
+
+import { ApiLog } from '../types';
+
+interface ApiLogValues {
+ isFlyoutOpen: boolean;
+ apiLog: ApiLog | null;
+}
+
+interface ApiLogActions {
+ openFlyout(apiLog: ApiLog): { apiLog: ApiLog };
+ closeFlyout(): void;
+}
+
+export const ApiLogLogic = kea>({
+ path: ['enterprise_search', 'app_search', 'api_log_logic'],
+ actions: () => ({
+ openFlyout: (apiLog) => ({ apiLog }),
+ closeFlyout: true,
+ }),
+ reducers: () => ({
+ isFlyoutOpen: [
+ false,
+ {
+ openFlyout: () => true,
+ closeFlyout: () => false,
+ },
+ ],
+ apiLog: [
+ null,
+ {
+ openFlyout: (_, { apiLog }) => apiLog,
+ closeFlyout: () => null,
+ },
+ ],
+ }),
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/index.ts
new file mode 100644
index 000000000000..dcf949d9bf22
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/index.ts
@@ -0,0 +1,9 @@
+/*
+ * 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 { ApiLogFlyout } from './api_log_flyout';
+export { ApiLogLogic } from './api_log_logic';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx
index 8ca15906783f..4690911fad77 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx
@@ -26,6 +26,7 @@ import { Loading } from '../../../shared/loading';
import { LogRetentionCallout, LogRetentionTooltip, LogRetentionOptions } from '../log_retention';
+import { ApiLogFlyout } from './api_log';
import { ApiLogsTable, NewApiEventsPrompt } from './components';
import { API_LOGS_TITLE, RECENT_API_EVENTS } from './constants';
@@ -75,6 +76,7 @@ export const ApiLogs: React.FC = ({ engineBreadcrumb }) => {
+
>
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts
index 7b3ee80668ac..2eda4c6323fa 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts
@@ -6,6 +6,7 @@
*/
import { LogicMounter, mockHttpValues, mockFlashMessageHelpers } from '../../../__mocks__';
+import { mockApiLog } from './__mocks__/api_log.mock';
import '../../__mocks__/engine_logic.mock';
import { nextTick } from '@kbn/test/jest';
@@ -29,17 +30,7 @@ describe('ApiLogsLogic', () => {
};
const MOCK_API_RESPONSE = {
- results: [
- {
- timestamp: '1970-01-01T12:00:00.000Z',
- http_method: 'POST',
- status: 200,
- user_agent: 'some browser agent string',
- full_request_path: '/api/as/v1/engines/national-parks-demo/search.json',
- request_body: '{"someMockRequest":"hello"}',
- response_body: '{"someMockResponse":"world"}',
- },
- ],
+ results: [mockApiLog, mockApiLog],
meta: {
page: {
current: 1,
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx
index 99fce81ca348..768295ec1389 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx
@@ -53,6 +53,7 @@ describe('ApiLogsTable', () => {
};
const actions = {
onPaginate: jest.fn(),
+ openFlyout: jest.fn(),
};
beforeEach(() => {
@@ -86,7 +87,7 @@ describe('ApiLogsTable', () => {
expect(wrapper.find(EuiButtonEmpty)).toHaveLength(3);
wrapper.find('[data-test-subj="ApiLogsTableDetailsButton"]').first().simulate('click');
- // TODO: API log details flyout
+ expect(actions.openFlyout).toHaveBeenCalled();
});
it('renders an empty prompt if no items are passed', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx
index 8ebcc4350f7f..5ecf8e1ba333 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx
@@ -22,6 +22,7 @@ import { FormattedRelative } from '@kbn/i18n/react';
import { convertMetaToPagination, handlePageChange } from '../../../../shared/table_pagination';
+import { ApiLogLogic } from '../api_log';
import { ApiLogsLogic } from '../index';
import { ApiLog } from '../types';
import { getStatusColor } from '../utils';
@@ -34,6 +35,7 @@ interface Props {
export const ApiLogsTable: React.FC = ({ hasPagination }) => {
const { dataLoading, apiLogs, meta } = useValues(ApiLogsLogic);
const { onPaginate } = useActions(ApiLogsLogic);
+ const { openFlyout } = useActions(ApiLogLogic);
const columns: Array> = [
{
@@ -81,7 +83,7 @@ export const ApiLogsTable: React.FC = ({ hasPagination }) => {
size="s"
className="apiLogDetailButton"
data-test-subj="ApiLogsTableDetailsButton"
- // TODO: flyout onclick
+ onClick={() => openFlyout(apiLog)}
>
{i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.detailsButtonLabel', {
defaultMessage: 'Details',
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts
index 183956e51d8d..568026dab231 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts
@@ -7,5 +7,6 @@
export { API_LOGS_TITLE } from './constants';
export { ApiLogsTable, NewApiEventsPrompt } from './components';
+export { ApiLogFlyout } from './api_log';
export { ApiLogs } from './api_logs';
export { ApiLogsLogic } from './api_logs_logic';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.test.ts
index f9b6dcea2cbf..ac464e2af353 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.test.ts
@@ -5,7 +5,9 @@
* 2.0.
*/
-import { getDateString, getStatusColor } from './utils';
+import dedent from 'dedent';
+
+import { getDateString, getStatusColor, attemptToFormatJson } from './utils';
describe('getDateString', () => {
const mockDate = jest
@@ -32,3 +34,20 @@ describe('getStatusColor', () => {
expect(getStatusColor(503)).toEqual('danger');
});
});
+
+describe('attemptToFormatJson', () => {
+ it('takes an unformatted JSON string and correctly newlines/indents it', () => {
+ expect(attemptToFormatJson('{"hello":"world","lorem":{"ipsum":"dolor","sit":"amet"}}'))
+ .toEqual(dedent`{
+ "hello": "world",
+ "lorem": {
+ "ipsum": "dolor",
+ "sit": "amet"
+ }
+ }`);
+ });
+
+ it('returns the original content if it is not properly formatted JSON', () => {
+ expect(attemptToFormatJson('{invalid json}')).toEqual('{invalid json}');
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.ts
index 3217a1561ce7..7e5f19686f13 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.ts
@@ -19,3 +19,13 @@ export const getStatusColor = (status: number) => {
if (status >= 500) color = 'danger';
return color;
};
+
+export const attemptToFormatJson = (possibleJson: string) => {
+ try {
+ // it is JSON, we can format it with newlines/indentation
+ return JSON.stringify(JSON.parse(possibleJson), null, 2);
+ } catch {
+ // if it's not JSON, we return the original content
+ return possibleJson;
+ }
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx
index 3686f380407e..18f27c3a1e83 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx
@@ -13,7 +13,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { EuiButtonEmptyTo } from '../../../../shared/react_router_helpers';
import { ENGINE_API_LOGS_PATH } from '../../../routes';
-import { ApiLogsLogic, ApiLogsTable, NewApiEventsPrompt } from '../../api_logs';
+import { ApiLogsLogic, ApiLogsTable, NewApiEventsPrompt, ApiLogFlyout } from '../../api_logs';
import { RECENT_API_EVENTS } from '../../api_logs/constants';
import { DataPanel } from '../../data_panel';
import { generateEnginePath } from '../../engine';
@@ -46,6 +46,7 @@ export const RecentApiLogs: React.FC = () => {
hasBorder
>
+
);
};