diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/app_context.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/app_context.tsx index 119ee271b414df..5f0be6c13d27a2 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/app_context.tsx +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/app_context.tsx @@ -11,7 +11,7 @@ export interface ContextValue { http: HttpSetup; notifications: NotificationsSetup; licenseEnabled: boolean; - formatAngularHttpError: (message: string) => string; + formatAngularHttpError: (error: any) => string; } const AppContext = createContext(null as any); diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/fixtures/search_response.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/fixtures/search_response.ts index 07582c5feed802..b4483cc0fc58e0 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/fixtures/search_response.ts +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/fixtures/search_response.ts @@ -6,7 +6,7 @@ export const searchResponse: any = [ { - id: ['L22w_FX2SbqlQYOP5QrYDg', '.apm-agent-configuration', '0'], + id: '[L22w_FX2SbqlQYOP5QrYDg] [.apm-agent-configuration] [0]', searches: [ { query: [ @@ -52,7 +52,7 @@ export const searchResponse: any = [ aggregations: [], }, { - id: ['L22w_FX2SbqlQYOP5QrYDg', '.kibana_1', '0'], + id: '[L22w_FX2SbqlQYOP5QrYDg] [.kibana_1] [0]', searches: [ { query: [ @@ -129,7 +129,7 @@ export const searchResponse: any = [ aggregations: [], }, { - id: ['L22w_FX2SbqlQYOP5QrYDg', '.kibana_task_manager_1', '0'], + id: '[L22w_FX2SbqlQYOP5QrYDg] [.kibana_task_manager_1] [0]', searches: [ { query: [ @@ -206,7 +206,7 @@ export const searchResponse: any = [ aggregations: [], }, { - id: ['L22w_FX2SbqlQYOP5QrYDg', '.security-7', '0'], + id: '[L22w_FX2SbqlQYOP5QrYDg] [.security-7] [0]', searches: [ { query: [ @@ -256,7 +256,7 @@ export const searchResponse: any = [ aggregations: [], }, { - id: ['L22w_FX2SbqlQYOP5QrYDg', 'kibana_sample_data_logs', '0'], + id: '[L22w_FX2SbqlQYOP5QrYDg] [kibana_sample_data_logs] [0]', searches: [ { query: [ diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/index_details.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/index_details.tsx index e902ac2d6b9dd2..90f983e0cbedff 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/index_details.tsx +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/index_details.tsx @@ -19,9 +19,9 @@ export interface Props { export const IndexDetails = ({ index, target }: Props) => { const { time, name } = index; return ( - + {/* Time details group */} - + { {/* Index Title group */} - +

diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/init_data.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/init_data.ts index c0c4ef1dc95a4f..d7990b1204b217 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/init_data.ts +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/init_data.ts @@ -53,12 +53,19 @@ export function mutateSearchTimesTree(shard: Shard) { const initShards = (data: ShardSerialized[]) => produce(data, draft => { - return draft.map(s => ({ - ...s, - time: 0, - color: '', - relative: 0, - })); + return draft.map(s => { + const idMatch = s.id.match(/\[([^\]\[]*?)\]/g) || []; + const ids = idMatch.map(id => { + return id.replace('[', '').replace(']', ''); + }); + return { + ...s, + id: ids, + time: 0, + color: '', + relative: 0, + }; + }); }); export const calculateShardValues = (target: Targets) => (data: Shard[]) => diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/profile_tree.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/profile_tree.tsx index d072d23a4a842c..485ad6bca7dbca 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/profile_tree.tsx +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/profile_tree.tsx @@ -29,12 +29,12 @@ export const ProfileTree = ({ data, target, onHighlight }: Props) => { return ( {sortedIndices.map(index => ( - - + + - + {index.shards.map(shard => ( { const [shardVisibility, setShardVisibility] = useState(false); return ( - + @@ -39,7 +39,9 @@ export const ShardDetails = ({ index, shard, operations }: Props) => { {shard.id[2]}] {shardVisibility - ? operations.map(data => ) + ? operations.map(data => ( + + )) : null} diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/shard_details/shard_details_tree.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/shard_details/shard_details_tree.tsx index 420ab540ef726d..7935170e1ea049 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/shard_details/shard_details_tree.tsx +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/shard_details/shard_details_tree.tsx @@ -20,15 +20,16 @@ export interface Props { export const ShardDetailTree = ({ data, index, shard }: Props) => { // Recursively render the tree structure const renderOperations = (operation: Operation): JSX.Element => { - const parent = operation.parent; + const nextOperation = operation.treeRoot || operation; + const parent = nextOperation.parent; const parentVisible = parent ? parent.visible : false; return ( <> ); diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main.tsx index aea2cd625c61ea..5ddf8ee3c4cd54 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main.tsx +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main.tsx @@ -50,6 +50,15 @@ export const Main = () => { [currentResponse] ); + const onProfileClick = () => { + setHighlightedDetails(null); + }; + + const onResponse = (resp: ShardSerialized[]) => { + setCurrentResponse(resp); + setActiveTab('searches'); + }; + return ( <> @@ -59,7 +68,7 @@ export const Main = () => {
- setCurrentResponse(resp)} /> +
diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/profile_query_editor.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/profile_query_editor.tsx index c4498a0db5cbd9..0af41ec54940ce 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/profile_query_editor.tsx +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/profile_query_editor.tsx @@ -12,10 +12,20 @@ import { ShardSerialized } from '../types'; import { useAppContext } from '../app_context'; interface Props { + onProfileClick: () => void; onResponse: (response: ShardSerialized[]) => void; } -export const ProfileQueryEditor = ({ onResponse }: Props) => { +const DEFAULT_INDEX_VALUE = '_all'; + +const INITIAL_EDITOR_VALUE = `{ + "query":{ + "match_all" : {} + } +}`; + +export const ProfileQueryEditor = ({ onResponse, onProfileClick }: Props) => { + const editorValueGetter = useRef<() => string>(null as any); const indexInputRef = useRef(null as any); const typeInputRef = useRef(null as any); @@ -23,8 +33,15 @@ export const ProfileQueryEditor = ({ onResponse }: Props) => { const doProfile = useDoProfile(); const handleProfileClick = async () => { - // TODO: Finish adding request body - const result = await doProfile({}); + onProfileClick(); + const result = await doProfile({ + query: editorValueGetter.current!(), + index: indexInputRef.current.value, + type: typeInputRef.current.value, + }); + if (result === null) { + return; + } onResponse(result); }; @@ -34,7 +51,12 @@ export const ProfileQueryEditor = ({ onResponse }: Props) => { - (indexInputRef.current = ref!)} /> + { + indexInputRef.current = ref!; + ref!.value = DEFAULT_INDEX_VALUE; + }} + /> { (typeInputRef.current = ref!)} /> - + handleProfileClick()}> {i18n.translate('xpack.searchProfiler.formProfileButtonLabel', { diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/editor.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/editor.tsx index 0b92dfcec80158..3f431a9693bfb0 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/editor.tsx +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/editor.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useRef, useEffect, useState } from 'react'; +import React, { useRef, useEffect, useState, MutableRefObject } from 'react'; import { Editor as AceEditor } from 'brace'; import { initializeEditor } from './init_editor'; @@ -12,9 +12,15 @@ import { useUIAceKeyboardMode } from './use_ui_ace_keyboard_mode'; export interface Props { licenseEnabled: boolean; + initialValue: string; + /** + * Hack to expose the editor instance's getValue(). This could be probably be better placed + * in React.Context. + */ + valueGetterRef: MutableRefObject<() => string | null>; } -export const Editor = ({ licenseEnabled }: Props) => { +export const Editor = ({ licenseEnabled, initialValue, valueGetterRef }: Props) => { const containerRef = useRef(null as any); const editorInstanceRef = useRef(null as any); const [textArea, setTextArea] = useState(null); @@ -22,6 +28,8 @@ export const Editor = ({ licenseEnabled }: Props) => { useEffect(() => { const divEl = containerRef.current; editorInstanceRef.current = initializeEditor({ el: divEl, licenseEnabled }); + editorInstanceRef.current.setValue(initialValue, 1); + valueGetterRef.current = () => editorInstanceRef.current.getValue() as string; setTextArea(containerRef.current!.querySelector('textarea')); }); return
; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/hooks/use_do_profile.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/hooks/use_do_profile.ts index 83146b8a6f8926..324e00d77a786a 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/hooks/use_do_profile.ts +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/hooks/use_do_profile.ts @@ -3,15 +3,57 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { useAppContext } from '../app_context'; +import { checkForParseErrors } from '../utils'; +import { ShardSerialized } from '../types'; + +interface Args { + query: string; + index: string; + type: string; +} export const useDoProfile = () => { - const { http, notifications, formatAngularHttpError } = useAppContext(); - return async (requestBody: any) => { + const { http, notifications, formatAngularHttpError, licenseEnabled } = useAppContext(); + return async ({ query, index, type }: Args): Promise => { + if (!licenseEnabled) { + return null; + } + const { error, parsed } = checkForParseErrors(query); + if (error) { + notifications.toasts.addError(error, { + title: i18n.translate('xpack.searchProfiler.errorToastTitle', { + defaultMessage: 'JSON parse error', + }), + }); + return null; + } + // Shortcut the network request if we have json with shards already... + if (parsed.profile && parsed.profile.shards) { + return parsed.profile.shards; + } + + const payload: Record = { query }; + + if (index == null || index === '') { + payload.index = '_all'; + } else { + payload.index = index; + } + + if (type != null && type !== '') { + payload.type = type; + } + try { - const resp = await http.post('../api/searchprofiler/profile', requestBody); - if (!resp.data.ok) { + const resp = await http.post('../api/searchprofiler/profile', { + body: JSON.stringify(payload), + headers: { 'Content-Type': 'application/json' }, + }); + + if (!resp.ok) { notifications.toasts.addDanger(resp.data.err.msg); // TODO: Get this error feedback working again // try { @@ -23,11 +65,23 @@ export const useDoProfile = () => { // } catch (e) { // Best attempt, not a big deal if we can't highlight the line // } - throw new Error(resp.data.err.msg); + return null; } - return resp.data.resp.profile.shards; + return resp.resp.profile.shards; } catch (e) { - notifications.toasts.addDanger(formatAngularHttpError(e)); + try { + // Is this a known error type? + const errorString = formatAngularHttpError(e); + notifications.toasts.addError(e, { title: errorString }); + } catch (_) { + // Otherwise just report the original error + notifications.toasts.addError(e, { + title: i18n.translate('xpack.searchProfiler.errorSomethingWentWrongTitle', { + defaultMessage: 'Something went wrong', + }), + }); + } + return null; } }; }; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/types.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/types.ts index 503156c344da4e..2b4dc01c45d6fc 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/types.ts +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/types.ts @@ -32,7 +32,7 @@ export interface Index { } export interface ShardSerialized { - id: string[]; + id: string; searches: Operation[]; aggregations: Operation[]; } diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/check_for_json_errors.test.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/check_for_json_errors.test.ts new file mode 100644 index 00000000000000..9dece5a39e96c3 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/check_for_json_errors.test.ts @@ -0,0 +1,22 @@ +/* + * 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 expect from '@kbn/expect'; +import { checkForParseErrors } from '.'; + +describe('checkForParseErrors', function() { + it('returns error from bad JSON', function() { + const json = '{"foo": {"bar": {"baz": "buzz}}}'; + const result = checkForParseErrors(json); + expect(result.error.message).to.be(`Unexpected end of JSON input`); + }); + + it('returns parsed value from good JSON', function() { + const json = '{"foo": {"bar": {"baz": "buzz"}}}'; + const result = checkForParseErrors(json); + expect(!!result.parsed).to.be(true); + }); +}); diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/check_for_json_errors.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/check_for_json_errors.ts new file mode 100644 index 00000000000000..4267fd0d2f9019 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/check_for_json_errors.ts @@ -0,0 +1,22 @@ +/* + * 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. + */ + +// Convert triple quotes into regular quotes and escape internal quotes. +function collapseLiteralStrings(data: string) { + return data.replace(/"""(?:\s*\r?\n)?((?:.|\r?\n)*?)(?:\r?\n\s*)?"""/g, function(match, literal) { + return JSON.stringify(literal); + }); +} + +export function checkForParseErrors(json: string) { + const sanitizedJson = collapseLiteralStrings(json); + try { + const parsedJson = JSON.parse(sanitizedJson); + return { parsed: parsedJson, error: null }; + } catch (error) { + return { error, parsed: null }; + } +} diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/index.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/index.ts index ebe647fb93e3eb..556a03fc96fe35 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/index.ts +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/index.ts @@ -4,5 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ +export { checkForParseErrors } from './check_for_json_errors'; export { msToPretty } from './ms_to_pretty'; export { nsToPretty } from './ns_to_pretty';