From 1ff73b2e9746a6d6ed204e703e4acc28dfda09db Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 21 Oct 2019 18:27:08 +0200 Subject: [PATCH] First iteration of nearly functional public --- .../public/application/app_context.tsx | 29 +++ .../np_ready/public/application/boot.tsx | 26 +++ .../highlight_details/highlight_details.tsx | 197 ------------------ .../highlight_details_flyout.test.tsx} | 6 +- .../highlight_details_flyout.tsx | 112 ++++++++++ .../highlight_details_table.tsx | 0 .../highlight_details_flyout/index.ts | 7 + .../public/application/components/index.ts | 3 + .../components/license_error_notice.tsx | 62 ++++++ .../profile_tree/__tests__/init_data.test.ts | 4 +- .../profile_tree/highlight_context.tsx | 32 ++- .../components/profile_tree/index.ts | 1 + .../components/profile_tree/profile_tree.tsx | 11 +- .../shard_details/shard_details.tsx | 4 +- .../shard_details/shard_details_tree.tsx | 8 +- .../shard_details/shard_details_tree_leaf.tsx | 42 ++-- .../profile_tree/use_highlight_tree_leaf.ts | 11 +- .../components/searchprofiler_tabs.tsx | 39 ++-- .../public/application/containers/index.ts | 8 + .../public/application/containers/main.tsx | 73 +++++++ .../containers/profile_query_editor.tsx | 53 +++++ .../highlight_details => hooks}/index.ts | 2 +- .../application/hooks/use_do_profile.ts | 33 +++ .../np_ready/public/application/index.tsx | 26 +++ .../application/styles/_directives.scss | 135 ++++++++++++ .../public/application/styles/_index.scss | 2 + .../public/application/styles/_mixins.scss | 36 ++++ .../application/utils/ns_to_pretty.test.ts | 50 +++++ .../searchprofiler/np_ready/public/index.ts | 7 + .../searchprofiler/np_ready/public/legacy.ts | 55 +++++ .../searchprofiler/np_ready/public/plugin.ts | 54 +++++ 31 files changed, 858 insertions(+), 270 deletions(-) create mode 100644 x-pack/legacy/plugins/searchprofiler/np_ready/public/application/app_context.tsx create mode 100644 x-pack/legacy/plugins/searchprofiler/np_ready/public/application/boot.tsx delete mode 100644 x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/highlight_details/highlight_details.tsx rename x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/{highlight_details/highlight_details.test.tsx => highlight_details_flyout/highlight_details_flyout.test.tsx} (86%) create mode 100644 x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/highlight_details_flyout/highlight_details_flyout.tsx rename x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/{highlight_details => highlight_details_flyout}/highlight_details_table.tsx (100%) create mode 100644 x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/highlight_details_flyout/index.ts create mode 100644 x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/license_error_notice.tsx create mode 100644 x-pack/legacy/plugins/searchprofiler/np_ready/public/application/containers/index.ts create mode 100644 x-pack/legacy/plugins/searchprofiler/np_ready/public/application/containers/main.tsx create mode 100644 x-pack/legacy/plugins/searchprofiler/np_ready/public/application/containers/profile_query_editor.tsx rename x-pack/legacy/plugins/searchprofiler/np_ready/public/application/{components/highlight_details => hooks}/index.ts (79%) create mode 100644 x-pack/legacy/plugins/searchprofiler/np_ready/public/application/hooks/use_do_profile.ts create mode 100644 x-pack/legacy/plugins/searchprofiler/np_ready/public/application/index.tsx create mode 100644 x-pack/legacy/plugins/searchprofiler/np_ready/public/application/styles/_directives.scss create mode 100644 x-pack/legacy/plugins/searchprofiler/np_ready/public/application/styles/_index.scss create mode 100644 x-pack/legacy/plugins/searchprofiler/np_ready/public/application/styles/_mixins.scss create mode 100644 x-pack/legacy/plugins/searchprofiler/np_ready/public/application/utils/ns_to_pretty.test.ts create mode 100644 x-pack/legacy/plugins/searchprofiler/np_ready/public/legacy.ts diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/app_context.tsx b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/app_context.tsx new file mode 100644 index 00000000000000..4ca1ffa3beceb4 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/app_context.tsx @@ -0,0 +1,29 @@ +/* + * 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 React, { useContext, createContext } from 'react'; +import { HttpSetup, NotificationsSetup } from '../../../../../../../src/core/public'; + +export interface ContextValue { + http: HttpSetup; + notifications: NotificationsSetup; + licenseEnabled: boolean; + formatAngularHttpError: (message: string) => string; +} + +const AppContext = createContext(null as any); + +export const AppContextProvider = ({ children, value }: { children: any; value: ContextValue }) => { + return {children}; +}; + +export const useAppContext = () => { + const ctx = useContext(AppContext); + if (ctx == null) { + throw new Error(`useAppContext must be called inside AppContextProvider`); + } + return ctx; +}; diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/boot.tsx b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/boot.tsx new file mode 100644 index 00000000000000..1944ff900bfff9 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/boot.tsx @@ -0,0 +1,26 @@ +/* + * 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 { render, unmountComponentAtNode } from 'react-dom'; +import React from 'react'; +import { HttpStart as Http, NotificationsSetup } from '../../../../../../../src/core/public'; +import { App } from '.'; + +export interface Dependencies { + el: HTMLElement; + http: Http; + licenseEnabled: boolean; + I18nContext: any; + notifications: NotificationsSetup; + formatAngularHttpError: any; +} + +export type AppDependencies = Omit; + +export function boot(deps: Dependencies): () => void { + const { el, ...rest } = deps; + render(, deps.el); + return () => unmountComponentAtNode(deps.el); +} diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/highlight_details/highlight_details.tsx b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/highlight_details/highlight_details.tsx deleted file mode 100644 index c991176d27d6c0..00000000000000 --- a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/highlight_details/highlight_details.tsx +++ /dev/null @@ -1,197 +0,0 @@ -/* - * 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 React from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiFlyout, - EuiFlyoutHeader, - EuiFlyoutBody, - EuiIconTip, - EuiText, - EuiCodeBlock, -} from '@elastic/eui'; - -import { msToPretty } from '../../utils'; -import { HighlightDetailsTable } from './highlight_details_table'; -import { BreakdownItem } from '../../types'; - -export interface Props { - indexName: string; - shardID: string; - shardNumber: string; - queryType: string; - lucene: string; - time: number; - selfTime: number; - breakdown: BreakdownItem[]; -} - -const DefEntry = ({ title, body }: { title: string | JSX.Element; body: string | JSX.Element }) => ( - <> -
{title}
-
{body}
- -); - -export const HighlightDetails = ({ - indexName, - shardID, - shardNumber, - queryType, - lucene, - selfTime, - time, - breakdown, -}: Props) => { - return ( - {}}> - - {indexName} - - [{shardID}][{shardNumber}] - - - - -
- {/* Type Entry */} - - {/* Description Entry */} - lucene} - /> - {/* Total Time Entry */} - - {i18n.translate('xpack.searchProfiler.highlightDetails.totalTimeTitle', { - defaultMessage: 'Total Time', - })} - - - } - body={msToPretty(time, 3)} - /> - {/* Self Time Entry */} - - {i18n.translate('xpack.searchProfiler.highlightDetails.selfTimeTooltip', { - defaultMessage: 'Total Time', - })} - - - } - body={msToPretty(selfTime, 3)} - /> -
- -
-
-
- ); -}; - -// -//
-//

-// {{ detailRow.indexName }}
-// [{{ detailRow.shardID }}][{{ detailRow.shardNumber }}] -//

-//
-// -//
-//
-//
-//
-//
-//
{{ detailRow.query_type }}
-//
-//
{{ detailRow.lucene }}
-//
-// -//
-//
{{ detailRow.time | msToPretty:3}}
-//
-// -//
-//
{{ detailRow.selfTime | msToPretty:3}}
-//
-//
-//
-//

-//
-// -// -// -// -// -// -//
{{ -// breakdown -// .key -// }} -// -// {{ breakdown.time | nsToPretty: 1}} -// -// -// -// -// {{ breakdown.relative }}% -// -//
-//
-//
diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/highlight_details/highlight_details.test.tsx b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/highlight_details_flyout/highlight_details_flyout.test.tsx similarity index 86% rename from x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/highlight_details/highlight_details.test.tsx rename to x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/highlight_details_flyout/highlight_details_flyout.test.tsx index b98e084ba1b4ca..5f0e4fc91fb743 100644 --- a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/highlight_details/highlight_details.test.tsx +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/highlight_details_flyout/highlight_details_flyout.test.tsx @@ -5,9 +5,9 @@ */ import { registerTestBed } from '../../../../../../../../test_utils'; -import { HighlightDetails, Props } from '.'; +import { HighlightDetailsFlyout, Props } from '.'; -describe('Highlight Details Component', () => { +describe('Highlight Details Flyout', () => { it('renders', async () => { const props: Props = { breakdown: [ @@ -42,7 +42,7 @@ describe('Highlight Details Component', () => { time: 100, }; - const init = registerTestBed(HighlightDetails); + const init = registerTestBed(HighlightDetailsFlyout); await init(props); }); }); diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/highlight_details_flyout/highlight_details_flyout.tsx b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/highlight_details_flyout/highlight_details_flyout.tsx new file mode 100644 index 00000000000000..8b8a1152d9dac3 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/highlight_details_flyout/highlight_details_flyout.tsx @@ -0,0 +1,112 @@ +/* + * 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 React from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiIconTip, + EuiText, + EuiCodeBlock, +} from '@elastic/eui'; + +import { msToPretty } from '../../utils'; +import { HighlightDetailsTable } from './highlight_details_table'; +import { Operation, Shard } from '../../types'; + +export interface Props { + operation: Operation; + shard: Shard; + indexName: string; + onClose: () => void; +} + +const DefEntry = ({ title, body }: { title: string | JSX.Element; body: string | JSX.Element }) => ( + <> +
{title}
+
{body}
+ +); + +export const HighlightDetailsFlyout = ({ indexName, operation, shard, onClose }: Props) => { + return ( + onClose()}> + + {indexName} + + [{/* shard id */ shard.id[0]}][{/* shard number */ shard.id[2]}] + + + + +
+ {/* Type Entry */} + + {/* Description Entry */} + {operation.lucene!}} + /> + {/* Total Time Entry */} + + {i18n.translate('xpack.searchProfiler.highlightDetails.totalTimeTitle', { + defaultMessage: 'Total Time', + })} + + + } + body={msToPretty(operation.time, 3)} + /> + {/* Self Time Entry */} + + {i18n.translate('xpack.searchProfiler.highlightDetails.selfTimeTooltip', { + defaultMessage: 'Total Time', + })} + + + } + body={msToPretty(operation.selfTime!, 3)} + /> +
+ +
+
+
+ ); +}; diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/highlight_details/highlight_details_table.tsx b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/highlight_details_flyout/highlight_details_table.tsx similarity index 100% rename from x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/highlight_details/highlight_details_table.tsx rename to x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/highlight_details_flyout/highlight_details_table.tsx diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/highlight_details_flyout/index.ts b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/highlight_details_flyout/index.ts new file mode 100644 index 00000000000000..36ae010fabfa48 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/highlight_details_flyout/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { HighlightDetailsFlyout, Props } from './highlight_details_flyout'; diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/index.ts b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/index.ts index 3b609caa1739a4..6a5a64d0ac1d6f 100644 --- a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/index.ts +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/index.ts @@ -5,3 +5,6 @@ */ export { SearchProfilerTabs } from './searchprofiler_tabs'; +export { LicenseWarningNotice } from './license_error_notice'; +export { ProfileTree, OnHighlightChangeArgs } from './profile_tree'; +export { HighlightDetailsFlyout } from './highlight_details_flyout'; diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/license_error_notice.tsx b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/license_error_notice.tsx new file mode 100644 index 00000000000000..73dc7837ade5df --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/license_error_notice.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiCallOut, EuiText, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export const LicenseWarningNotice = () => { + const registerLicenseLinkLabel = i18n.translate('xpack.searchProfiler.registerLicenseLinkLabel', { + defaultMessage: 'register a license', + }); + const trialLicense = i18n.translate('xpack.searchProfiler.trialLicenseTitle', { + defaultMessage: 'Trial', + }); + const basicLicense = i18n.translate('xpack.searchProfiler.basicLicenseTitle', { + defaultMessage: 'Basic', + }); + const goldLicense = i18n.translate('xpack.searchProfiler.goldLicenseTitle', { + defaultMessage: 'Gold', + }); + const platinumLicense = i18n.translate('xpack.searchProfiler.platinumLicenseTitle', { + defaultMessage: 'Platinum', + }); + + return ( + + +

+ + {i18n.translate('xpack.searchProfiler.licenseErrorMessageTitle', { + defaultMessage: 'License error', + })} +

+
+ + +

+ {i18n.translate('xpack.searchProfiler.licenseErrorMessageDescription', { + defaultMessage: + 'The Profiler Visualization requires an active license ({licenseTypeList} or {platinumLicenseType}), but none were found in your cluster.', + values: { + html_licenseTypeList: `${trialLicense}, ${basicLicense}, ${goldLicense}`, + html_platinumLicenseType: `${platinumLicense}`, + }, + })} +

+
+ +

+ {i18n.translate('xpack.searchProfiler.registerLicenseDescription', { + defaultMessage: 'Please {registerLicenseLink} to continue using the Search Profiler', + values: { + html_registerLicenseLink: `${registerLicenseLinkLabel}`, + }, + })} +

+
+ ); +}; diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/__tests__/init_data.test.ts b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/__tests__/init_data.test.ts index d0034e753c9a5e..6cd19947a26bc3 100644 --- a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/__tests__/init_data.test.ts +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/__tests__/init_data.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Shard } from '../../../types'; +import { ShardSerialized } from '../../../types'; import { initDataFor } from '../init_data'; import { searchResponse } from './fixtures/search_response'; @@ -12,7 +12,7 @@ import { processedResponseWithFirstShard } from './fixtures/processed_search_res describe('ProfileTree init data', () => { test('provides the expected result', () => { - const input: Shard[] = searchResponse as any; + const input: ShardSerialized[] = searchResponse as any; const actual = initDataFor('searches')(input); expect(actual[0].name).toEqual(processedResponseWithFirstShard[0].name); diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/highlight_context.tsx b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/highlight_context.tsx index 69767f13d24bb7..554780d8294170 100644 --- a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/highlight_context.tsx +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/highlight_context.tsx @@ -5,21 +5,37 @@ */ import React, { useContext, createContext, useState } from 'react'; - -type Store = Record; +import { Operation, Shard } from '../../types'; const HighlightContext = createContext<{ - store: Store; - setStore: (id: string, value: boolean) => void; + selectedRow: string; + setStore: (args: OnHighlightChangeArgs & { id: string }) => void; }>(null as any); -export const HighlightContextProvider = ({ children }: { children: any }) => { - const [store, setStore] = useState(Object.create(null)); +export interface OnHighlightChangeArgs { + indexName: string; + shard: Shard; + operation: Operation; +} + +type OnHighlightChangeHandler = (data: OnHighlightChangeArgs) => void; + +export const HighlightContextProvider = ({ + children, + onHighlight, +}: { + children: any; + onHighlight: OnHighlightChangeHandler; +}) => { + const [selectedRow, setSelectedRow] = useState(''); return ( setStore({ ...store, [id]: value }), + selectedRow, + setStore: ({ id, ...onHighlightChangeArgs }: OnHighlightChangeArgs & { id: string }) => { + onHighlight(onHighlightChangeArgs); + setSelectedRow(id); + }, }} > {children} diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/index.ts b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/index.ts index a501cca946b5c5..78efd2cb6687f0 100644 --- a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/index.ts +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/index.ts @@ -5,3 +5,4 @@ */ export { ProfileTree } from './profile_tree'; +export { OnHighlightChangeArgs } from './highlight_context'; diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/profile_tree.tsx b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/profile_tree.tsx index e55a6535567708..d072d23a4a842c 100644 --- a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/profile_tree.tsx +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/profile_tree.tsx @@ -11,22 +11,23 @@ import { IndexDetails } from './index_details'; import { ShardDetails } from './shard_details'; import { initDataFor } from './init_data'; import { Targets, ShardSerialized } from '../../types'; -import { HighlightContextProvider } from './highlight_context'; +import { HighlightContextProvider, OnHighlightChangeArgs } from './highlight_context'; export interface Props { target: Targets; - data: ShardSerialized[]; + data: ShardSerialized[] | null; + onHighlight: (args: OnHighlightChangeArgs) => void; } -export const ProfileTree = ({ data, target }: Props) => { - if (data.length === 0) { +export const ProfileTree = ({ data, target, onHighlight }: Props) => { + if (!data || data.length === 0) { return null; } const sortedIndices = initDataFor(target)(data); return ( - + {sortedIndices.map(index => ( diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/shard_details/shard_details.tsx b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/shard_details/shard_details.tsx index dfdadc03595f5a..39af28a2665f67 100644 --- a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/shard_details/shard_details.tsx +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/shard_details/shard_details.tsx @@ -38,7 +38,9 @@ export const ShardDetails = ({ index, shard, operations }: Props) => { [{shard.id[0]}][ {shard.id[2]}] - {shardVisibility ? operations.map(data => ) : null} + {shardVisibility + ? operations.map(data => ) + : null} ); diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/shard_details/shard_details_tree.tsx b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/shard_details/shard_details_tree.tsx index b735b0d79bd72d..420ab540ef726d 100644 --- a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/shard_details/shard_details_tree.tsx +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/shard_details/shard_details_tree.tsx @@ -9,13 +9,15 @@ import { i18n } from '@kbn/i18n'; import { EuiText } from '@elastic/eui'; import { ShardDetailsTreeLeaf } from './shard_details_tree_leaf'; -import { Operation } from '../../../types'; +import { Index, Operation, Shard } from '../../../types'; export interface Props { data: Operation; + index: Index; + shard: Shard; } -export const ShardDetailTree = ({ data }: Props) => { +export const ShardDetailTree = ({ data, index, shard }: Props) => { // Recursively render the tree structure const renderOperations = (operation: Operation): JSX.Element => { const parent = operation.parent; @@ -23,6 +25,8 @@ export const ShardDetailTree = ({ data }: Props) => { return ( <> diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/shard_details/shard_details_tree_leaf.tsx b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/shard_details/shard_details_tree_leaf.tsx index 65724f2536eddf..0fe39dc7a3628a 100644 --- a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/shard_details/shard_details_tree_leaf.tsx +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/shard_details/shard_details_tree_leaf.tsx @@ -11,10 +11,12 @@ import { i18n } from '@kbn/i18n'; import { useHighlightTreeLeaf } from '../use_highlight_tree_leaf'; import { msToPretty } from '../../../utils'; -import { Operation } from '../../../types'; +import { Index, Operation, Shard } from '../../../types'; export interface Props { parentVisible: boolean; + index: Index; + shard: Shard; operation: Operation; } @@ -23,7 +25,7 @@ const TAB_WIDTH_PX = 32; const limitString = (string: string, limit: number) => `${string.slice(0, limit)}${string.length > limit ? '...' : ''}`; -export const ShardDetailsTreeLeaf = ({ parentVisible, operation }: Props) => { +export const ShardDetailsTreeLeaf = ({ parentVisible, operation, index, shard }: Props) => { if (!parentVisible) { return null; } @@ -36,33 +38,27 @@ export const ShardDetailsTreeLeaf = ({ parentVisible, operation }: Props) => {
setVisible(!visible)} > - {operation.hasChildren ? ( - + {op.hasChildren ? ( + ) : ( // Use dot icon for alignment if arrow isn't there )} - {operation.query_type} + {op.query_type}
- - {msToPretty(operation.selfTime!, 1)} + + {msToPretty(op.selfTime!, 1)}
- - {msToPretty(operation.time!, 1)} + + {msToPretty(op.time!, 1)}
@@ -71,7 +67,7 @@ export const ShardDetailsTreeLeaf = ({ parentVisible, operation }: Props) => { const renderLuceneRow = (op: Operation) => (
- {limitString(operation.lucene || '', 120)} + {limitString(op.lucene || '', 120)}
); @@ -84,7 +80,10 @@ export const ShardDetailsTreeLeaf = ({ parentVisible, operation }: Props) => { > {renderTimeRow(operation)} {renderLuceneRow(operation)} - highlight()}> + highlight({ indexName: index.name, operation, shard })} + > {i18n.translate('xpack.searchProfiler.profileTree.body.viewDetailsLabel', { defaultMessage: 'View Details', })} @@ -92,7 +91,12 @@ export const ShardDetailsTreeLeaf = ({ parentVisible, operation }: Props) => { {operation.hasChildren && operation.children.flatMap(childOp => ( - + ))} ); diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/use_highlight_tree_leaf.ts b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/use_highlight_tree_leaf.ts index da35219a59867e..1a1b4739336456 100644 --- a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/use_highlight_tree_leaf.ts +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/profile_tree/use_highlight_tree_leaf.ts @@ -6,24 +6,23 @@ import { useEffect, useState } from 'react'; import uuid from 'uuid'; -import { useHighlightContext } from './highlight_context'; +import { useHighlightContext, OnHighlightChangeArgs } from './highlight_context'; export const useHighlightTreeLeaf = () => { const [id, setId] = useState(''); - const { store, setStore } = useHighlightContext(); + const { selectedRow, setStore } = useHighlightContext(); - const highlight = () => { - setStore(id, true); + const highlight = (value: OnHighlightChangeArgs) => { + setStore({ id, ...value }); }; const isHighlighted = () => { - return store[id]; + return selectedRow === id; }; useEffect(() => { const newId = uuid.v4(); setId(newId); - setStore(newId, false); }, []); return { diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/searchprofiler_tabs.tsx b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/searchprofiler_tabs.tsx index e8aa9a2cd733e7..92324714ac5a67 100644 --- a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/searchprofiler_tabs.tsx +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/searchprofiler_tabs.tsx @@ -8,39 +8,26 @@ import _ from 'lodash'; import React from 'react'; import { EuiTabs, EuiTab } from '@elastic/eui'; - import { FormattedMessage } from '@kbn/i18n/target/types/react'; -function hasSearch(profileResponse: any[]) { - const aggs = _.get(profileResponse, '[0].searches', []); - return aggs.length > 0; -} - -function hasAggregations(profileResponse: any[]) { - const aggs = _.get(profileResponse, '[0].aggregations', []); - return aggs.length > 0; -} - -function handleClick(activateTab: (tabName: string) => void, tabName: string) { - activateTab(tabName); -} +import { Targets } from '../types'; interface Props { - activeTab: { - search: boolean; - aggregations: any; + activeTab: Targets | null; + activateTab: (target: Targets) => void; + has: { + searches: boolean; + aggregations: boolean; }; - activateTab: (tabName: string) => void; - profileResponse: any[]; } -export const SearchProfilerTabs = (props: Props) => { +export const SearchProfilerTabs = ({ activeTab, activateTab, has }: Props) => { return ( handleClick(props.activateTab, 'search')} + isSelected={activeTab === 'searches'} + disabled={!has.searches} + onClick={() => activateTab('searches')} > { /> handleClick(props.activateTab, 'aggregations')} + isSelected={activeTab === 'aggregations'} + disabled={!has.aggregations} + onClick={() => activateTab('aggregations')} > 0; +} + +function hasAggregations(profileResponse: ShardSerialized[]) { + const aggs = _.get(profileResponse, '[0].aggregations', []); + return aggs.length > 0; +} + +export const Main = () => { + const [activeTab, setActiveTab] = useState(null); + const [showDetailsFlyout, setShowDetailsFlyout] = useState(false); + const [currentResponse, setCurrentResponse] = useState(null); + + const [highlightedDetails, setHighlightedDetails] = useState(null); + + const onHighlight = useCallback( + (args: OnHighlightChangeArgs) => { + setHighlightedDetails(args); + setShowDetailsFlyout(true); + }, + [currentResponse] + ); + + return ( + <> +
+
+ setActiveTab(target)} + has={{ + aggregations: Boolean(currentResponse && hasAggregations(currentResponse)), + searches: Boolean(currentResponse && hasSearch(currentResponse)), + }} + /> + {activeTab ? ( + + ) : null} +
+
+
+ setCurrentResponse(resp)} /> + {showDetailsFlyout ? ( + setShowDetailsFlyout(false)} + /> + ) : null} +
+ + ); +}; diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/containers/profile_query_editor.tsx b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/containers/profile_query_editor.tsx new file mode 100644 index 00000000000000..62d26817be375f --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/containers/profile_query_editor.tsx @@ -0,0 +1,53 @@ +/* + * 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 React, { useRef } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiForm, EuiFieldText, EuiFormRow, EuiButton, EuiText } from '@elastic/eui'; +import { Editor } from '../editor'; +import { useDoProfile } from '../hooks'; +import { ShardSerialized } from '../types'; + +interface Props { + onResponse: (response: ShardSerialized[]) => void; +} + +export const ProfileQueryEditor = ({ onResponse }: Props) => { + const indexInputRef = useRef(null as any); + const typeInputRef = useRef(null as any); + + const doProfile = useDoProfile(); + + const handleProfileClick = async () => { + // TODO: Finish adding request body + const result = await doProfile({}); + onResponse(result); + }; + + return ( + <> + + + + + + + + + + handleProfileClick()}> + + {i18n.translate('xpack.searchProfiler.formProfileButtonLabel', { + defaultMessage: 'Profile', + })} + + + + ); +}; diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/highlight_details/index.ts b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/hooks/index.ts similarity index 79% rename from x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/highlight_details/index.ts rename to x-pack/legacy/plugins/searchprofiler/np_ready/public/application/hooks/index.ts index 57d4b5099ba169..4a0de2f1c00f01 100644 --- a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/components/highlight_details/index.ts +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/hooks/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { HighlightDetails, Props } from './highlight_details'; +export { useDoProfile } from './use_do_profile'; diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/hooks/use_do_profile.ts b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/hooks/use_do_profile.ts new file mode 100644 index 00000000000000..83146b8a6f8926 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/hooks/use_do_profile.ts @@ -0,0 +1,33 @@ +/* + * 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 { useAppContext } from '../app_context'; + +export const useDoProfile = () => { + const { http, notifications, formatAngularHttpError } = useAppContext(); + return async (requestBody: any) => { + try { + const resp = await http.post('../api/searchprofiler/profile', requestBody); + if (!resp.data.ok) { + notifications.toasts.addDanger(resp.data.err.msg); + // TODO: Get this error feedback working again + // try { + // const regex = /line=([0-9]+) col=([0-9]+)/g; + // const [, row, column] = regex.exec(resp.data.err.msg); + // + // $scope.markers.push($scope.ace.session.addMarker( + // new Range(row - 1, 0, row - 1, column), 'errorMarker', 'fullLine')); + // } catch (e) { + // Best attempt, not a big deal if we can't highlight the line + // } + throw new Error(resp.data.err.msg); + } + return resp.data.resp.profile.shards; + } catch (e) { + notifications.toasts.addDanger(formatAngularHttpError(e)); + } + }; +}; diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/index.tsx b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/index.tsx new file mode 100644 index 00000000000000..68053109750307 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/index.tsx @@ -0,0 +1,26 @@ +/* + * 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 React from 'react'; +import { Main } from './containers'; +import { AppContextProvider } from './app_context'; + +import { AppDependencies } from './boot'; + +export function App({ + I18nContext, + licenseEnabled, + notifications, + http, + formatAngularHttpError, +}: AppDependencies) { + return ( + + +
+ + + ); +} diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/styles/_directives.scss b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/styles/_directives.scss new file mode 100644 index 00000000000000..7f4f44748aa937 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/styles/_directives.scss @@ -0,0 +1,135 @@ +.prfDevTool__panel { + border-bottom: $euiBorderThin; +} + +.prfDevTool__panelBody { + margin-top: $euiSizeS; + margin-left: $euiSizeL; +} + +// Profile details treeview +.prfDevTool__shardDetailsWrapper { + display: flex; + flex-direction: row-reverse; + justify-content: space-between; + align-items: center; +} + +.prfDevTool__shardDetails--dim small { + color: $euiColorDarkShade; +} + +.prfDevTool__shardBody { + margin-top: $euiSize; +} + +.prfDevTool__shardDetails { + line-height: 1; + overflow-wrap: break-word; + + &:disabled { + text-decoration: none !important; + cursor: default; + } +} + +.prfDevTool__shard { + border: none; +} + +.prfDevTool__index { + width: 100%; + padding: $euiSize $euiSizeS; +} + +.prfDevTool__tvRow--last { + cursor: pointer; +} + +.prfDevTool__tvRow, +.prfDevTool__tvHeader { + display: table; + width: 100%; + table-layout: fixed; +} + +.prfDevTool__tvHeader { + @include euiFontSizeXS; + color: $euiColorDarkShade; +} + +.prfDevTool__cell { + display: table-cell; + vertical-align: middle; + text-align: center; + padding: $euiSizeXS; + + &:first-of-type { + padding-left: 0; + } + + &:last-of-type { + padding-right: 0; + } +} + +.prfDevTool__detail { + font-size: $euiFontSizeS; + padding-left: $euiSizeL - 3px; // Alignment is weird + margin-bottom: $euiSizeS; + display: flex; + justify-content: space-between; + + .euiLink { + flex-shrink: 0; + } +} + +.prfDevTool__description { + text-align: left; +} + +.prfDevTool__time, +.prfDevTool__totalTime, +.prfDevTool__percentage { + width: $euiSize * 5.5; +} + +// BADGES (and those used for progress) + +.prfDevTool__badge { + border: none; + display: block; + // Force text to always be dark on top of white -> pink color + color: lightOrDarkTheme($euiColorDarkestShade, $euiColorLightestShade); +} + +.prfDevTool__progress--percent { + @include prfDevToolProgress; + width: $euiSize * 4; +} + +.prfDevTool__progress--time { + @include prfDevToolProgress(#FFAFAF); + background-color: #F5F5F5; // Must be light at all times + width: $euiSize * 15.5; + // Force text to always be dark on top of white -> pink color + color: lightOrDarkTheme($euiColorDarkestShade, $euiColorLightestShade); +} + +// Breakdown table +.prfDevTool__breakdown { + width:100%; +} + +.prfDevTool__flyoutSubtitle { + margin-bottom: $euiSizeS; + display: inline-block; +} + +@include euiBreakpoint('xs', 's') { + .prfDevTool__shardDetailsWrapper { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/styles/_index.scss b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/styles/_index.scss new file mode 100644 index 00000000000000..69a53dbafe2899 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/styles/_index.scss @@ -0,0 +1,2 @@ +@import 'mixins'; +@import 'directives'; diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/styles/_mixins.scss b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/styles/_mixins.scss new file mode 100644 index 00000000000000..3ca8dce7d6a210 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/styles/_mixins.scss @@ -0,0 +1,36 @@ +@mixin prfDevToolProgress($color: tintOrShade($euiColorPrimary, 60%, 60%)) { + border: none; + display: block; + background-image: + linear-gradient( + to left, + $color 0%, + $color var(--prfDevToolProgressPercentage, auto), + $euiColorLightestShade var(--prfDevToolProgressPercentage, auto), + $euiColorLightestShade 100% + ); + + .prfDevTool__progress--percent-ie { + display: none; + } + + @include internetExplorerOnly { + position: relative; + overflow: hidden; + + .prfDevTool__progress--percent-ie { + display: block; + position: absolute; + top: 0; + bottom: 0; + right: 0; + background: $color; + z-index: 1; + } + + .prfDevTool__progressText { + position: relative; + z-index: 2; + } + } +} diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/utils/ns_to_pretty.test.ts b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/utils/ns_to_pretty.test.ts new file mode 100644 index 00000000000000..6c0e1124af176c --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/application/utils/ns_to_pretty.test.ts @@ -0,0 +1,50 @@ +/* + * 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 { nsToPretty } from './ns_to_pretty'; + +describe('nsToPretty', function() { + it('returns correct time for ns', function() { + const result = nsToPretty(500, 1); + expect(result).to.eql('500.0ns'); + }); + + it('returns correct time for µs', function() { + const result = nsToPretty(5000, 1); + expect(result).to.eql('5.0µs'); + }); + + it('returns correct time for ms', function() { + const result = nsToPretty(5000000, 1); + expect(result).to.eql('5.0ms'); + }); + + it('returns correct time for s', function() { + const result = nsToPretty(5000000000, 1); + expect(result).to.eql('5.0s'); + }); + + it('returns correct time for min', function() { + const result = nsToPretty(5000000000 * 60, 1); + expect(result).to.eql('5.0min'); + }); + + it('returns correct time for hr', function() { + const result = nsToPretty(3.6e12 * 5, 1); + expect(result).to.eql('5.0hr'); + }); + + it('returns correct time for day', function() { + const result = nsToPretty(3.6e12 * 24 * 5, 1); + expect(result).to.eql('5.0d'); + }); + + it('returns correct time for precision', function() { + const result = nsToPretty(500, 5); + expect(result).to.eql('500.00000ns'); + }); +}); diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/index.ts b/x-pack/legacy/plugins/searchprofiler/np_ready/public/index.ts index 41bc2aa2588073..512a45ab5f20a7 100644 --- a/x-pack/legacy/plugins/searchprofiler/np_ready/public/index.ts +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/index.ts @@ -3,3 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import { PluginInitializerContext } from '../../../../../../src/core/public'; +import { SearchProfilerUIPlugin } from './plugin'; + +export function plugin(ctx: PluginInitializerContext) { + return new SearchProfilerUIPlugin(ctx); +} diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/legacy.ts b/x-pack/legacy/plugins/searchprofiler/np_ready/public/legacy.ts new file mode 100644 index 00000000000000..473b8235599026 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/legacy.ts @@ -0,0 +1,55 @@ +/* + * 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. + */ + +/* Legacy imports */ +import { npSetup, npStart } from 'ui/new_platform'; +import { I18nContext } from 'ui/i18n'; +import uiRoutes from 'ui/routes'; +import 'ui/capabilities/route_setup'; +import { toastNotifications } from 'ui/notify'; +// @ts-ignore +import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; +// @ts-ignore +import { formatAngularHttpError } from 'ui/notify/lib'; +/* Legacy imports */ + +import { NotificationsSetup } from '../../../../../../src/core/public'; +import { plugin } from '.'; + +const pluginInstance = plugin({} as any); + +uiRoutes.when('/dev_tools/searchprofiler', { + template: ` +
+`, + requireUICapability: 'dev_tools.show', + controller: $scope => { + const el = document.querySelector('seachProfilerAppRoot')!; + + if (!el) { + const error = `Could not mount app!`; + npSetup.core.fatalErrors.add(error); + throw new Error(error); + } + + pluginInstance.setup(npSetup.core, { + __LEGACY: { + I18nContext, + licenseEnabled: xpackInfo.get('features.searchprofiler.enableAppLink'), + notifications: (toastNotifications as unknown) as NotificationsSetup, + formatAngularHttpError, + el, + }, + }); + + pluginInstance.start( + { + ...npStart.core, + }, + {} + ); + }, +}); diff --git a/x-pack/legacy/plugins/searchprofiler/np_ready/public/plugin.ts b/x-pack/legacy/plugins/searchprofiler/np_ready/public/plugin.ts index 41bc2aa2588073..742a2b2db017d5 100644 --- a/x-pack/legacy/plugins/searchprofiler/np_ready/public/plugin.ts +++ b/x-pack/legacy/plugins/searchprofiler/np_ready/public/plugin.ts @@ -3,3 +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 { + Plugin, + CoreStart, + CoreSetup, + PluginInitializerContext, + NotificationsSetup, +} from '../../../../../../src/core/public'; + +import { boot } from './application/boot'; + +export class SearchProfilerUIPlugin implements Plugin { + constructor(ctx: PluginInitializerContext) {} + + private startDeps: CoreStart = null as any; + + async setup( + core: CoreSetup, + plugins: { + __LEGACY: { + I18nContext: any; + licenseEnabled: boolean; + el: HTMLElement; + notifications: NotificationsSetup; + formatAngularHttpError: any; + }; + } + ) { + const { http } = this.startDeps; + const { + __LEGACY: { I18nContext, licenseEnabled, el, notifications, formatAngularHttpError }, + } = plugins; + core.application.register({ + id: 'searchprofiler', + title: 'SearchProfiler', + mount(ctx, params) { + return boot({ + http, + licenseEnabled, + el, + I18nContext, + notifications, + formatAngularHttpError, + }); + }, + }); + } + + async start(core: CoreStart, plugins: any) { + this.startDeps = core; + } + + async stop() {} +}