diff --git a/x-pack/legacy/plugins/searchprofiler/index.js b/x-pack/legacy/plugins/searchprofiler/index.js deleted file mode 100644 index 107edeb1f408df..00000000000000 --- a/x-pack/legacy/plugins/searchprofiler/index.js +++ /dev/null @@ -1,56 +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 { resolve } from 'path'; -import { profileRoute } from './server/routes/profile'; - -// License -import Boom from 'boom'; -import { checkLicense } from './server/lib/check_license'; -import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; - - -export const searchprofiler = (kibana) => { - return new kibana.Plugin({ - require: ['elasticsearch', 'xpack_main'], - id: 'searchprofiler', - configPrefix: 'xpack.searchprofiler', - publicDir: resolve(__dirname, 'public'), - - uiExports: { - devTools: ['plugins/searchprofiler/app'], - hacks: ['plugins/searchprofiler/register'], - home: ['plugins/searchprofiler/register_feature'], - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }, - init: function (server) { - const thisPlugin = this; - const xpackMainPlugin = server.plugins.xpack_main; - mirrorPluginStatus(xpackMainPlugin, thisPlugin); - xpackMainPlugin.status.once('green', () => { - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - xpackMainPlugin.info.feature(thisPlugin.id).registerLicenseCheckResultsGenerator(checkLicense); - }); - - // Add server routes and initialize the plugin here - const commonRouteConfig = { - pre: [ - function forbidApiAccess() { - const licenseCheckResults = xpackMainPlugin.info.feature(thisPlugin.id).getLicenseCheckResults(); - if (licenseCheckResults.showAppLink && licenseCheckResults.enableAppLink) { - return null; - } else { - throw Boom.forbidden(licenseCheckResults.message); - } - } - ] - }; - profileRoute(server, commonRouteConfig); - } - - }); -}; diff --git a/x-pack/legacy/plugins/searchprofiler/index.ts b/x-pack/legacy/plugins/searchprofiler/index.ts new file mode 100644 index 00000000000000..5de6ba710235bc --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/index.ts @@ -0,0 +1,65 @@ +/* + * 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 { resolve } from 'path'; +import Boom from 'boom'; + +import { CoreSetup } from 'src/core/server'; +import { Server } from 'src/legacy/server/kbn_server'; +import { LegacySetup } from './server/np_ready/types'; +import { plugin } from './server/np_ready'; + +export const searchprofiler = (kibana: any) => { + const publicSrc = resolve(__dirname, 'public'); + + return new kibana.Plugin({ + require: ['elasticsearch', 'xpack_main'], + id: 'searchprofiler', + configPrefix: 'xpack.searchprofiler', + publicDir: publicSrc, + + uiExports: { + // NP Ready + devTools: [`${publicSrc}/legacy`], + styleSheetPaths: `${publicSrc}/np_ready/application/index.scss`, + // Legacy + hacks: ['plugins/searchprofiler/register'], + home: ['plugins/searchprofiler/register_feature'], + }, + init(server: Server) { + const serverPlugin = plugin(); + const thisPlugin = this; + + const commonRouteConfig = { + pre: [ + function forbidApiAccess() { + const licenseCheckResults = server.plugins.xpack_main.info + .feature(thisPlugin.id) + .getLicenseCheckResults(); + if (licenseCheckResults.showAppLink && licenseCheckResults.enableAppLink) { + return null; + } else { + throw Boom.forbidden(licenseCheckResults.message); + } + }, + ], + }; + + const legacySetup: LegacySetup = { + route: (args: Parameters[0]) => server.route(args), + plugins: { + __LEGACY: { + thisPlugin, + xpackMain: server.plugins.xpack_main, + elasticsearch: server.plugins.elasticsearch, + commonRouteConfig, + }, + }, + }; + serverPlugin.setup({} as CoreSetup, legacySetup); + }, + }); +}; diff --git a/x-pack/legacy/plugins/searchprofiler/public/app.js b/x-pack/legacy/plugins/searchprofiler/public/app.js deleted file mode 100644 index 1c7598ab982bcb..00000000000000 --- a/x-pack/legacy/plugins/searchprofiler/public/app.js +++ /dev/null @@ -1,193 +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. - */ - - -// K5 imports -import { uiModules } from 'ui/modules'; -import { i18n } from '@kbn/i18n'; -import uiRoutes from 'ui/routes'; -import 'ui/capabilities/route_setup'; -import { toastNotifications } from 'ui/notify'; -import { formatAngularHttpError } from 'ui/notify/lib'; - -// License -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; - -// Our imports -import $ from 'jquery'; -import _ from 'lodash'; -import 'ace'; -import 'angular-ui-ace'; -import 'plugins/searchprofiler/directives'; -import './components/searchprofiler_tabs_directive'; -import { Range } from './range'; -import { nsToPretty } from 'plugins/searchprofiler/filters/ns_to_pretty'; -import { msToPretty } from 'plugins/searchprofiler/filters/ms_to_pretty'; -import { checkForParseErrors } from 'plugins/searchprofiler/app_util.js'; -import { initializeEditor } from 'plugins/searchprofiler/editor'; - -// Styles and templates -import 'ui/autoload/all'; -import template from './templates/index.html'; -import { defaultQuery } from './templates/default_query'; - -uiRoutes.when('/dev_tools/searchprofiler', { - template: template, - requireUICapability: 'dev_tools.show', - controller: $scope => { - $scope.registerLicenseLinkLabel = i18n.translate('xpack.searchProfiler.registerLicenseLinkLabel', - { defaultMessage: 'register a license' }); - $scope.trialLicense = i18n.translate('xpack.searchProfiler.trialLicenseTitle', - { defaultMessage: 'Trial' }); - $scope.basicLicense = i18n.translate('xpack.searchProfiler.basicLicenseTitle', - { defaultMessage: 'Basic' }); - $scope.goldLicense = i18n.translate('xpack.searchProfiler.goldLicenseTitle', - { defaultMessage: 'Gold' }); - $scope.platinumLicense = i18n.translate('xpack.searchProfiler.platinumLicenseTitle', - { defaultMessage: 'Platinum' }); - }, -}); - -uiModules - .get('app/searchprofiler', ['ui.ace']) - .controller('profileViz', profileVizController) - .filter('nsToPretty', () => nsToPretty) - .filter('msToPretty', () => msToPretty) - .factory('HighlightService', () => { - const service = { - details: null - }; - return service; - }); - -function profileVizController($scope, $timeout, $http, HighlightService) { - $scope.title = 'Search Profile'; - $scope.description = 'Search profiling and visualization'; - $scope.profileResponse = []; - $scope.highlight = HighlightService; - $scope.index = '_all'; - $scope.query = ''; - - // TODO this map controls which tab is active, but due to how - // the tab directive works, we cannot use a single variable to hold the state. - // Instead we have to map the tab name to true/false, and make sure only one - // state is active. This should be refactored if possible, as it could be trappy! - $scope.activeTab = { - search: true - }; - $scope.markers = []; - $scope.licenseEnabled = xpackInfo.get('features.searchprofiler.enableAppLink'); - - - const editor = initializeEditor({ - el: $('#SearchProfilerInput')[0], - licenseEnabled: $scope.licenseEnabled, - }); - - editor.on('change', () => { - // Do a safe apply/trigger digest - $timeout(() => { - $scope.query = editor.getValue(); - }); - }); - - editor.setValue(defaultQuery, 1); - - $scope.hasQuery = () => Boolean($scope.query); - - $scope.profile = () => { - const { query } = $scope; - if (!$scope.licenseEnabled) { - return; - } - // Reset right detail panel - $scope.resetHighlightPanel(); - let json = checkForParseErrors(query); - if (json.status === false) { - toastNotifications.addError(json.error, { - title: i18n.translate('xpack.searchProfiler.errorToastTitle', { - defaultMessage: 'JSON parse error', - }), - }); - return; - } - json = json.parsed; - - // If we can find the start of a profile JSON output, just try to render it - // without executing - if (json.profile && json.profile.shards) { - $scope.renderProfile(json.profile.shards); - } else { - // Otherwise it's (probably) a regular search, execute remotely - const requestBody = { query }; - if ($scope.index == null || $scope.index === '') { - requestBody.index = '_all'; - } else { - requestBody.index = $scope.index; - } - if (!$scope.type === '') { - requestBody.type = $scope.type; - } - $scope.executeRemoteQuery(requestBody); - } - }; - - $scope.executeRemoteQuery = requestBody => { - $http.post('../api/searchprofiler/profile', requestBody).then(resp => { - if (!resp.data.ok) { - toastNotifications.addDanger(resp.data.err.msg); - - 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 - } - - return; - } - - $scope.renderProfile(resp.data.resp.profile.shards); - }).catch(reason => toastNotifications.addDanger(formatAngularHttpError(reason))); - }; - - $scope.renderProfile = data => { - for (const shard of data) { - shard.id = shard.id.match(/\[([^\]\[]*?)\]/g); - shard.id = _.map(shard.id, id => { - return id.replace('[', '').replace(']', ''); - }); - } - $scope.profileResponse = data; - - const hasAggregations = data[0].aggregations != null && data[0].aggregations.length > 0; - if (!hasAggregations) { - // No aggs, reset back to search panel - $scope.activateTab('search'); - } - }; - - $scope.activateTab = tab => { - // Reset right detail panel - $scope.resetHighlightPanel(); - // Reset active tab map - $scope.activeTab = {}; - if (tab === 'aggregations') { - $scope.activeTab.aggregations = true; - } else { - // Everything has a search, so default to this - $scope.activeTab.search = true; - } - }; - - $scope.resetHighlightPanel = () => { - $scope.highlight.details = null; - }; - -} diff --git a/x-pack/legacy/plugins/searchprofiler/public/components/searchprofiler_tabs.js b/x-pack/legacy/plugins/searchprofiler/public/components/searchprofiler_tabs.js deleted file mode 100644 index 5394e56cef70ad..00000000000000 --- a/x-pack/legacy/plugins/searchprofiler/public/components/searchprofiler_tabs.js +++ /dev/null @@ -1,65 +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 _ from 'lodash'; -import PropTypes from 'prop-types'; -import React from 'react'; - -import { - EuiTabs, - EuiTab -} from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -function hasSearch(profileResponse) { - const aggs = _.get(profileResponse, '[0].searches', []); - return aggs.length > 0; -} - -function hasAggregations(profileResponse) { - const aggs = _.get(profileResponse, '[0].aggregations', []); - return aggs.length > 0; -} - - -function handleClick(activateTab, tabName) { - activateTab(tabName); -} - -export function SearchProfilerTabs(props) { - return ( - - handleClick(props.activateTab, 'search')} - > - - - handleClick(props.activateTab, 'aggregations')} - > - - - - ); -} - -SearchProfilerTabs.propTypes = { - activeTab: PropTypes.any.isRequired, - activateTab: PropTypes.func.isRequired, - profileResponse: PropTypes.array.isRequired, -}; diff --git a/x-pack/legacy/plugins/searchprofiler/public/components/searchprofiler_tabs_directive.js b/x-pack/legacy/plugins/searchprofiler/public/components/searchprofiler_tabs_directive.js deleted file mode 100644 index 6cfeb97f05ebd4..00000000000000 --- a/x-pack/legacy/plugins/searchprofiler/public/components/searchprofiler_tabs_directive.js +++ /dev/null @@ -1,21 +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 'ngreact'; - -import { wrapInI18nContext } from 'ui/i18n'; -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/searchprofiler', ['react']); - -import { SearchProfilerTabs } from './searchprofiler_tabs'; - -module.directive('searchProfilerTabs', function (reactDirective) { - return reactDirective( - wrapInI18nContext(SearchProfilerTabs), - undefined, - { restrict: 'E' } - ); -}); diff --git a/x-pack/legacy/plugins/searchprofiler/public/directives/_directives.scss b/x-pack/legacy/plugins/searchprofiler/public/directives/_directives.scss deleted file mode 100644 index 7f4f44748aa937..00000000000000 --- a/x-pack/legacy/plugins/searchprofiler/public/directives/_directives.scss +++ /dev/null @@ -1,135 +0,0 @@ -.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/public/directives/_index.scss b/x-pack/legacy/plugins/searchprofiler/public/directives/_index.scss deleted file mode 100644 index 69a53dbafe2899..00000000000000 --- a/x-pack/legacy/plugins/searchprofiler/public/directives/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'mixins'; -@import 'directives'; diff --git a/x-pack/legacy/plugins/searchprofiler/public/directives/highlight_details/index.html b/x-pack/legacy/plugins/searchprofiler/public/directives/highlight_details/index.html deleted file mode 100644 index f58515390ded2d..00000000000000 --- a/x-pack/legacy/plugins/searchprofiler/public/directives/highlight_details/index.html +++ /dev/null @@ -1,65 +0,0 @@ -
-

- {{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/public/directives/highlight_details/index.js b/x-pack/legacy/plugins/searchprofiler/public/directives/highlight_details/index.js deleted file mode 100644 index 717778ed3f4a4d..00000000000000 --- a/x-pack/legacy/plugins/searchprofiler/public/directives/highlight_details/index.js +++ /dev/null @@ -1,35 +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 _ from 'lodash'; -import template from 'plugins/searchprofiler/directives/highlight_details/index.html'; -import { uiModules } from 'ui/modules'; - -const uiModule = uiModules.get('app/searchprofiler/directives', []); -uiModule.directive('highlightdetails', HighlightService => { - return { - restrict: 'E', - scope: { - data: '@' - }, - template: template, - link: $scope => { - - function render(data) { - if (!data) { - return; - } - data.breakdown = _.filter(data.breakdown, o => o.key.indexOf('_count') === -1); - $scope.detailRow = data; - } - - $scope.$watch(() => { - return HighlightService.details; - }, render); - - } - }; -}); diff --git a/x-pack/legacy/plugins/searchprofiler/public/directives/profile_tree/__tests__/fixtures/breakdown.js b/x-pack/legacy/plugins/searchprofiler/public/directives/profile_tree/__tests__/fixtures/breakdown.js deleted file mode 100644 index 1d88310dafbfd6..00000000000000 --- a/x-pack/legacy/plugins/searchprofiler/public/directives/profile_tree/__tests__/fixtures/breakdown.js +++ /dev/null @@ -1,76 +0,0 @@ -export const breakdown = { - advance:0, - advance_count:0, - build_scorer:6273, - build_scorer_count:2, - create_weight:1852, - create_weight_count:1, - match:0, - match_count:0, - next_doc:2593093, - next_doc_count:27958, - score:2525502, - score_count:27948 -}; - -export const normalized = [ { key: 'next_doc', - time: 2593093, - relative: '50.6', - color: '#fad2d2', - tip: 'The time taken to advance the iterator to the next matching document.' }, - { key: 'score', - time: 2525502, - relative: '49.3', - color: '#fad2d2', - tip: 'The time taken in actually scoring the document against the query.' }, - { key: 'next_doc_count', - time: 27958, - relative: 0, - color: '#f5f5f5', - tip: '' }, - { key: 'score_count', - time: 27948, - relative: 0, - color: '#f5f5f5', - tip: '' }, - { key: 'build_scorer', - time: 6273, - relative: '0.1', - color: '#f5f5f5', - tip: 'The time taken to create the Scoring object, which is later used to execute the actual scoring of each doc.' }, - { key: 'create_weight', - time: 1852, - relative: '0.0', - color: '#f5f5f5', - tip: 'The time taken to create the Weight object, which holds temporary information during scoring.' }, - { key: 'build_scorer_count', - time: 2, - relative: 0, - color: '#f5f5f5', - tip: '' }, - { key: 'create_weight_count', - time: 1, - relative: 0, - color: '#f5f5f5', - tip: '' }, - { key: 'advance', - time: 0, - relative: '0.0', - color: '#f5f5f5', - tip: 'The time taken to advance the iterator to the next document.' }, - { key: 'advance_count', - time: 0, - relative: 0, - color: '#f5f5f5', - tip: '' }, - { key: 'match', - time: 0, - relative: '0.0', - color: '#f5f5f5', - tip: 'The time taken to execute a secondary, more precise scoring phase (used by phrase queries).' }, - { key: 'match_count', - time: 0, - relative: 0, - color: '#f5f5f5', - tip: '' }, - ]; diff --git a/x-pack/legacy/plugins/searchprofiler/public/directives/profile_tree/__tests__/fixtures/flatten_times.js b/x-pack/legacy/plugins/searchprofiler/public/directives/profile_tree/__tests__/fixtures/flatten_times.js deleted file mode 100644 index c2017f472bc0ba..00000000000000 --- a/x-pack/legacy/plugins/searchprofiler/public/directives/profile_tree/__tests__/fixtures/flatten_times.js +++ /dev/null @@ -1,405 +0,0 @@ -/* eslint quotes: 0 */ -export const flatTimes = [ - { - id:"f1e689b1-dafe-4c2b-9a4d-9bd8f1a53803", - childrenIds:[ - "3339dca6-c34a-49f3-a534-27e46f238bcd", - "9b75ecdd-a1da-45eb-8d13-5bc5f472dba3", - "ddf5aa3e-4b22-4332-9d5e-79a6ae0cc9cb" - ], - lucene:"hour:1 hour:2 #MatchNoDocsQuery[\"User requested \"match_none\" query.\"]", - time:0.447365, - selfTime:0.057085, - timePercentage:"100.00", - query_type:"BooleanQuery", - absoluteColor:"#ffafaf", - depth:0, - hasChildren:true, - breakdown:[ - { - key:"create_weight", - time:401690, - relative:"89.8", - color:"#feb6b6", - tip:"The time taken to create the Weight object, which holds temporary information during scoring." - }, - { - key:"build_scorer", - time:45672, - relative:"10.2", - color:"#f6eeee", - tip:"The time taken to create the Scoring object, which is later used to execute the actual scoring of each doc." - }, - { - key:"build_scorer_count", - time:2, - relative:0, - color:"#f5f5f5", - tip:"" - }, - { - key:"create_weight_count", - time:1, - relative:0, - color:"#f5f5f5", - tip:"" - }, - { - key:"next_doc", - time:0, - relative:"0.0", - color:"#f5f5f5", - tip:"The time taken to advance the iterator to the next matching document." - }, - { - key:"match", - time:0, - relative:"0.0", - color:"#f5f5f5", - tip:"The time taken to execute a secondary, more precise scoring phase (used by phrase queries)." - }, - { - key:"match_count", - time:0, - relative:0, - color:"#f5f5f5", - tip:"" - }, - { - key:"next_doc_count", - time:0, - relative:0, - color:"#f5f5f5", - tip:"" - }, - { - key:"score_count", - time:0, - relative:0, - color:"#f5f5f5", - tip:"" - }, - { - key:"score", - time:0, - relative:"0.0", - color:"#f5f5f5", - tip:"The time taken in actually scoring the document against the query." - }, - { - key:"advance", - time:0, - relative:"0.0", - color:"#f5f5f5", - tip:"The time taken to advance the iterator to the next document." - }, - { - key:"advance_count", - time:0, - relative:0, - color:"#f5f5f5", - tip:"" - } - ] - }, - { - id:"3339dca6-c34a-49f3-a534-27e46f238bcd", - parentId:"f1e689b1-dafe-4c2b-9a4d-9bd8f1a53803", - childrenIds:[ - - ], - lucene:"hour:1", - time:0.192502, - selfTime:0.192502, - timePercentage:"43.03", - query_type:"TermQuery", - absoluteColor:"#f9d7d7", - depth:1, - breakdown:[ - { - key:"create_weight", - time:190989, - relative:"99.2", - color:"#ffb0b0", - tip:"The time taken to create the Weight object, which holds temporary information during scoring." - }, - { - key:"build_scorer", - time:1510, - relative:"0.8", - color:"#f5f4f4", - tip:"The time taken to create the Scoring object, which is later used to execute the actual scoring of each doc." - }, - { - key:"build_scorer_count", - time:2, - relative:0, - color:"#f5f5f5", - tip:"" - }, - { - key:"create_weight_count", - time:1, - relative:0, - color:"#f5f5f5", - tip:"" - }, - { - key:"next_doc", - time:0, - relative:"0.0", - color:"#f5f5f5", - tip:"The time taken to advance the iterator to the next matching document." - }, - { - key:"match", - time:0, - relative:"0.0", - color:"#f5f5f5", - tip:"The time taken to execute a secondary, more precise scoring phase (used by phrase queries)." - }, - { - key:"match_count", - time:0, - relative:0, - color:"#f5f5f5", - tip:"" - }, - { - key:"next_doc_count", - time:0, - relative:0, - color:"#f5f5f5", - tip:"" - }, - { - key:"score_count", - time:0, - relative:0, - color:"#f5f5f5", - tip:"" - }, - { - key:"score", - time:0, - relative:"0.0", - color:"#f5f5f5", - tip:"The time taken in actually scoring the document against the query." - }, - { - key:"advance", - time:0, - relative:"0.0", - color:"#f5f5f5", - tip:"The time taken to advance the iterator to the next document." - }, - { - key:"advance_count", - time:0, - relative:0, - color:"#f5f5f5", - tip:"" - } - ] - }, - { - id:"9b75ecdd-a1da-45eb-8d13-5bc5f472dba3", - parentId:"f1e689b1-dafe-4c2b-9a4d-9bd8f1a53803", - childrenIds:[ - - ], - lucene:"hour:2", - time:0.162608, - selfTime:0.162608, - timePercentage:"36.35", - query_type:"TermQuery", - absoluteColor:"#f9dcdc", - depth:1, - breakdown:[ - { - key:"create_weight", - time:162016, - relative:"99.6", - color:"#ffafaf", - tip:"The time taken to create the Weight object, which holds temporary information during scoring." - }, - { - key:"build_scorer", - time:589, - relative:"0.4", - color:"#f5f5f5", - tip:"The time taken to create the Scoring object, which is later used to execute the actual scoring of each doc." - }, - { - key:"build_scorer_count", - time:2, - relative:0, - color:"#f5f5f5", - tip:"" - }, - { - key:"create_weight_count", - time:1, - relative:0, - color:"#f5f5f5", - tip:"" - }, - { - key:"next_doc", - time:0, - relative:"0.0", - color:"#f5f5f5", - tip:"The time taken to advance the iterator to the next matching document." - }, - { - key:"match", - time:0, - relative:"0.0", - color:"#f5f5f5", - tip:"The time taken to execute a secondary, more precise scoring phase (used by phrase queries)." - }, - { - key:"match_count", - time:0, - relative:0, - color:"#f5f5f5", - tip:"" - }, - { - key:"next_doc_count", - time:0, - relative:0, - color:"#f5f5f5", - tip:"" - }, - { - key:"score_count", - time:0, - relative:0, - color:"#f5f5f5", - tip:"" - }, - { - key:"score", - time:0, - relative:"0.0", - color:"#f5f5f5", - tip:"The time taken in actually scoring the document against the query." - }, - { - key:"advance", - time:0, - relative:"0.0", - color:"#f5f5f5", - tip:"The time taken to advance the iterator to the next document." - }, - { - key:"advance_count", - time:0, - relative:0, - color:"#f5f5f5", - tip:"" - } - ] - }, - { - id:"ddf5aa3e-4b22-4332-9d5e-79a6ae0cc9cb", - parentId:"f1e689b1-dafe-4c2b-9a4d-9bd8f1a53803", - childrenIds:[ - - ], - lucene:"MatchNoDocsQuery[\"User requested \"match_none\" query.\"]", - time:0.03517, - selfTime:0.03517, - timePercentage:"7.86", - query_type:"MatchNoDocsQuery", - absoluteColor:"#f6efef", - depth:1, - breakdown:[ - { - key:"build_scorer", - time:32522, - relative:"92.5", - color:"#feb4b4", - tip:"The time taken to create the Scoring object, which is later used to execute the actual scoring of each doc." - }, - { - key:"create_weight", - time:2645, - relative:"7.5", - color:"#f6f0f0", - tip:"The time taken to create the Weight object, which holds temporary information during scoring." - }, - { - key:"build_scorer_count", - time:2, - relative:0, - color:"#f5f5f5", - tip:"" - }, - { - key:"create_weight_count", - time:1, - relative:0, - color:"#f5f5f5", - tip:"" - }, - { - key:"next_doc", - time:0, - relative:"0.0", - color:"#f5f5f5", - tip:"The time taken to advance the iterator to the next matching document." - }, - { - key:"match", - time:0, - relative:"0.0", - color:"#f5f5f5", - tip:"The time taken to execute a secondary, more precise scoring phase (used by phrase queries)." - }, - { - key:"match_count", - time:0, - relative:0, - color:"#f5f5f5", - tip:"" - }, - { - key:"next_doc_count", - time:0, - relative:0, - color:"#f5f5f5", - tip:"" - }, - { - key:"score_count", - time:0, - relative:0, - color:"#f5f5f5", - tip:"" - }, - { - key:"score", - time:0, - relative:"0.0", - color:"#f5f5f5", - tip:"The time taken in actually scoring the document against the query." - }, - { - key:"advance", - time:0, - relative:"0.0", - color:"#f5f5f5", - tip:"The time taken to advance the iterator to the next document." - }, - { - key:"advance_count", - time:0, - relative:0, - color:"#f5f5f5", - tip:"" - } - ] - } -]; diff --git a/x-pack/legacy/plugins/searchprofiler/public/directives/profile_tree/__tests__/fixtures/normalize_indices.js b/x-pack/legacy/plugins/searchprofiler/public/directives/profile_tree/__tests__/fixtures/normalize_indices.js deleted file mode 100644 index fee12c17e7d580..00000000000000 --- a/x-pack/legacy/plugins/searchprofiler/public/directives/profile_tree/__tests__/fixtures/normalize_indices.js +++ /dev/null @@ -1,4 +0,0 @@ -/*eslint-disable */ -export const inputIndices = JSON.parse('{"test":{"shards":[{"id":["F-R7QxH4S42fMnPfmFUKMQ","test","0"],"searches":[{"query":null,"rewrite_time":2656,"collector":[{"name":"MultiCollector","reason":"search_multi","time":"0.1815780000ms","children":[{"name":"SimpleTopScoreDocCollector","reason":"search_top_hits","time":"0.02393700000ms"},{"name":"ProfilingAggregator: [org.elasticsearch.search.profile.aggregation.ProfilingAggregator@43c8a536]","reason":"aggregation","time":"0.1140000000ms"}]}],"flat":[{"id":"af697413-f76b-458e-b265-f4930dbdee2a","childrenIds":[],"lucene":"name:george","time":0.219343,"selfTime":0.219343,"timePercentage":"100.00","query_type":"TermQuery","absoluteColor":"#ffafaf","depth":0,"breakdown":[{"key":"create_weight","time":160673,"relative":"73.3","color":"#fcc2c2","tip":"The time taken to create the Weight object, which holds temporary information during scoring."},{"key":"build_scorer","time":50157,"relative":"22.9","color":"#f7e5e5","tip":"The time taken to create the Scoring object, which is later used to execute the actual scoring of each doc."},{"key":"score","time":5783,"relative":"2.6","color":"#f5f3f3","tip":"The time taken in actually scoring the document against the query."},{"key":"next_doc","time":2718,"relative":"1.2","color":"#f5f4f4","tip":"The time taken to advance the iterator to the next matching document."},{"key":"build_scorer_count","time":5,"relative":0,"color":"#f5f5f5","tip":""},{"key":"next_doc_count","time":4,"relative":0,"color":"#f5f5f5","tip":""},{"key":"score_count","time":2,"relative":0,"color":"#f5f5f5","tip":""},{"key":"create_weight_count","time":1,"relative":0,"color":"#f5f5f5","tip":""},{"key":"match","time":0,"relative":"0.0","color":"#f5f5f5","tip":"The time taken to execute a secondary, more precise scoring phase (used by phrase queries)."},{"key":"match_count","time":0,"relative":0,"color":"#f5f5f5","tip":""},{"key":"advance","time":0,"relative":"0.0","color":"#f5f5f5","tip":"The time taken to advance the iterator to the next document."},{"key":"advance_count","time":0,"relative":0,"color":"#f5f5f5","tip":""}]}]}],"aggregations":[{"type":"org.elasticsearch.search.aggregations.metrics.stats.StatsAggregator","description":"stats","time":"0.03053500000ms","breakdown":{"reduce":0,"build_aggregation":9447,"build_aggregation_count":1,"initialize":5589,"initialize_count":1,"reduce_count":0,"collect":15495,"collect_count":2}}],"time":{"searches":0.219343,"aggregations":0},"color":{"searches":0,"aggregations":0},"relative":{"searches":0,"aggregations":0},"rewrite_time":2656}],"time":{"searches":0.219343,"aggregations":0},"name":"test"}}'); - -export const normalizedIndices = JSON.parse('[{"shards":[{"id":["F-R7QxH4S42fMnPfmFUKMQ","test","0"],"searches":[{"query":null,"rewrite_time":2656,"collector":[{"name":"MultiCollector","reason":"search_multi","time":"0.1815780000ms","children":[{"name":"SimpleTopScoreDocCollector","reason":"search_top_hits","time":"0.02393700000ms"},{"name":"ProfilingAggregator: [org.elasticsearch.search.profile.aggregation.ProfilingAggregator@43c8a536]","reason":"aggregation","time":"0.1140000000ms"}]}],"flat":[{"id":"af697413-f76b-458e-b265-f4930dbdee2a","childrenIds":[],"lucene":"name:george","time":0.219343,"selfTime":0.219343,"timePercentage":"100.00","query_type":"TermQuery","absoluteColor":"#ffafaf","depth":0,"breakdown":[{"key":"create_weight","time":160673,"relative":"73.3","color":"#fcc2c2","tip":"The time taken to create the Weight object, which holds temporary information during scoring."},{"key":"build_scorer","time":50157,"relative":"22.9","color":"#f7e5e5","tip":"The time taken to create the Scoring object, which is later used to execute the actual scoring of each doc."},{"key":"score","time":5783,"relative":"2.6","color":"#f5f3f3","tip":"The time taken in actually scoring the document against the query."},{"key":"next_doc","time":2718,"relative":"1.2","color":"#f5f4f4","tip":"The time taken to advance the iterator to the next matching document."},{"key":"build_scorer_count","time":5,"relative":0,"color":"#f5f5f5","tip":""},{"key":"next_doc_count","time":4,"relative":0,"color":"#f5f5f5","tip":""},{"key":"score_count","time":2,"relative":0,"color":"#f5f5f5","tip":""},{"key":"create_weight_count","time":1,"relative":0,"color":"#f5f5f5","tip":""},{"key":"match","time":0,"relative":"0.0","color":"#f5f5f5","tip":"The time taken to execute a secondary, more precise scoring phase (used by phrase queries)."},{"key":"match_count","time":0,"relative":0,"color":"#f5f5f5","tip":""},{"key":"advance","time":0,"relative":"0.0","color":"#f5f5f5","tip":"The time taken to advance the iterator to the next document."},{"key":"advance_count","time":0,"relative":0,"color":"#f5f5f5","tip":""}]}]}],"aggregations":[{"type":"org.elasticsearch.search.aggregations.metrics.stats.StatsAggregator","description":"stats","time":"0.03053500000ms","breakdown":{"reduce":0,"build_aggregation":9447,"build_aggregation_count":1,"initialize":5589,"initialize_count":1,"reduce_count":0,"collect":15495,"collect_count":2}}],"time":{"searches":0.219343,"aggregations":0},"color":{"searches":"#ffafaf","aggregations":0},"relative":{"searches":"100.00","aggregations":0},"rewrite_time":2656}],"time":{"searches":0.219343,"aggregations":0},"name":"test"}]'); diff --git a/x-pack/legacy/plugins/searchprofiler/public/directives/profile_tree/index.html b/x-pack/legacy/plugins/searchprofiler/public/directives/profile_tree/index.html deleted file mode 100644 index e6994a07aa6d16..00000000000000 --- a/x-pack/legacy/plugins/searchprofiler/public/directives/profile_tree/index.html +++ /dev/null @@ -1,115 +0,0 @@ -
-
-
- - {{:: 'xpack.searchProfiler.profileTree.cumulativeTimeTitle' | i18n: { defaultMessage: "Cumulative Time:" } }} {{ index.time[target] | msToPretty: 3 }} - -
-
-

- - {{index.name}} -

-
-
- -
- -
-
-
-
- - - {{shard.time[target] | msToPretty: 3}} - -
- -
- -
- -
-
-
-
-
-
- -
- -
-
- -
-
- - {{row.selfTime | msToPretty: 1}} - -
-
- - {{row.time | msToPretty: 1}} - -
-
- - - {{row.timePercentage}}% - -
-
- -
- - - {{row.lucene | limitTo : 120}}{{row.lucene.length > 120 ? '...' : ''}} - - - -
-
-
-
-
-
-
diff --git a/x-pack/legacy/plugins/searchprofiler/public/directives/profile_tree/index.js b/x-pack/legacy/plugins/searchprofiler/public/directives/profile_tree/index.js deleted file mode 100644 index b212439e98394a..00000000000000 --- a/x-pack/legacy/plugins/searchprofiler/public/directives/profile_tree/index.js +++ /dev/null @@ -1,172 +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 template from 'plugins/searchprofiler/directives/profile_tree/index.html'; -import { - closeNode, - normalizeIndices, - calcTimes, - normalizeTimes, - flattenResults -} from 'plugins/searchprofiler/directives/profile_tree/util'; -import { uiModules } from 'ui/modules'; - -const uiModule = uiModules.get('app/searchprofiler/directives', []); -uiModule.directive('profiletree', HighlightService => { - return { - restrict: 'E', - scope: { - data: '=', - target: '@' - }, - template: template, - link: $scope => { - $scope.visible = { - 'foo': {} - }; - $scope.indexVisibility = {}; - $scope.highlightedRow = null; - - $scope.updateDetail = (row, indexName, shardID, shardNumber) => { - HighlightService.details = row; - HighlightService.details.indexName = indexName; - HighlightService.details.shardID = shardID; - HighlightService.details.shardNumber = shardNumber; - HighlightService.details.highlightedRow = row.id; - }; - - $scope.getHighlightedRow = () => { - if (HighlightService.details) { - return HighlightService.details.highlightedRow; - } - return null; - }; - - $scope.toggle = id => { - // If the branch is open and toggled close, we need to - // also close the children - if ($scope.visible[id].visible === true) { - closeNode($scope.visible, id); - } else { - // Otherwise just toggle on - $scope.visible[id].visible = true; - } - }; - - function render(data) { - if (data.length === 0) { - return; - } - - $scope.visible = {}; - let indices = {}; - - for (const shard of data) { - initShardTargets(shard); - - if ($scope.target === 'searches') { - shard.time[$scope.target] = collectSearchTimes(shard); - } else if ($scope.target === 'aggregations') { - shard.time[$scope.target] = collectAggTimes(shard); - } - if (!indices[shard.id[1]]) { - indices[shard.id[1]] = { - shards: [], - time: { - searches: 0, - aggregations: 0 - }, - name: shard.id[1] - }; - } - indices[shard.id[1]].shards.push(shard); - indices[shard.id[1]].time[$scope.target] += shard.time[$scope.target]; - } - data = null; - const finalIndices = normalizeIndices(indices, $scope.indexVisibility, $scope.target); - indices = null; - - $scope.profileResponse = finalIndices; - } - - function collectSearchTimes(shard) { - if (shard.searches == null) { - return 0; - } - shard.rewrite_time = 0; - - let shardTime = 0; - for (const search of shard.searches) { - shard.rewrite_time += search.rewrite_time; - const totalTime = calcTimes(search.query); - shardTime += totalTime; - normalizeTimes(search.query, totalTime, 0); - - const flat = []; - flattenResults(search.query, flat, 0, $scope.visible); - search.flat = flat; - search.query = null; - } - return shardTime; - } - - function collectAggTimes(shard) { - if (shard.aggregations == null) { - return 0; - } - let shardTime = 0; - for (const agg of shard.aggregations) { - const totalTime = calcTimes([agg]); - shardTime += totalTime; - } - for (const agg of shard.aggregations) { - normalizeTimes([agg], shardTime, 0); - - const flat = []; - flattenResults([agg], flat, 0, $scope.visible); - agg.flat = flat; - } - return shardTime; - } - - // TODO the addition of aggregation profiling made the mutability of - // `shards` a liability. Previously we set things directly on the shards - // tree because it was the only source of data. Now we have agg data, - // so final, accumulated stats need to be saved on a per-target basis - // - // In the future, we should really remove this setup and create two immutable - // result sets that are generated from a single (also immutable) input set of - // `shards` data - // - // Particularly important if/when we add a third target - function initShardTargets(shard) { - if (!shard.time) { - shard.time = { - searches: 0, - aggregations: 0 - }; - } - - if (!shard.color) { - shard.color = { - searches: 0, - aggregations: 0 - }; - } - - if (!shard.relative) { - shard.relative = { - searches: 0, - aggregations: 0 - }; - } - } - - $scope.$watch('data', render); - - } - }; -}); diff --git a/x-pack/legacy/plugins/searchprofiler/public/directives/profile_tree/util.js b/x-pack/legacy/plugins/searchprofiler/public/directives/profile_tree/util.js deleted file mode 100644 index 6190d299dcf10d..00000000000000 --- a/x-pack/legacy/plugins/searchprofiler/public/directives/profile_tree/util.js +++ /dev/null @@ -1,219 +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 uuid from 'uuid'; -import tinycolor from 'tinycolor2'; -import _ from 'lodash'; - -const comparator = (v1, v2) => { - if (v1 < v2) { - return 1; - } - return (v1 > v2) ? -1 : 0; -}; - -function getToolTip(key) { - switch (key) { - case 'build_scorer': - return 'The time taken to create the Scoring object, which is later used to execute the actual scoring of each doc.'; - case 'create_weight': - return 'The time taken to create the Weight object, which holds temporary information during scoring.'; - case 'next_doc': - return 'The time taken to advance the iterator to the next matching document.'; - case 'score': - return 'The time taken in actually scoring the document against the query.'; - case 'match': - return 'The time taken to execute a secondary, more precise scoring phase (used by phrase queries).'; - case 'advance': - return 'The time taken to advance the iterator to the next document.'; - default: - return ''; - } -} - -export function timeInMilliseconds(data) { - if (data.time_in_nanos) { - return data.time_in_nanos / 1000000; - } - - if (typeof data.time === 'string') { - return data.time.replace('ms', ''); - } - - return data.time; -} - -export function calcTimes(data, parentId) { - if (data == null) { - return; - } - - let totalTime = 0; - //First pass to collect total - for (const child of data) { - totalTime += timeInMilliseconds(child); - - child.id = uuid.v4(); - child.parentId = parentId; - child.childrenIds = []; - child.breakdown = normalizeBreakdown(child.breakdown); - - let childrenTime = 0; - if (child.children != null && child.children.length !== 0) { - childrenTime = calcTimes(child.children, child.id); - child.hasChildren = true; - - // Save the IDs of our children, has to be called after calcTimes recursion above - for (const c of child.children) { - child.childrenIds.push(c.id); - } - } - child.selfTime = (timeInMilliseconds(child) - childrenTime); - } - return totalTime; -} - -export function normalizeBreakdown(breakdown) { - const final = []; - const total = Object.keys(breakdown).reduce((partialTotal, currentKey) => { - if (currentKey.indexOf('_count') === -1) { - partialTotal += breakdown[currentKey]; - } - return partialTotal; - }, 0); - Object.keys(breakdown).sort().forEach(key => { - let relative = 0; - if (key.indexOf('_count') === -1) { - relative = ((breakdown[key] / total) * 100).toFixed(1); - } - final.push({ - key: key, - time: breakdown[key], - relative: relative, - color: tinycolor.mix('#F5F5F5', '#FFAFAF', relative).toHexString(), - tip: getToolTip(key) - }); - }); - - // Sort by time descending and then key ascending - return final.sort((a, b) => { - if (comparator(a.time, b.time) !== 0) { - return comparator(a.time, b.time); - } - - return -1 * comparator(a.key, b.key); - }); -} - -export function normalizeTimes(data, totalTime, depth) { - //Second pass to normalize - for (const child of data) { - child.timePercentage = ((timeInMilliseconds(child) / totalTime) * 100).toFixed(2); - child.absoluteColor = tinycolor.mix('#F5F5F5', '#FFAFAF', child.timePercentage).toHexString(); - child.depth = depth; - - if (child.children != null && child.children.length !== 0) { - normalizeTimes(child.children, totalTime, depth + 1); - } - } - - data.sort((a, b) => comparator(timeInMilliseconds(a), timeInMilliseconds(b))); -} - -export function normalizeIndices(indices, visibility, target) { - // Sort the shards per-index - let sortQueryComponents; - if (target === 'searches') { - sortQueryComponents = (a, b) => { - const aTime = _.sum(a.searches, (search) => { - return search.flat[0].time; - }); - const bTime = _.sum(b.searches, (search) => { - return search.flat[0].time; - }); - - return comparator(aTime, bTime); - }; - } else if (target === 'aggregations') { - sortQueryComponents = (a, b) => { - const aTime = _.sum(a.aggregations, (agg) => { - return agg.flat[0].time; - }); - const bTime = _.sum(b.aggregations, (agg) => { - return agg.flat[0].time; - }); - - return comparator(aTime, bTime); - }; - } - const sortedIndices = []; - for (const [key, index] of Object.entries(indices)) { - index.shards.sort(sortQueryComponents); - for (const shard of index.shards) { - shard.relative[target] = ((shard.time[target] / index.time[target]) * 100).toFixed(2); - shard.color[target] = tinycolor.mix('#F5F5F5', '#FFAFAF', shard.relative[target]).toHexString(); - } - sortedIndices.push(index); - visibility[key] = false; - } - - // And now sort the indices themselves - sortedIndices.sort((a, b) => comparator(a.time, b.time)); - return sortedIndices; -} - -export function flattenResults(data, accumulator, depth, visibleMap) { - if (data == null) { - return; - } - - for (const child of data) { - - // For bwc of older profile responses - if (!child.description) { - child.description = child.lucene; - child.lucene = null; - - child.type = child.query_type; - child.query_type = null; - } - accumulator.push({ - id: child.id, - parentId: child.parentId, - childrenIds: child.childrenIds, - lucene: child.description, - time: timeInMilliseconds(child), - selfTime: child.selfTime, - timePercentage: child.timePercentage, - query_type: child.type.split('.').pop(), - absoluteColor: child.absoluteColor, - depth: depth, - hasChildren: child.hasChildren, - breakdown: child.breakdown - }); - - visibleMap[child.id] = { - visible: child.timePercentage > 20, - children: child.children - }; - - if (child.children != null && child.children.length !== 0) { - flattenResults(child.children, accumulator, depth + 1, visibleMap); - } - } -} - -export function closeNode(visibleMap, id) { - visibleMap[id].visible = false; - - if (visibleMap[id].children == null || visibleMap[id].children.length === 0) { - return; - } - - for (const child of visibleMap[id].children) { - closeNode(visibleMap, child.id); - } -} diff --git a/x-pack/legacy/plugins/searchprofiler/public/legacy.ts b/x-pack/legacy/plugins/searchprofiler/public/legacy.ts new file mode 100644 index 00000000000000..61dabe8ac7b05c --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/legacy.ts @@ -0,0 +1,65 @@ +/* + * 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. + */ + +/* eslint-disable @kbn/eslint/no-restricted-paths */ +import { npSetup } from 'ui/new_platform'; +import { I18nContext } from 'ui/i18n'; +import uiRoutes from 'ui/routes'; +import 'ui/capabilities/route_setup'; +// @ts-ignore +import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; +// @ts-ignore +import { formatAngularHttpError } from 'ui/notify/lib'; +import 'ui/autoload/all'; +/* eslint-enable @kbn/eslint/no-restricted-paths */ + +import { ApplicationSetup } from 'src/core/public'; +import { plugin } from './np_ready'; + +const pluginInstance = plugin({} as any); + +const template = ` +
+`; + +uiRoutes.when('/dev_tools/searchprofiler', { + template, + requireUICapability: 'dev_tools.show', + controller: $scope => { + $scope.startReactApp = () => { + const el = document.querySelector('#searchProfilerAppRoot'); + if (!el) { + const errorMessage = 'Could not mount Searchprofiler App!'; + npSetup.core.fatalErrors.add(errorMessage); + throw new Error(errorMessage); + } + + const coreApplicationSetupShim: ApplicationSetup = { + register(app: any) { + const unmount = app.mount(); + $scope.$on('$destroy', () => unmount()); + }, + registerMountContext: {} as any, + }; + + pluginInstance.setup( + { + ...npSetup.core, + application: coreApplicationSetupShim, + }, + { + __LEGACY: { + I18nContext, + licenseEnabled: xpackInfo.get('features.searchprofiler.enableAppLink'), + notifications: npSetup.core.notifications.toasts, + formatAngularHttpError, + el, + }, + } + ); + }; + }, +}); diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/boot.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/boot.tsx new file mode 100644 index 00000000000000..fa02124f8a2454 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/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, ToastsSetup } from 'src/core/public'; +import { App } from '.'; + +export interface Dependencies { + el: HTMLElement; + http: Http; + licenseEnabled: boolean; + I18nContext: any; + notifications: ToastsSetup; + 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/public/np_ready/application/components/highlight_details_flyout/highlight_details_flyout.test.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/highlight_details_flyout/highlight_details_flyout.test.tsx new file mode 100644 index 00000000000000..cf8052cbf35806 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/highlight_details_flyout/highlight_details_flyout.test.tsx @@ -0,0 +1,63 @@ +/* + * 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 { registerTestBed } from '../../../../../../../../test_utils'; +import { HighlightDetailsFlyout, Props } from '.'; + +describe('Highlight Details Flyout', () => { + it('renders', async () => { + const props: Props = { + onClose: () => {}, + shard: { + aggregations: [], + id: ['test', 'test', 'test'], + searches: [], + color: '#fff', + time: 123, + relative: 100, + }, + operation: { + parent: null, + breakdown: [ + { + color: 'test', + key: 'test', + relative: 100, + tip: 'test', + time: 100, + }, + { + color: 'test', + key: 'test', + relative: 100, + tip: 'test', + time: 100, + }, + { + color: 'test', + key: 'test', + relative: 100, + tip: 'test', + time: 100, + }, + ], + lucene: 'test', + query_type: 'test', + selfTime: 100, + time: 100, + children: [], + timePercentage: 100, + hasChildren: false, + visible: true, + absoluteColor: '123', + }, + indexName: 'test', + }; + + const init = registerTestBed(HighlightDetailsFlyout); + await init(props); + }); +}); diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/highlight_details_flyout/highlight_details_flyout.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/highlight_details_flyout/highlight_details_flyout.tsx new file mode 100644 index 00000000000000..6ba39d15b83411 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/highlight_details_flyout/highlight_details_flyout.tsx @@ -0,0 +1,124 @@ +/* + * 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 FlyoutEntry = ({ + 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.selfTimeTitle', { + defaultMessage: 'Self time', + })}{' '} + + + } + body={msToPretty(operation.selfTime || 0, 3)} + /> + {/* Breakdown Table Entry */} + } + /> +
+
+
+
+ ); +}; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/highlight_details_flyout/highlight_details_table.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/highlight_details_flyout/highlight_details_table.tsx new file mode 100644 index 00000000000000..4bfa7365de1efa --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/highlight_details_flyout/highlight_details_table.tsx @@ -0,0 +1,45 @@ +/* + * 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 { EuiBasicTable, EuiToolTip, EuiBadge } from '@elastic/eui'; + +import { BreakdownItem } from '../../types'; +import { nsToPretty } from '../../utils'; +import { PercentageBadge } from '../percentage_badge'; + +interface Props { + breakdown: BreakdownItem[]; +} + +export const HighlightDetailsTable = ({ breakdown }: Props) => { + const columns = [ + { + name: 'Description', + render: (item: BreakdownItem) => ( + + {item.key} + + ), + }, + { + name: 'Time', + render: (item: BreakdownItem) => ( + + {nsToPretty(item.time, 1)} + + ), + }, + { + name: 'Percentage', + render: (item: BreakdownItem) => ( + + ), + }, + ]; + + return ; +}; diff --git a/x-pack/legacy/plugins/searchprofiler/public/directives/index.js b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/highlight_details_flyout/index.ts similarity index 67% rename from x-pack/legacy/plugins/searchprofiler/public/directives/index.js rename to x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/highlight_details_flyout/index.ts index 4bd4146a1a0fe3..36ae010fabfa48 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/directives/index.js +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/highlight_details_flyout/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import 'plugins/searchprofiler/directives/profile_tree'; -import 'plugins/searchprofiler/directives/highlight_details'; +export { HighlightDetailsFlyout, Props } from './highlight_details_flyout'; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/index.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/index.ts new file mode 100644 index 00000000000000..db1fea0033db5c --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { SearchProfilerTabs } from './searchprofiler_tabs'; +export { LicenseWarningNotice } from './license_warning_notice'; +export { ProfileTree, OnHighlightChangeArgs } from './profile_tree'; +export { HighlightDetailsFlyout } from './highlight_details_flyout'; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/license_warning_notice.test.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/license_warning_notice.test.ts new file mode 100644 index 00000000000000..ebe7a00737868f --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/license_warning_notice.test.ts @@ -0,0 +1,16 @@ +/* + * 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 { registerTestBed } from '../../../../../../../test_utils'; + +import { LicenseWarningNotice } from './license_warning_notice'; + +describe('License Error Notice', () => { + it('renders', async () => { + const init = registerTestBed(LicenseWarningNotice); + await init({}); + }); +}); diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/license_warning_notice.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/license_warning_notice.tsx new file mode 100644 index 00000000000000..da9991529e7d44 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/license_warning_notice.tsx @@ -0,0 +1,76 @@ +/* + * 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, EuiLink, EuiCode } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +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 ( +
+ + +

+ + {trialLicense}, {basicLicense},{' '} + {goldLicense} + + ), + platinumLicenseType: {platinumLicense}, + }} + /> +

+

+ + {registerLicenseLinkLabel} + + ), + }} + /> +

+
+
+
+ ); +}; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/percentage_badge.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/percentage_badge.tsx new file mode 100644 index 00000000000000..4b53b2e3c18c56 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/percentage_badge.tsx @@ -0,0 +1,37 @@ +/* + * 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 { EuiBadge } from '@elastic/eui'; +import classNames from 'classnames'; + +interface Props { + timePercentage: number; + label: string; + valueType?: 'percent' | 'time'; +} + +/** + * This component has IE specific provision for rendering the percentage portion of the badge correctly. + * + * This component uses CSS vars injected against the DOM element and resolves this in CSS to calculate + * how far the percent bar should be drawn. + */ +export const PercentageBadge = ({ timePercentage, label, valueType = 'percent' }: Props) => { + return ( + + + {label} + + ); +}; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/fixtures/breakdown.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/fixtures/breakdown.ts new file mode 100644 index 00000000000000..a65af9a7a3ff3e --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/fixtures/breakdown.ts @@ -0,0 +1,68 @@ +export const breakdown = { + advance: 0, + advance_count: 0, + build_scorer: 6273, + build_scorer_count: 2, + create_weight: 1852, + create_weight_count: 1, + match: 0, + match_count: 0, + next_doc: 2593093, + next_doc_count: 27958, + score: 2525502, + score_count: 27948, +}; + +export const normalized = [ + { + key: 'next_doc', + time: 2593093, + relative: '50.6', + color: '#fad2d2', + tip: 'The time taken to advance the iterator to the next matching document.', + }, + { + key: 'score', + time: 2525502, + relative: '49.3', + color: '#fad2d2', + tip: 'The time taken in actually scoring the document against the query.', + }, + { key: 'next_doc_count', time: 27958, relative: 0, color: '#f5f5f5', tip: '' }, + { key: 'score_count', time: 27948, relative: 0, color: '#f5f5f5', tip: '' }, + { + key: 'build_scorer', + time: 6273, + relative: '0.1', + color: '#f5f5f5', + tip: + 'The time taken to create the Scoring object, which is later used to execute the actual scoring of each doc.', + }, + { + key: 'create_weight', + time: 1852, + relative: '0.0', + color: '#f5f5f5', + tip: + 'The time taken to create the Weight object, which holds temporary information during scoring.', + }, + { key: 'build_scorer_count', time: 2, relative: 0, color: '#f5f5f5', tip: '' }, + { key: 'create_weight_count', time: 1, relative: 0, color: '#f5f5f5', tip: '' }, + { + key: 'advance', + time: 0, + relative: '0.0', + color: '#f5f5f5', + tip: 'The time taken to advance the iterator to the next document.', + }, + { key: 'advance_count', time: 0, relative: 0, color: '#f5f5f5', tip: '' }, + { + key: 'match', + time: 0, + relative: '0.0', + color: '#f5f5f5', + tip: + 'The time taken to execute a secondary, more precise scoring phase (used by phrase queries).', + }, + { key: 'match_count', time: 0, relative: 0, color: '#f5f5f5', tip: '' }, +]; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/fixtures/normalize_indices.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/fixtures/normalize_indices.ts new file mode 100644 index 00000000000000..bd8e44d82379ba --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/fixtures/normalize_indices.ts @@ -0,0 +1,8 @@ +/*eslint-disable */ +export const inputIndices = JSON.parse( + '{"test":{"shards":[{"id":["F-R7QxH4S42fMnPfmFUKMQ","test","0"],"searches":[{"query":null,"rewrite_time":2656,"collector":[{"name":"MultiCollector","reason":"search_multi","time":"0.1815780000ms","children":[{"name":"SimpleTopScoreDocCollector","reason":"search_top_hits","time":"0.02393700000ms"},{"name":"ProfilingAggregator: [org.elasticsearch.search.profile.aggregation.ProfilingAggregator@43c8a536]","reason":"aggregation","time":"0.1140000000ms"}]}],"flat":[{"id":"af697413-f76b-458e-b265-f4930dbdee2a","childrenIds":[],"lucene":"name:george","time":0.219343,"selfTime":0.219343,"timePercentage":"100.00","query_type":"TermQuery","absoluteColor":"#ffafaf","depth":0,"breakdown":[{"key":"create_weight","time":160673,"relative":"73.3","color":"#fcc2c2","tip":"The time taken to create the Weight object, which holds temporary information during scoring."},{"key":"build_scorer","time":50157,"relative":"22.9","color":"#f7e5e5","tip":"The time taken to create the Scoring object, which is later used to execute the actual scoring of each doc."},{"key":"score","time":5783,"relative":"2.6","color":"#f5f3f3","tip":"The time taken in actually scoring the document against the query."},{"key":"next_doc","time":2718,"relative":"1.2","color":"#f5f4f4","tip":"The time taken to advance the iterator to the next matching document."},{"key":"build_scorer_count","time":5,"relative":0,"color":"#f5f5f5","tip":""},{"key":"next_doc_count","time":4,"relative":0,"color":"#f5f5f5","tip":""},{"key":"score_count","time":2,"relative":0,"color":"#f5f5f5","tip":""},{"key":"create_weight_count","time":1,"relative":0,"color":"#f5f5f5","tip":""},{"key":"match","time":0,"relative":"0.0","color":"#f5f5f5","tip":"The time taken to execute a secondary, more precise scoring phase (used by phrase queries)."},{"key":"match_count","time":0,"relative":0,"color":"#f5f5f5","tip":""},{"key":"advance","time":0,"relative":"0.0","color":"#f5f5f5","tip":"The time taken to advance the iterator to the next document."},{"key":"advance_count","time":0,"relative":0,"color":"#f5f5f5","tip":""}]}]}],"aggregations":[{"type":"org.elasticsearch.search.aggregations.metrics.stats.StatsAggregator","description":"stats","time":"0.03053500000ms","breakdown":{"reduce":0,"build_aggregation":9447,"build_aggregation_count":1,"initialize":5589,"initialize_count":1,"reduce_count":0,"collect":15495,"collect_count":2}}],"time":0.219343,"color":0,"relative":0,"rewrite_time":2656}],"time":0.219343,"name":"test"}}' +); + +export const normalizedIndices = JSON.parse( + '[{"shards":[{"id":["F-R7QxH4S42fMnPfmFUKMQ","test","0"],"searches":[{"query":null,"rewrite_time":2656,"collector":[{"name":"MultiCollector","reason":"search_multi","time":"0.1815780000ms","children":[{"name":"SimpleTopScoreDocCollector","reason":"search_top_hits","time":"0.02393700000ms"},{"name":"ProfilingAggregator: [org.elasticsearch.search.profile.aggregation.ProfilingAggregator@43c8a536]","reason":"aggregation","time":"0.1140000000ms"}]}],"flat":[{"id":"af697413-f76b-458e-b265-f4930dbdee2a","childrenIds":[],"lucene":"name:george","time":0.219343,"selfTime":0.219343,"timePercentage":"100.00","query_type":"TermQuery","absoluteColor":"#ffafaf","depth":0,"breakdown":[{"key":"create_weight","time":160673,"relative":"73.3","color":"#fcc2c2","tip":"The time taken to create the Weight object, which holds temporary information during scoring."},{"key":"build_scorer","time":50157,"relative":"22.9","color":"#f7e5e5","tip":"The time taken to create the Scoring object, which is later used to execute the actual scoring of each doc."},{"key":"score","time":5783,"relative":"2.6","color":"#f5f3f3","tip":"The time taken in actually scoring the document against the query."},{"key":"next_doc","time":2718,"relative":"1.2","color":"#f5f4f4","tip":"The time taken to advance the iterator to the next matching document."},{"key":"build_scorer_count","time":5,"relative":0,"color":"#f5f5f5","tip":""},{"key":"next_doc_count","time":4,"relative":0,"color":"#f5f5f5","tip":""},{"key":"score_count","time":2,"relative":0,"color":"#f5f5f5","tip":""},{"key":"create_weight_count","time":1,"relative":0,"color":"#f5f5f5","tip":""},{"key":"match","time":0,"relative":"0.0","color":"#f5f5f5","tip":"The time taken to execute a secondary, more precise scoring phase (used by phrase queries)."},{"key":"match_count","time":0,"relative":0,"color":"#f5f5f5","tip":""},{"key":"advance","time":0,"relative":"0.0","color":"#f5f5f5","tip":"The time taken to advance the iterator to the next document."},{"key":"advance_count","time":0,"relative":0,"color":"#f5f5f5","tip":""}]}]}],"aggregations":[{"type":"org.elasticsearch.search.aggregations.metrics.stats.StatsAggregator","description":"stats","time":"0.03053500000ms","breakdown":{"reduce":0,"build_aggregation":9447,"build_aggregation_count":1,"initialize":5589,"initialize_count":1,"reduce_count":0,"collect":15495,"collect_count":2}}],"time":0.219343,"color":"#ffafaf","relative":"100.00","rewrite_time":2656}],"time":0.219343,"name":"test"}]' +); diff --git a/x-pack/legacy/plugins/searchprofiler/public/directives/profile_tree/__tests__/fixtures/normalize_times.js b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/fixtures/normalize_times.ts similarity index 100% rename from x-pack/legacy/plugins/searchprofiler/public/directives/profile_tree/__tests__/fixtures/normalize_times.js rename to x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/fixtures/normalize_times.ts diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/fixtures/processed_search_response.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/fixtures/processed_search_response.ts new file mode 100644 index 00000000000000..ede36ba2b01699 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/fixtures/processed_search_response.ts @@ -0,0 +1,344 @@ +/* + * 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 { produce } from 'immer'; + +const shard1 = { + id: ['L22w_FX2SbqlQYOP5QrYDg', '.kibana_1', '0'], + searches: [], + aggregations: [], + time: 0.058419, + color: '#ffafaf', + relative: '100.00', + rewrite_time: 14670, +}; + +const searchRoot = { + query: null, + rewrite_time: 14670, + collector: [ + { + name: 'CancellableCollector', + reason: 'search_cancelled', + time_in_nanos: 14706, + children: [ + { + name: 'SimpleTopScoreDocCollector', + reason: 'search_top_hits', + time_in_nanos: 7851, + }, + ], + }, + ], + treeRoot: null, +}; + +const search1 = { + type: 'ConstantScoreQuery', + description: 'ConstantScore(DocValuesFieldExistsQuery [field=_primary_term])', + time_in_nanos: 58419, + breakdown: [ + { + key: 'build_scorer', + time: 40061, + relative: '68.7', + color: '#fcc5c5', + tip: + 'The time taken to create the Scoring object, which is later used to execute the actual scoring of each doc.', + }, + { + key: 'create_weight', + time: 8238, + relative: '14.1', + color: '#f6ebeb', + tip: + 'The time taken to create the Weight object, which holds temporary information during scoring.', + }, + { + key: 'next_doc', + time: 5767, + relative: '9.9', + color: '#f6eeee', + tip: 'The time taken to advance the iterator to the next matching document.', + }, + { + key: 'advance', + time: 2849, + relative: '4.9', + color: '#f5f2f2', + tip: 'The time taken to advance the iterator to the next document.', + }, + { + key: 'score', + time: 1431, + relative: '2.5', + color: '#f5f3f3', + tip: 'The time taken in actually scoring the document against the query.', + }, + { + key: 'next_doc_count', + time: 24, + relative: 0, + color: '#f5f5f5', + tip: '', + }, + { + key: 'score_count', + time: 24, + relative: 0, + color: '#f5f5f5', + tip: '', + }, + { + key: 'build_scorer_count', + time: 16, + relative: 0, + color: '#f5f5f5', + tip: '', + }, + { + key: 'advance_count', + time: 8, + relative: 0, + color: '#f5f5f5', + tip: '', + }, + { + key: 'create_weight_count', + time: 1, + relative: 0, + color: '#f5f5f5', + tip: '', + }, + { + key: 'compute_max_score', + time: 0, + relative: '0.0', + color: '#f5f5f5', + tip: '', + }, + { + key: 'compute_max_score_count', + time: 0, + relative: 0, + color: '#f5f5f5', + tip: '', + }, + { + key: 'match', + time: 0, + relative: '0.0', + color: '#f5f5f5', + tip: + 'The time taken to execute a secondary, more precise scoring phase (used by phrase queries).', + }, + { + key: 'match_count', + time: 0, + relative: 0, + color: '#f5f5f5', + tip: '', + }, + { + key: 'set_min_competitive_score', + time: 0, + relative: '0.0', + color: '#f5f5f5', + tip: '', + }, + { + key: 'set_min_competitive_score_count', + time: 0, + relative: 0, + color: '#f5f5f5', + tip: '', + }, + { + key: 'shallow_advance', + time: 0, + relative: '0.0', + color: '#f5f5f5', + tip: '', + }, + { + key: 'shallow_advance_count', + time: 0, + relative: 0, + color: '#f5f5f5', + tip: '', + }, + ], + children: [ + /* See search1Child */ + ], + hasChildren: true, + selfTime: 0.028784999999999998, + timePercentage: '100.00', + absoluteColor: '#ffafaf', + depth: 0, + parent: null, + time: 0.058419, + lucene: 'ConstantScore(DocValuesFieldExistsQuery [field=_primary_term])', + query_type: 'ConstantScoreQuery', + visible: true, +}; + +const search1Child = { + type: 'DocValuesFieldExistsQuery', + description: 'DocValuesFieldExistsQuery [field=_primary_term]', + time_in_nanos: 29634, + breakdown: [ + { + key: 'build_scorer', + time: 24059, + relative: '81.3', + color: '#fdbcbc', + tip: + 'The time taken to create the Scoring object, which is later used to execute the actual scoring of each doc.', + }, + { + key: 'next_doc', + time: 2434, + relative: '8.2', + color: '#f6efef', + tip: 'The time taken to advance the iterator to the next matching document.', + }, + { + key: 'create_weight', + time: 1586, + relative: '5.4', + color: '#f6f1f1', + tip: + 'The time taken to create the Weight object, which holds temporary information during scoring.', + }, + { + key: 'advance', + time: 1506, + relative: '5.1', + color: '#f6f1f1', + tip: 'The time taken to advance the iterator to the next document.', + }, + { + key: 'next_doc_count', + time: 24, + relative: 0, + color: '#f5f5f5', + tip: '', + }, + { + key: 'build_scorer_count', + time: 16, + relative: 0, + color: '#f5f5f5', + tip: '', + }, + { + key: 'advance_count', + time: 8, + relative: 0, + color: '#f5f5f5', + tip: '', + }, + { + key: 'create_weight_count', + time: 1, + relative: 0, + color: '#f5f5f5', + tip: '', + }, + { + key: 'compute_max_score', + time: 0, + relative: '0.0', + color: '#f5f5f5', + tip: '', + }, + { + key: 'compute_max_score_count', + time: 0, + relative: 0, + color: '#f5f5f5', + tip: '', + }, + { + key: 'match', + time: 0, + relative: '0.0', + color: '#f5f5f5', + tip: + 'The time taken to execute a secondary, more precise scoring phase (used by phrase queries).', + }, + { + key: 'match_count', + time: 0, + relative: 0, + color: '#f5f5f5', + tip: '', + }, + { + key: 'score', + time: 0, + relative: '0.0', + color: '#f5f5f5', + tip: 'The time taken in actually scoring the document against the query.', + }, + { + key: 'score_count', + time: 0, + relative: 0, + color: '#f5f5f5', + tip: '', + }, + { + key: 'set_min_competitive_score', + time: 0, + relative: '0.0', + color: '#f5f5f5', + tip: '', + }, + { + key: 'set_min_competitive_score_count', + time: 0, + relative: 0, + color: '#f5f5f5', + tip: '', + }, + { + key: 'shallow_advance', + time: 0, + relative: '0.0', + color: '#f5f5f5', + tip: '', + }, + { + key: 'shallow_advance_count', + time: 0, + relative: 0, + color: '#f5f5f5', + tip: '', + }, + ], + selfTime: 0.029634, + timePercentage: '50.73', + absoluteColor: '#fad1d1', + depth: 1, + parent: search1, + time: 0.029634, + lucene: 'DocValuesFieldExistsQuery [field=_primary_term]', + query_type: 'DocValuesFieldExistsQuery', + visible: true, +}; +(search1.children[0] as any) = search1Child; +(searchRoot.treeRoot as any) = search1; +(shard1.searches[0] as any) = searchRoot; + +export const processedResponseWithFirstShard = produce(null, () => [ + { + shards: [shard1], + time: 0.058419, + name: '.kibana_1', + visible: false, + }, +]); 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 new file mode 100644 index 00000000000000..b4483cc0fc58e0 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/fixtures/search_response.ts @@ -0,0 +1,308 @@ +/* + * 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 const searchResponse: any = [ + { + id: '[L22w_FX2SbqlQYOP5QrYDg] [.apm-agent-configuration] [0]', + searches: [ + { + query: [ + { + type: 'MatchAllDocsQuery', + description: '*:*', + time_in_nanos: 1971, + breakdown: { + set_min_competitive_score_count: 0, + match_count: 0, + shallow_advance_count: 0, + set_min_competitive_score: 0, + next_doc: 0, + match: 0, + next_doc_count: 0, + score_count: 0, + compute_max_score_count: 0, + compute_max_score: 0, + advance: 0, + advance_count: 0, + score: 0, + build_scorer_count: 0, + create_weight: 1970, + shallow_advance: 0, + create_weight_count: 1, + build_scorer: 0, + }, + }, + ], + rewrite_time: 908, + collector: [ + { + name: 'CancellableCollector', + reason: 'search_cancelled', + time_in_nanos: 1176, + children: [ + { name: 'SimpleTopScoreDocCollector', reason: 'search_top_hits', time_in_nanos: 517 }, + ], + }, + ], + }, + ], + aggregations: [], + }, + { + id: '[L22w_FX2SbqlQYOP5QrYDg] [.kibana_1] [0]', + searches: [ + { + query: [ + { + type: 'ConstantScoreQuery', + description: 'ConstantScore(DocValuesFieldExistsQuery [field=_primary_term])', + time_in_nanos: 58419, + breakdown: { + set_min_competitive_score_count: 0, + match_count: 0, + shallow_advance_count: 0, + set_min_competitive_score: 0, + next_doc: 5767, + match: 0, + next_doc_count: 24, + score_count: 24, + compute_max_score_count: 0, + compute_max_score: 0, + advance: 2849, + advance_count: 8, + score: 1431, + build_scorer_count: 16, + create_weight: 8238, + shallow_advance: 0, + create_weight_count: 1, + build_scorer: 40061, + }, + children: [ + { + type: 'DocValuesFieldExistsQuery', + description: 'DocValuesFieldExistsQuery [field=_primary_term]', + time_in_nanos: 29634, + breakdown: { + set_min_competitive_score_count: 0, + match_count: 0, + shallow_advance_count: 0, + set_min_competitive_score: 0, + next_doc: 2434, + match: 0, + next_doc_count: 24, + score_count: 0, + compute_max_score_count: 0, + compute_max_score: 0, + advance: 1506, + advance_count: 8, + score: 0, + build_scorer_count: 16, + create_weight: 1586, + shallow_advance: 0, + create_weight_count: 1, + build_scorer: 24059, + }, + }, + ], + }, + ], + rewrite_time: 14670, + collector: [ + { + name: 'CancellableCollector', + reason: 'search_cancelled', + time_in_nanos: 14706, + children: [ + { + name: 'SimpleTopScoreDocCollector', + reason: 'search_top_hits', + time_in_nanos: 7851, + }, + ], + }, + ], + }, + ], + aggregations: [], + }, + { + id: '[L22w_FX2SbqlQYOP5QrYDg] [.kibana_task_manager_1] [0]', + searches: [ + { + query: [ + { + type: 'ConstantScoreQuery', + description: 'ConstantScore(DocValuesFieldExistsQuery [field=_primary_term])', + time_in_nanos: 30013, + breakdown: { + set_min_competitive_score_count: 0, + match_count: 0, + shallow_advance_count: 0, + set_min_competitive_score: 0, + next_doc: 1497, + match: 0, + next_doc_count: 5, + score_count: 3, + compute_max_score_count: 0, + compute_max_score: 0, + advance: 1058, + advance_count: 3, + score: 309, + build_scorer_count: 6, + create_weight: 6727, + shallow_advance: 0, + create_weight_count: 1, + build_scorer: 20404, + }, + children: [ + { + type: 'DocValuesFieldExistsQuery', + description: 'DocValuesFieldExistsQuery [field=_primary_term]', + time_in_nanos: 19795, + breakdown: { + set_min_competitive_score_count: 0, + match_count: 0, + shallow_advance_count: 0, + set_min_competitive_score: 0, + next_doc: 600, + match: 0, + next_doc_count: 5, + score_count: 0, + compute_max_score_count: 0, + compute_max_score: 0, + advance: 378, + advance_count: 3, + score: 0, + build_scorer_count: 6, + create_weight: 1121, + shallow_advance: 0, + create_weight_count: 1, + build_scorer: 17681, + }, + }, + ], + }, + ], + rewrite_time: 10713, + collector: [ + { + name: 'CancellableCollector', + reason: 'search_cancelled', + time_in_nanos: 4924, + children: [ + { + name: 'SimpleTopScoreDocCollector', + reason: 'search_top_hits', + time_in_nanos: 2223, + }, + ], + }, + ], + }, + ], + aggregations: [], + }, + { + id: '[L22w_FX2SbqlQYOP5QrYDg] [.security-7] [0]', + searches: [ + { + query: [ + { + type: 'MatchAllDocsQuery', + description: '*:*', + time_in_nanos: 13254, + breakdown: { + set_min_competitive_score_count: 6, + match_count: 0, + shallow_advance_count: 0, + set_min_competitive_score: 3016, + next_doc: 1442, + match: 0, + next_doc_count: 10, + score_count: 10, + compute_max_score_count: 0, + compute_max_score: 0, + advance: 1313, + advance_count: 6, + score: 1169, + build_scorer_count: 12, + create_weight: 1132, + shallow_advance: 0, + create_weight_count: 1, + build_scorer: 5137, + }, + }, + ], + rewrite_time: 755, + collector: [ + { + name: 'CancellableCollector', + reason: 'search_cancelled', + time_in_nanos: 18172, + children: [ + { + name: 'SimpleTopScoreDocCollector', + reason: 'search_top_hits', + time_in_nanos: 12507, + }, + ], + }, + ], + }, + ], + aggregations: [], + }, + { + id: '[L22w_FX2SbqlQYOP5QrYDg] [kibana_sample_data_logs] [0]', + searches: [ + { + query: [ + { + type: 'MatchAllDocsQuery', + description: '*:*', + time_in_nanos: 12764, + breakdown: { + set_min_competitive_score_count: 4, + match_count: 0, + shallow_advance_count: 0, + set_min_competitive_score: 2685, + next_doc: 1831, + match: 0, + next_doc_count: 10, + score_count: 10, + compute_max_score_count: 0, + compute_max_score: 0, + advance: 1621, + advance_count: 4, + score: 835, + build_scorer_count: 8, + create_weight: 972, + shallow_advance: 0, + create_weight_count: 1, + build_scorer: 4783, + }, + }, + ], + rewrite_time: 691, + collector: [ + { + name: 'CancellableCollector', + reason: 'search_cancelled', + time_in_nanos: 17700, + children: [ + { + name: 'SimpleTopScoreDocCollector', + reason: 'search_top_hits', + time_in_nanos: 12839, + }, + ], + }, + ], + }, + ], + aggregations: [], + }, +]; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/init_data.test.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/init_data.test.ts new file mode 100644 index 00000000000000..6cd19947a26bc3 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/init_data.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 { ShardSerialized } from '../../../types'; +import { initDataFor } from '../init_data'; + +import { searchResponse } from './fixtures/search_response'; +import { processedResponseWithFirstShard } from './fixtures/processed_search_response'; + +describe('ProfileTree init data', () => { + test('provides the expected result', () => { + const input: ShardSerialized[] = searchResponse as any; + const actual = initDataFor('searches')(input); + + expect(actual[0].name).toEqual(processedResponseWithFirstShard[0].name); + const expectedFirstShard = processedResponseWithFirstShard[0].shards[0]; + expect(actual[0].shards[0]).toEqual(expectedFirstShard); + }); +}); diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/profile_tree.test.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/profile_tree.test.tsx new file mode 100644 index 00000000000000..ca95ac97e260a9 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/profile_tree.test.tsx @@ -0,0 +1,21 @@ +/* + * 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 { registerTestBed } from '../../../../../../../../../test_utils'; +import { searchResponse } from './fixtures/search_response'; +import { ProfileTree, Props } from '../profile_tree'; + +describe('ProfileTree', () => { + it('renders', async () => { + const props: Props = { + onHighlight: () => {}, + target: 'searches', + data: searchResponse, + }; + const init = registerTestBed(ProfileTree); + await init(props); + }); +}); diff --git a/x-pack/legacy/plugins/searchprofiler/public/directives/profile_tree/__tests__/profile_tree.js b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/unsafe_utils.test.ts similarity index 52% rename from x-pack/legacy/plugins/searchprofiler/public/directives/profile_tree/__tests__/profile_tree.js rename to x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/unsafe_utils.test.ts index 6da9a452e5e3c9..17c7051f09769c 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/directives/profile_tree/__tests__/profile_tree.js +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/unsafe_utils.test.ts @@ -5,21 +5,20 @@ */ import expect from '@kbn/expect'; -import * as util from '../util.js'; -import { normalized, breakdown } from './fixtures/breakdown.js'; -import { inputTimes, normalizedTimes } from './fixtures/normalize_times.js'; -import { inputIndices, normalizedIndices } from './fixtures/normalize_indices.js'; -import { flatTimes } from './fixtures/flatten_times.js'; +import * as util from '../unsafe_utils'; +import { normalized, breakdown } from './fixtures/breakdown'; +import { inputTimes, normalizedTimes } from './fixtures/normalize_times'; +import { inputIndices, normalizedIndices } from './fixtures/normalize_indices'; -describe('normalizeBreakdown', function () { - it('returns correct breakdown', function () { +describe('normalizeBreakdown', function() { + it('returns correct breakdown', function() { const result = util.normalizeBreakdown(breakdown); expect(result).to.eql(normalized); }); }); -describe('normalizeTimes', function () { - it('returns correct normalization', function () { +describe('normalizeTimes', function() { + it('returns correct normalization', function() { const totalTime = 0.447365; // Deep clone the object to preserve the original @@ -31,21 +30,12 @@ describe('normalizeTimes', function () { }); }); -describe('flattenResults', function () { - it('returns correct flattening', function () { - // Deep clone the object to preserve the original - const input = JSON.parse(JSON.stringify(normalizedTimes)); - const flat = []; - util.flattenResults(input, flat, 0, []); - expect(JSON.parse(JSON.stringify(flat))).to.eql(flatTimes); - }); -}); - -describe('normalizeIndices', function () { - it('returns correct ordering', function () { +describe('normalizeIndices', function() { + it('returns correct ordering', function() { // Deep clone the object to preserve the original const input = JSON.parse(JSON.stringify(inputIndices)); - const result = util.normalizeIndices(input, [], 'searches'); + util.normalizeIndices(input, 'searches'); + const result = util.sortIndices(input); expect(result).to.eql(normalizedIndices); }); }); diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/utils.test.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/utils.test.ts new file mode 100644 index 00000000000000..3a3d606434241a --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/__tests__/utils.test.ts @@ -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 { hasVisibleChild } from '../utils'; + +describe('Profile Tree utils', () => { + describe('#hasVisibleChild', () => { + it('base case no children', () => { + const op: any = { children: [] }; + expect(hasVisibleChild(op)).toBe(false); + }); + + it('base case with children', () => { + const op: any = { children: [{ visible: false }, { visible: false }, { visible: true }] }; + expect(hasVisibleChild(op)).toBe(true); + }); + + it('is robust', () => { + const op: any = {}; + expect(hasVisibleChild(op)).toBe(false); + }); + }); +}); diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/highlight_context.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/highlight_context.tsx new file mode 100644 index 00000000000000..f781d01352f854 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/highlight_context.tsx @@ -0,0 +1,52 @@ +/* + * 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, useState } from 'react'; +import { Operation, Shard } from '../../types'; + +const HighlightContext = createContext<{ + selectedRow: string; + setStore: (args: OnHighlightChangeArgs & { id: string }) => void; +}>(null as any); + +export interface OnHighlightChangeArgs { + indexName: string; + shard: Shard; + operation: Operation; +} + +type OnHighlightChangeHandler = (data: OnHighlightChangeArgs) => void; + +export const HighlightContextProvider = ({ + children, + onHighlight, +}: { + children: React.ReactNode; + onHighlight: OnHighlightChangeHandler; +}) => { + const [selectedRow, setSelectedRow] = useState(''); + return ( + { + onHighlight(onHighlightChangeArgs); + setSelectedRow(id); + }, + }} + > + {children} + + ); +}; + +export const useHighlightContext = () => { + const ctx = useContext(HighlightContext); + if (ctx == null) { + throw new Error(`useHighlightContext must be called inside HighlightContext`); + } + return ctx; +}; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/index.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/index.ts new file mode 100644 index 00000000000000..78efd2cb6687f0 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { ProfileTree } from './profile_tree'; +export { OnHighlightChangeArgs } from './highlight_context'; 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 new file mode 100644 index 00000000000000..20ea7c636ee74f --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/index_details.tsx @@ -0,0 +1,56 @@ +/* + * 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 { EuiText, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { msToPretty } from '../../utils'; +import { Index } from '../../types'; + +export interface Props { + index: Index; +} + +export const IndexDetails = ({ index }: Props) => { + const { time, name } = index; + return ( + + {/* Index Title group */} + + +

+ + {i18n.translate('xpack.searchProfiler.profileTree.indexTitle', { + defaultMessage: 'Index:', + })} + + {' ' + name} +

+
+
+ {/* Time details group */} + + + + + {i18n.translate('xpack.searchProfiler.profileTree.cumulativeTimeTitle', { + defaultMessage: 'Cumulative time:', + })} + + + {' ' + msToPretty(time, 3)} + + +
+ ); +}; 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 new file mode 100644 index 00000000000000..d7990b1204b217 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/init_data.ts @@ -0,0 +1,114 @@ +/* + * 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 { produce } from 'immer'; +import { flow } from 'fp-ts/lib/function'; +import { Targets, Shard, ShardSerialized } from '../../types'; +import { calcTimes, normalizeTimes, initTree, normalizeIndices, sortIndices } from './unsafe_utils'; +import { IndexMap } from './types'; + +/** + * Functions prefixed with "mutate" change values by reference. Be careful when using these! + * + * It's recommended to us immer's `produce` functions to ensure immutability. + */ + +export function mutateAggsTimesTree(shard: Shard) { + if (shard.aggregations == null) { + shard.time = 0; + } + let shardTime = 0; + for (const agg of shard.aggregations!) { + const totalTime = calcTimes([agg]); + shardTime += totalTime; + } + for (const agg of shard.aggregations!) { + normalizeTimes([agg], shardTime, 0); + initTree([agg], 0); + } + shard.time = shardTime; +} + +export function mutateSearchTimesTree(shard: Shard) { + if (shard.searches == null) { + shard.time = 0; + } + shard.rewrite_time = 0; + + let shardTime = 0; + for (const search of shard.searches!) { + shard.rewrite_time += search.rewrite_time!; + const totalTime = calcTimes(search.query!); + shardTime += totalTime; + normalizeTimes(search.query!, totalTime, 0); + initTree(search.query!, 0); + search.treeRoot = search.query![0]; + search.query = null as any; + } + shard.time = shardTime; +} + +const initShards = (data: ShardSerialized[]) => + produce(data, draft => { + 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[]) => + produce(data, draft => { + for (const shard of draft) { + if (target === 'searches') { + mutateSearchTimesTree(shard); + } else if (target === 'aggregations') { + mutateAggsTimesTree(shard); + } + } + }); + +export const initIndices = (data: Shard[]) => + produce(data, doNotChange => { + const indices: IndexMap = {}; + + for (const shard of doNotChange) { + if (!indices[shard.id[1]]) { + indices[shard.id[1]] = { + shards: [], + time: 0, + name: shard.id[1], + visible: false, + }; + } + indices[shard.id[1]].shards.push(shard); + indices[shard.id[1]].time += shard.time; + } + + return indices; + }); + +export const normalize = (target: Targets) => (data: IndexMap) => + produce(data, draft => { + normalizeIndices(draft, target); + }); + +export const initDataFor = (target: Targets) => + flow( + initShards, + calculateShardValues(target), + initIndices, + normalize(target), + sortIndices + ); 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 new file mode 100644 index 00000000000000..ea24abbdb56db2 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/profile_tree.tsx @@ -0,0 +1,64 @@ +/* + * 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, { memo } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; + +import { IndexDetails } from './index_details'; +import { ShardDetails } from './shard_details'; +import { initDataFor } from './init_data'; +import { Targets, ShardSerialized } from '../../types'; +import { HighlightContextProvider, OnHighlightChangeArgs } from './highlight_context'; + +export interface Props { + target: Targets; + data: ShardSerialized[] | null; + onHighlight: (args: OnHighlightChangeArgs) => void; +} + +export const ProfileTree = memo(({ data, target, onHighlight }: Props) => { + if (!data || data.length === 0) { + return null; + } + + const sortedIndices = initDataFor(target)(data); + + return ( + + + {sortedIndices.map(index => ( + + + + + + + + {index.shards.map(shard => ( + + ))} + + + + ))} + + + ); +}); diff --git a/x-pack/legacy/plugins/searchprofiler/public/templates/default_query.js b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/shard_details/index.ts similarity index 77% rename from x-pack/legacy/plugins/searchprofiler/public/templates/default_query.js rename to x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/shard_details/index.ts index afab7e808eb1a2..ea9874d559b378 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/templates/default_query.js +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/shard_details/index.ts @@ -4,8 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export const defaultQuery = `{ - "query":{ - "match_all" : {} - } -}`; +export { ShardDetails } from './shard_details'; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/shard_details/shard_details.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/shard_details/shard_details.tsx new file mode 100644 index 00000000000000..eca2d1994f8c56 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/shard_details/shard_details.tsx @@ -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. + */ + +import React, { useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiLink, EuiIcon } from '@elastic/eui'; + +import { Index, Operation, Shard } from '../../../types'; +import { msToPretty } from '../../../utils'; +import { ShardDetailTree } from './shard_details_tree'; +import { PercentageBadge } from '../../percentage_badge'; + +interface Props { + index: Index; + shard: Shard; + operations: Operation[]; +} + +export const ShardDetails = ({ index, shard, operations }: Props) => { + const { relative, time } = shard; + + const [shardVisibility, setShardVisibility] = useState(false); + + return ( + <> + + + setShardVisibility(!shardVisibility)} + > + [{shard.id[0]}][ + {shard.id[2]}] + + + + + + + + + {shardVisibility + ? operations.map((data, idx) => ( + + )) + : 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 new file mode 100644 index 00000000000000..d3b0a1a5a6355d --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/shard_details/shard_details_tree.tsx @@ -0,0 +1,59 @@ +/* + * 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 { EuiText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { ShardDetailsTreeNode } from './shard_details_tree_node'; +import { Index, Operation, Shard } from '../../../types'; + +export interface Props { + data: Operation; + index: Index; + shard: Shard; +} + +export const ShardDetailTree = ({ data, index, shard }: Props) => { + const renderOperations = (operation: Operation): JSX.Element => { + const nextOperation = operation.treeRoot || operation; + return ; + }; + + return ( +
+ + {/* Setting grow to false here is important for IE11. Don't change without testing first! */} + +
+ + {i18n.translate('xpack.searchProfiler.profileTree.header.typeTitle', { + defaultMessage: 'Type and description', + })} + + + {i18n.translate('xpack.searchProfiler.profileTree.header.selfTimeTitle', { + defaultMessage: 'Self time', + })} + + + {i18n.translate('xpack.searchProfiler.profileTree.header.totalTimeTitle', { + defaultMessage: 'Total time', + })} + +
+
+ + {renderOperations(data)} + +
+ ); +}; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/shard_details/shard_details_tree_node.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/shard_details/shard_details_tree_node.tsx new file mode 100644 index 00000000000000..75da10f8aca2ef --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/shard_details/shard_details_tree_node.tsx @@ -0,0 +1,114 @@ +/* + * 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, { useState } from 'react'; +import { EuiLink, EuiBadge, EuiCodeBlock, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { hasVisibleChild } from '../utils'; +import { useHighlightTreeNode } from '../use_highlight_tree_node'; +import { msToPretty } from '../../../utils'; + +import { PercentageBadge } from '../../percentage_badge'; + +import { Index, Operation, Shard } from '../../../types'; + +export interface Props { + index: Index; + shard: Shard; + operation: Operation; +} + +const TAB_WIDTH_PX = 32; + +const limitString = (string: string, limit: number) => + `${string.slice(0, limit)}${string.length > limit ? '...' : ''}`; + +/** + * This is a component that recursively iterates over data to render out a tree like + * structure in a flatly. + */ +export const ShardDetailsTreeNode = ({ operation, index, shard }: Props) => { + const [childrenVisible, setChildrenVisible] = useState(hasVisibleChild(operation)); + const { highlight, isHighlighted, id } = useHighlightTreeNode(); + + const renderTimeRow = (op: Operation) => ( +
+
+ {op.hasChildren ? ( + setChildrenVisible(!childrenVisible)} + > + + + {' ' + op.query_type} + + ) : ( + <> + + {' ' + op.query_type} + + )} +
+ {/* Self Time Badge */} +
+ + {msToPretty(op.selfTime || 0, 1)} + +
+ {/* Total Time Badge */} +
+ + {msToPretty(op.time, 1)} + +
+ {/* Time percentage Badge */} +
+ +
+
+ ); + + return ( + <> +
+ {renderTimeRow(operation)} +
+ + + {limitString(operation.lucene || '', 120)} + + highlight({ indexName: index.name, operation, shard })} + > + {i18n.translate('xpack.searchProfiler.profileTree.body.viewDetailsLabel', { + defaultMessage: 'View details', + })} + + +
+
+ {childrenVisible && + operation.hasChildren && + operation.children.flatMap((childOp, idx) => ( + + ))} + + ); +}; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/types.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/types.ts new file mode 100644 index 00000000000000..d38e895f66bbcb --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/types.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Index } from '../../types'; + +export type IndexMap = Record; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/unsafe_utils.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/unsafe_utils.ts new file mode 100644 index 00000000000000..fc21c5da37764c --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/unsafe_utils.ts @@ -0,0 +1,221 @@ +/* + * 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 { produce } from 'immer'; +import { i18n } from '@kbn/i18n'; +import tinycolor from 'tinycolor2'; +import _ from 'lodash'; + +import { BreakdownItem, Index, Operation, Shard, Targets } from '../../types'; +import { IndexMap } from './types'; + +export const comparator = (v1: number, v2: number) => { + if (v1 < v2) { + return 1; + } + return v1 > v2 ? -1 : 0; +}; + +function getToolTip(key: string) { + switch (key) { + case 'build_scorer': + return i18n.translate('xpack.searchProfiler.buildScorerTimeDescription', { + defaultMessage: + 'The time taken to create the Scoring object, which is later used to execute the actual scoring of each doc.', + }); + case 'create_weight': + return i18n.translate('xpack.searchProfiler.createWeightTimeDescription', { + defaultMessage: + 'The time taken to create the Weight object, which holds temporary information during scoring.', + }); + case 'next_doc': + return i18n.translate('xpack.searchProfiler.nextDocTimeDescription', { + defaultMessage: 'The time taken to advance the iterator to the next matching document.', + }); + case 'score': + return i18n.translate('xpack.searchProfiler.scoreTimeDescription', { + defaultMessage: 'The time taken in actually scoring the document against the query.', + }); + case 'match': + return i18n.translate('xpack.searchProfiler.matchTimeDescription', { + defaultMessage: + 'The time taken to execute a secondary, more precise scoring phase (used by phrase queries).', + }); + case 'advance': + return i18n.translate('xpack.searchProfiler.advanceTimeDescription', { + defaultMessage: 'The time taken to advance the iterator to the next document.', + }); + default: + return ''; + } +} + +export function timeInMilliseconds(data: any) { + if (data.time_in_nanos) { + return data.time_in_nanos / 1000000; + } + + if (typeof data.time === 'string') { + return data.time.replace('ms', ''); + } + + return data.time; +} + +export function calcTimes(data: any[], parentId?: string) { + let totalTime = 0; + // First pass to collect total + for (const child of data) { + totalTime += timeInMilliseconds(child); + + child.breakdown = normalizeBreakdown(child.breakdown); + + let childrenTime = 0; + if (child.children != null && child.children.length !== 0) { + childrenTime = calcTimes(child.children, child.id)!; + child.hasChildren = true; + } + child.selfTime = timeInMilliseconds(child) - childrenTime; + } + return totalTime; +} + +export function normalizeBreakdown(breakdown: Record) { + const final: BreakdownItem[] = []; + const total = Object.keys(breakdown).reduce((partialTotal, currentKey) => { + if (currentKey.indexOf('_count') === -1) { + partialTotal += breakdown[currentKey]; + } + return partialTotal; + }, 0); + Object.keys(breakdown) + .sort() + .forEach(key => { + let relative = 0; + if (key.indexOf('_count') === -1) { + relative = ((breakdown[key] / total) * 100).toFixed(1) as any; + } + final.push({ + key, + time: breakdown[key], + relative, + color: tinycolor.mix('#F5F5F5', '#FFAFAF', relative).toHexString(), + tip: getToolTip(key), + }); + }); + + // Sort by time descending and then key ascending + return final.sort((a, b) => { + if (comparator(a.time, b.time) !== 0) { + return comparator(a.time, b.time); + } + + return -1 * comparator(a.key as any, b.key as any); + }); +} + +export function normalizeTimes(data: any[], totalTime: number, depth: number) { + // Second pass to normalize + for (const child of data) { + child.timePercentage = ((timeInMilliseconds(child) / totalTime) * 100).toFixed(2); + child.absoluteColor = tinycolor.mix('#F5F5F5', '#FFAFAF', child.timePercentage).toHexString(); + child.depth = depth; + + if (child.children != null && child.children.length !== 0) { + normalizeTimes(child.children, totalTime, depth + 1); + } + } + + data.sort((a, b) => comparator(timeInMilliseconds(a), timeInMilliseconds(b))); +} + +export function normalizeIndices(indices: IndexMap, target: Targets) { + // Sort the shards per-index + let sortQueryComponents; + if (target === 'searches') { + sortQueryComponents = (a: Shard, b: Shard) => { + const aTime = _.sum(a.searches!, search => { + return search.treeRoot!.time; + }); + const bTime = _.sum(b.searches!, search => { + return search.treeRoot!.time; + }); + + return comparator(aTime, bTime); + }; + } else if (target === 'aggregations') { + sortQueryComponents = (a: Shard, b: Shard) => { + const aTime = _.sum(a.aggregations!, agg => { + return agg.treeRoot!.time; + }); + const bTime = _.sum(b.aggregations!, agg => { + return agg.treeRoot!.time; + }); + + return comparator(aTime, bTime); + }; + } + for (const index of Object.values(indices)) { + index.shards.sort(sortQueryComponents); + for (const shard of index.shards) { + shard.relative = ((shard.time / index.time) * 100).toFixed(2); + shard.color = tinycolor.mix('#F5F5F5', '#FFAFAF', shard.relative as any).toHexString(); + } + } +} + +export function initTree(data: Operation[], depth = 0, parent: Operation | null = null) { + for (const child of data) { + // For bwc of older profile responses + if (!child.description) { + child.description = child.lucene!; + child.lucene = null; + + child.type = child.query_type!; + child.query_type = null; + } + + // Use named function for tests. + child.parent = parent; + child.time = timeInMilliseconds(child); + child.lucene = child.description; + child.query_type = child.type!.split('.').pop()!; + child.visible = child.timePercentage > 20; + child.depth = depth; + + if (child.children != null && child.children.length !== 0) { + initTree(child.children, depth + 1, child); + } + } +} + +export function closeNode(node: Operation) { + const closeDraft = (draft: Operation) => { + draft.visible = false; + + if (draft.children == null || draft.children.length === 0) { + return; + } + + for (const child of draft.children) { + closeDraft(child); + } + }; + return produce(node, draft => { + closeDraft(draft); + }); +} + +export const sortIndices = (data: IndexMap) => + produce(data, doNotChange => { + const sortedIndices: Index[] = []; + for (const index of Object.values(doNotChange)) { + sortedIndices.push(index); + } + // And now sort the indices themselves + sortedIndices.sort((a, b) => comparator(a.time, b.time)); + return sortedIndices; + }); diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/use_highlight_tree_node.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/use_highlight_tree_node.ts new file mode 100644 index 00000000000000..6e6f16a2cb683a --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/use_highlight_tree_node.ts @@ -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 { useRef } from 'react'; +import uuid from 'uuid'; + +import { useHighlightContext, OnHighlightChangeArgs } from './highlight_context'; + +export const useHighlightTreeNode = () => { + const idRef = useRef(uuid.v4()); + const { selectedRow, setStore } = useHighlightContext(); + + const highlight = (value: OnHighlightChangeArgs) => { + setStore({ id: idRef.current, ...value }); + }; + + const isHighlighted = () => { + return selectedRow === idRef.current; + }; + + return { + id: idRef.current, + highlight, + isHighlighted, + }; +}; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/utils.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/utils.ts new file mode 100644 index 00000000000000..d0bedd873a1bd2 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/profile_tree/utils.ts @@ -0,0 +1,11 @@ +/* + * 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 { Operation } from '../../types'; + +export const hasVisibleChild = ({ children }: Operation) => { + return Boolean(children && children.some(child => child.visible)); +}; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/searchprofiler_tabs.test.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/searchprofiler_tabs.test.ts new file mode 100644 index 00000000000000..8ee43e28bcd2c5 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/searchprofiler_tabs.test.ts @@ -0,0 +1,24 @@ +/* + * 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 { registerTestBed } from '../../../../../../../test_utils'; + +import { SearchProfilerTabs, Props } from './searchprofiler_tabs'; + +describe('Search Profiler Tabs', () => { + it('renders', async () => { + const props: Props = { + activateTab: () => {}, + activeTab: null, + has: { + aggregations: true, + searches: true, + }, + }; + const init = registerTestBed(SearchProfilerTabs); + await init(props); + }); +}); diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/searchprofiler_tabs.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/searchprofiler_tabs.tsx new file mode 100644 index 00000000000000..19224e7099fd66 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/components/searchprofiler_tabs.tsx @@ -0,0 +1,46 @@ +/* + * 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 { EuiTabs, EuiTab } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { Targets } from '../types'; + +export interface Props { + activeTab: Targets | null; + activateTab: (target: Targets) => void; + has: { + searches: boolean; + aggregations: boolean; + }; +} + +export const SearchProfilerTabs = ({ activeTab, activateTab, has }: Props) => { + return ( + + activateTab('searches')} + > + {i18n.translate('xpack.searchProfiler.queryProfileTabTitle', { + defaultMessage: 'Query Profile', + })} + + activateTab('aggregations')} + > + {i18n.translate('xpack.searchProfiler.aggregationProfileTabTitle', { + defaultMessage: 'Aggregation Profile', + })} + + + ); +}; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/index.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/index.ts new file mode 100644 index 00000000000000..1830a34e93d309 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { Main } from './main'; +export { ProfileQueryEditor } from './profile_query_editor'; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/empty_tree_placeholder.test.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/empty_tree_placeholder.test.tsx new file mode 100644 index 00000000000000..4f17d0b4304f64 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/empty_tree_placeholder.test.tsx @@ -0,0 +1,14 @@ +/* + * 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 { registerTestBed } from '../../../../../../../../../test_utils'; +import { EmptyTreePlaceHolder } from '.'; + +describe('EmptyTreePlaceholder', () => { + it('renders', async () => { + const init = registerTestBed(EmptyTreePlaceHolder); + await init(); + }); +}); diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/empty_tree_placeholder.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/empty_tree_placeholder.tsx new file mode 100644 index 00000000000000..bf27620dcac181 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/empty_tree_placeholder.tsx @@ -0,0 +1,30 @@ +/* + * 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 { EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export const EmptyTreePlaceHolder = () => { + return ( +
+ + {/* TODO: translations */} +

+ {i18n.translate('xpack.searchProfiler.emptyProfileTreeTitle', { + defaultMessage: 'Nothing to see here yet.', + })} +

+

+ {i18n.translate('xpack.searchProfiler.emptyProfileTreeDescription', { + defaultMessage: + 'Enter a query and press the "Profile" button or provide profile data in the editor.', + })} +

+
+
+ ); +}; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/index.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/index.ts new file mode 100644 index 00000000000000..e8b7450ed96453 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { EmptyTreePlaceHolder } from './empty_tree_placeholder'; +export { ProfileLoadingPlaceholder } from './profile_loading_placeholder'; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/profile_loading_placeholder.test.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/profile_loading_placeholder.test.tsx new file mode 100644 index 00000000000000..9b3348b4cab4b2 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/profile_loading_placeholder.test.tsx @@ -0,0 +1,15 @@ +/* + * 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 { registerTestBed } from '../../../../../../../../../test_utils'; +import { ProfileLoadingPlaceholder } from '.'; + +describe('Profile Loading Placeholder', () => { + it('renders', async () => { + const init = registerTestBed(ProfileLoadingPlaceholder); + await init(); + }); +}); diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/profile_loading_placeholder.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/profile_loading_placeholder.tsx new file mode 100644 index 00000000000000..fb09c6cddf70a4 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/profile_loading_placeholder.tsx @@ -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 React from 'react'; +import { EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +export const ProfileLoadingPlaceholder = () => { + return ( +
+ +

+ {i18n.translate('xpack.searchProfiler.profilingLoaderText', { + defaultMessage: 'Profiling...', + })} +

+
+
+ ); +}; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/index.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/index.ts new file mode 100644 index 00000000000000..509a15c0c71a52 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/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 { Main } from './main'; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/main.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/main.tsx new file mode 100644 index 00000000000000..7f5d223949e610 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/main.tsx @@ -0,0 +1,136 @@ +/* + * 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, { useCallback } from 'react'; +import _ from 'lodash'; + +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; +import { ProfileQueryEditor } from '../'; + +import { + SearchProfilerTabs, + ProfileTree, + HighlightDetailsFlyout, + LicenseWarningNotice, +} from '../../components'; + +import { useAppContext } from '../../contexts/app_context'; + +import { EmptyTreePlaceHolder, ProfileLoadingPlaceholder } from './components'; +import { Targets, ShardSerialized } from '../../types'; +import { useProfilerActionContext, useProfilerReadContext } from '../../contexts/profiler_context'; + +function hasSearch(profileResponse: ShardSerialized[]) { + const aggs = _.get(profileResponse, '[0].searches', []); + return aggs.length > 0; +} + +function hasAggregations(profileResponse: ShardSerialized[]) { + const aggs = _.get(profileResponse, '[0].aggregations', []); + return aggs.length > 0; +} + +export const Main = () => { + const { licenseEnabled } = useAppContext(); + + const { + activeTab, + currentResponse, + highlightDetails, + pristine, + profiling, + } = useProfilerReadContext(); + const dispatch = useProfilerActionContext(); + + const setActiveTab = useCallback( + (target: Targets) => dispatch({ type: 'setActiveTab', value: target }), + [dispatch] + ); + + const onHighlight = useCallback(value => dispatch({ type: 'setHighlightDetails', value }), [ + dispatch, + ]); + + const renderLicenseWarning = () => { + return !licenseEnabled ? ( + <> + + + + ) : null; + }; + + const renderProfileTreeArea = () => { + if (profiling) { + return ; + } + + if (activeTab) { + return ( +
+ +
+ ); + } + + if (licenseEnabled && pristine) { + return ; + } + + return null; + }; + + return ( + <> + + + {renderLicenseWarning()} + + + + + + + + + + {renderProfileTreeArea()} + + + + {highlightDetails ? ( + dispatch({ type: 'setHighlightDetails', value: null })} + /> + ) : null} + + + + + + ); +}; 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 new file mode 100644 index 00000000000000..ea3421ffe89aa8 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/profile_query_editor.tsx @@ -0,0 +1,131 @@ +/* + * 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, memo, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiForm, + EuiFieldText, + EuiFormRow, + EuiButton, + EuiText, + EuiFlexGroup, + EuiSpacer, + EuiFlexItem, +} from '@elastic/eui'; +import { Editor, EditorInstance } from '../editor'; +import { useRequestProfile } from '../hooks'; +import { useAppContext } from '../contexts/app_context'; +import { useProfilerActionContext } from '../contexts/profiler_context'; + +const DEFAULT_INDEX_VALUE = '_all'; + +const INITIAL_EDITOR_VALUE = `{ + "query":{ + "match_all" : {} + } +}`; + +/** + * This component should only need to render once. + * + * Drives state changes for mine via profiler action context. + */ +export const ProfileQueryEditor = memo(() => { + const editorRef = useRef(null as any); + const indexInputRef = useRef(null as any); + + const dispatch = useProfilerActionContext(); + + const { licenseEnabled, notifications } = useAppContext(); + const requestProfile = useRequestProfile(); + + const handleProfileClick = async () => { + dispatch({ type: 'setProfiling', value: true }); + try { + const { current: editor } = editorRef; + const { data: result, error } = await requestProfile({ + query: editorRef.current.getValue(), + index: indexInputRef.current.value, + }); + if (error) { + notifications.addDanger(error); + editor.focus(); + return; + } + if (result === null) { + return; + } + dispatch({ type: 'setCurrentResponse', value: result }); + } finally { + dispatch({ type: 'setProfiling', value: false }); + } + }; + + const onEditorReady = useCallback(editorInstance => (editorRef.current = editorInstance), []); + + return ( + + {/* Form */} + + + + + + { + indexInputRef.current = ref!; + ref!.value = DEFAULT_INDEX_VALUE; + }} + /> + + + + + + + {/* Editor */} + + + + + {/* Button */} + + + + + + + handleProfileClick()}> + + {i18n.translate('xpack.searchProfiler.formProfileButtonLabel', { + defaultMessage: 'Profile', + })} + + + + + + + ); +}); diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/contexts/app_context.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/contexts/app_context.tsx new file mode 100644 index 00000000000000..7f8a6cf01dcb27 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/contexts/app_context.tsx @@ -0,0 +1,35 @@ +/* + * 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, ToastsSetup } from 'kibana/public'; + +export interface ContextValue { + http: HttpSetup; + notifications: ToastsSetup; + licenseEnabled: boolean; + formatAngularHttpError: (error: any) => string; +} + +const AppContext = createContext(null as any); + +export const AppContextProvider = ({ + children, + value, +}: { + children: React.ReactNode; + 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/public/np_ready/application/contexts/profiler_context.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/contexts/profiler_context.tsx new file mode 100644 index 00000000000000..ad46c61b6cd49d --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/contexts/profiler_context.tsx @@ -0,0 +1,36 @@ +/* + * 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, Dispatch } from 'react'; +import { useStore, State, Action } from '../store'; + +const ProfilerReadContext = createContext(null as any); +const ProfilerActionContext = createContext>(null as any); + +export const ProfileContextProvider = ({ children }: { children: React.ReactNode }) => { + const { dispatch, state } = useStore(); + return ( + + {children} + + ); +}; + +export const useProfilerReadContext = () => { + const ctx = useContext(ProfilerReadContext); + if (ctx == null) { + throw new Error(`useProfilerReadContext must be called inside ProfilerReadContext`); + } + return ctx; +}; + +export const useProfilerActionContext = () => { + const ctx = useContext(ProfilerActionContext); + if (ctx == null) { + throw new Error(`useProfilerActionContext must be called inside ProfilerActionContext`); + } + return ctx; +}; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/editor.test.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/editor.test.tsx new file mode 100644 index 00000000000000..d6702ef080ab73 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/editor.test.tsx @@ -0,0 +1,27 @@ +/* + * 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 'brace'; +import 'brace/mode/json'; + +jest.mock('./worker', () => { + return { workerModule: { id: 'ace/mode/json_worker', src: '' } }; +}); + +import { registerTestBed } from '../../../../../../../test_utils'; +import { Editor, Props } from '.'; + +describe('Editor Component', () => { + it('renders', async () => { + const props: Props = { + initialValue: '', + licenseEnabled: true, + onEditorReady: e => {}, + }; + // Ignore the warning about Worker not existing for now... + const init = registerTestBed(Editor); + await init(props); + }); +}); 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 new file mode 100644 index 00000000000000..014336f379059a --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/editor.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useRef, useEffect, useState } from 'react'; +import { Editor as AceEditor } from 'brace'; + +import { initializeEditor } from './init_editor'; +import { useUIAceKeyboardMode } from './use_ui_ace_keyboard_mode'; + +interface EditorShim { + getValue(): string; + focus(): void; +} + +export type EditorInstance = EditorShim; + +export interface Props { + licenseEnabled: boolean; + initialValue: string; + onEditorReady: (editor: EditorShim) => void; +} + +const createEditorShim = (aceEditor: AceEditor): EditorShim => { + return { + getValue() { + return aceEditor.getValue(); + }, + focus() { + aceEditor.focus(); + }, + }; +}; + +export const Editor = memo(({ licenseEnabled, initialValue, onEditorReady }: Props) => { + const containerRef = useRef(null as any); + const editorInstanceRef = useRef(null as any); + + const [textArea, setTextArea] = useState(null); + + if (licenseEnabled) { + useUIAceKeyboardMode(textArea); + } + + useEffect(() => { + const divEl = containerRef.current; + editorInstanceRef.current = initializeEditor({ el: divEl, licenseEnabled }); + editorInstanceRef.current.setValue(initialValue, 1); + setTextArea(containerRef.current!.querySelector('textarea')); + + onEditorReady(createEditorShim(editorInstanceRef.current)); + }, [initialValue, onEditorReady, licenseEnabled]); + + return
; +}); diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/index.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/index.ts new file mode 100644 index 00000000000000..f17f37923ae837 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/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 { Editor, Props, EditorInstance } from './editor'; diff --git a/x-pack/legacy/plugins/searchprofiler/public/editor/index.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/init_editor.ts similarity index 85% rename from x-pack/legacy/plugins/searchprofiler/public/editor/index.ts rename to x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/init_editor.ts index 45562385c2e691..e5aad16bc4af20 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/editor/index.ts +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/init_editor.ts @@ -10,11 +10,9 @@ import { installXJsonMode } from './x_json_mode'; export function initializeEditor({ el, licenseEnabled, - initialContent, }: { el: HTMLDivElement; licenseEnabled: boolean; - initialContent: string; }) { const editor: ace.Editor = ace.acequire('ace/ace').edit(el); @@ -25,6 +23,10 @@ export function initializeEditor({ editor.setReadOnly(true); editor.container.style.pointerEvents = 'none'; editor.container.style.opacity = '0.5'; + const textArea = editor.container.querySelector('textarea'); + if (textArea) { + textArea.setAttribute('tabindex', '-1'); + } editor.renderer.setStyle('disabled'); editor.blur(); } diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/use_ui_ace_keyboard_mode.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/use_ui_ace_keyboard_mode.tsx new file mode 100644 index 00000000000000..edf31c2e7c07f8 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/use_ui_ace_keyboard_mode.tsx @@ -0,0 +1,110 @@ +/* + * 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. + */ + +/** + * Copied from Console plugin + */ + +import React, { useEffect, useRef } from 'react'; +import * as ReactDOM from 'react-dom'; +import { keyCodes, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +const OverlayText = () => ( + // The point of this element is for accessibility purposes, so ignore eslint error + // in this case + // + // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions + <> + + {i18n.translate('xpack.searchProfiler.aceAccessibilityOverlayInstructionEnter', { + defaultMessage: 'Press Enter to start editing.', + })} + + + {i18n.translate('xpack.searchProfiler.aceAccessibilityOverlayInstructionExit', { + defaultMessage: `When you are done, press Escape to stop editing.`, + })} + + +); + +export function useUIAceKeyboardMode(aceTextAreaElement: HTMLTextAreaElement | null) { + const overlayMountNode = useRef(null); + const autoCompleteVisibleRef = useRef(false); + + useEffect(() => { + function onDismissOverlay(event: KeyboardEvent) { + if (event.keyCode === keyCodes.ENTER) { + event.preventDefault(); + aceTextAreaElement!.focus(); + } + } + + function enableOverlay() { + if (overlayMountNode.current) { + overlayMountNode.current.focus(); + } + } + + const isAutoCompleteVisible = () => { + const autoCompleter = document.querySelector('.ace_autocomplete'); + if (!autoCompleter) { + return false; + } + // The autoComplete is just hidden when it's closed, not removed from the DOM. + return autoCompleter.style.display !== 'none'; + }; + + const documentKeyDownListener = () => { + autoCompleteVisibleRef.current = isAutoCompleteVisible(); + }; + + const aceKeydownListener = (event: KeyboardEvent) => { + if (event.keyCode === keyCodes.ESCAPE && !autoCompleteVisibleRef.current) { + event.preventDefault(); + event.stopPropagation(); + enableOverlay(); + } + }; + + if (aceTextAreaElement) { + // We don't control HTML elements inside of ace so we imperatively create an element + // that acts as a container and insert it just before ace's textarea element + // so that the overlay lives at the correct spot in the DOM hierarchy. + overlayMountNode.current = document.createElement('div'); + overlayMountNode.current.className = 'kbnUiAceKeyboardHint'; + overlayMountNode.current.setAttribute('role', 'application'); + overlayMountNode.current.tabIndex = 0; + overlayMountNode.current.addEventListener('focus', enableOverlay); + overlayMountNode.current.addEventListener('keydown', onDismissOverlay); + + ReactDOM.render(, overlayMountNode.current); + + aceTextAreaElement.parentElement!.insertBefore(overlayMountNode.current, aceTextAreaElement); + aceTextAreaElement.setAttribute('tabindex', '-1'); + + // Order of events: + // 1. Document capture event fires first and we check whether an autocomplete menu is open on keydown + // (not ideal because this is scoped to the entire document). + // 2. Ace changes it's state (like hiding or showing autocomplete menu) + // 3. We check what button was pressed and whether autocomplete was visible then determine + // whether it should act like a dismiss or if we should display an overlay. + document.addEventListener('keydown', documentKeyDownListener, { capture: true }); + aceTextAreaElement.addEventListener('keydown', aceKeydownListener); + } + return () => { + if (aceTextAreaElement) { + document.removeEventListener('keydown', documentKeyDownListener); + aceTextAreaElement.removeEventListener('keydown', aceKeydownListener); + const textAreaContainer = aceTextAreaElement.parentElement; + if (textAreaContainer && textAreaContainer.contains(overlayMountNode.current!)) { + textAreaContainer.removeChild(overlayMountNode.current!); + } + } + }; + }, [aceTextAreaElement]); +} diff --git a/x-pack/legacy/plugins/searchprofiler/public/editor/worker/index.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/worker/index.ts similarity index 100% rename from x-pack/legacy/plugins/searchprofiler/public/editor/worker/index.ts rename to x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/worker/index.ts diff --git a/x-pack/legacy/plugins/searchprofiler/public/editor/worker/worker.d.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/worker/worker.d.ts similarity index 100% rename from x-pack/legacy/plugins/searchprofiler/public/editor/worker/worker.d.ts rename to x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/worker/worker.d.ts diff --git a/x-pack/legacy/plugins/searchprofiler/public/editor/worker/worker.js b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/worker/worker.js similarity index 100% rename from x-pack/legacy/plugins/searchprofiler/public/editor/worker/worker.js rename to x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/worker/worker.js diff --git a/x-pack/legacy/plugins/searchprofiler/public/editor/x_json_highlight_rules.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/x_json_highlight_rules.ts similarity index 100% rename from x-pack/legacy/plugins/searchprofiler/public/editor/x_json_highlight_rules.ts rename to x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/x_json_highlight_rules.ts diff --git a/x-pack/legacy/plugins/searchprofiler/public/editor/x_json_mode.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/x_json_mode.ts similarity index 100% rename from x-pack/legacy/plugins/searchprofiler/public/editor/x_json_mode.ts rename to x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/x_json_mode.ts diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/hooks/index.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/hooks/index.ts new file mode 100644 index 00000000000000..a9fa5fee994e0f --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/hooks/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 { useRequestProfile } from './use_request_profile'; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/hooks/use_request_profile.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/hooks/use_request_profile.ts new file mode 100644 index 00000000000000..34b49be7dc39c6 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/hooks/use_request_profile.ts @@ -0,0 +1,77 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { useAppContext } from '../contexts/app_context'; +import { checkForParseErrors } from '../utils'; +import { ShardSerialized } from '../types'; + +interface Args { + query: string; + index: string; +} + +interface ReturnValue { + data: ShardSerialized[] | null; + error?: string; +} + +export const useRequestProfile = () => { + const { http, notifications, formatAngularHttpError, licenseEnabled } = useAppContext(); + return async ({ query, index }: Args): Promise => { + if (!licenseEnabled) { + return { data: null }; + } + const { error, parsed } = checkForParseErrors(query); + if (error) { + notifications.addError(error, { + title: i18n.translate('xpack.searchProfiler.errorToastTitle', { + defaultMessage: 'JSON parse error', + }), + }); + return { data: null }; + } + // Shortcut the network request if we have json with shards already... + if (parsed.profile && parsed.profile.shards) { + return { data: parsed.profile.shards }; + } + + const payload: Record = { query }; + + if (index == null || index === '') { + payload.index = '_all'; + } else { + payload.index = index; + } + + try { + const resp = await http.post('../api/searchprofiler/profile', { + body: JSON.stringify(payload), + headers: { 'Content-Type': 'application/json' }, + }); + + if (!resp.ok) { + return { data: null, error: resp.err.msg }; + } + + return { data: resp.resp.profile.shards }; + } catch (e) { + try { + // Is this a known error type? + const errorString = formatAngularHttpError(e); + notifications.addError(e, { title: errorString }); + } catch (_) { + // Otherwise just report the original error + notifications.addError(e, { + title: i18n.translate('xpack.searchProfiler.errorSomethingWentWrongTitle', { + defaultMessage: 'Something went wrong', + }), + }); + } + return { data: null }; + } + }; +}; diff --git a/x-pack/legacy/plugins/searchprofiler/public/index.scss b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/index.scss similarity index 84% rename from x-pack/legacy/plugins/searchprofiler/public/index.scss rename to x-pack/legacy/plugins/searchprofiler/public/np_ready/application/index.scss index 2db81310a8db48..e04e81c023196a 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/index.scss +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/index.scss @@ -9,5 +9,4 @@ // prfDevTool__cell // prfDevTool__shardDetails -@import 'directives/index'; -@import 'templates/index'; \ No newline at end of file +@import 'styles/index'; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/index.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/index.tsx new file mode 100644 index 00000000000000..d29f193ce9d90d --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/index.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 from 'react'; +import { Main } from './containers'; +import { AppContextProvider } from './contexts/app_context'; +import { ProfileContextProvider } from './contexts/profiler_context'; + +import { AppDependencies } from './boot'; + +export function App({ + I18nContext, + licenseEnabled, + notifications, + http, + formatAngularHttpError, +}: AppDependencies) { + return ( + + + +
+ + + + ); +} diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/index.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/index.ts new file mode 100644 index 00000000000000..5e30e07a7b8b73 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { useStore, State } from './store'; +export { Action } from './reducer'; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/reducer.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/reducer.ts new file mode 100644 index 00000000000000..dac9dab9bd092f --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/reducer.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { produce } from 'immer'; +import { Reducer } from 'react'; +import { State } from './store'; + +import { OnHighlightChangeArgs } from '../components/profile_tree'; +import { ShardSerialized, Targets } from '../types'; + +export type Action = + | { type: 'setPristine'; value: boolean } + | { type: 'setProfiling'; value: boolean } + | { type: 'setHighlightDetails'; value: OnHighlightChangeArgs | null } + | { type: 'setActiveTab'; value: Targets | null } + | { type: 'setCurrentResponse'; value: ShardSerialized[] | null }; + +export const reducer: Reducer = (state, action) => + produce(state, draft => { + if (action.type === 'setPristine') { + draft.pristine = action.value; + return; + } + + if (action.type === 'setProfiling') { + draft.profiling = action.value; + if (draft.profiling) { + draft.currentResponse = null; + draft.highlightDetails = null; + } + return; + } + + if (action.type === 'setHighlightDetails') { + draft.highlightDetails = action.value; + return; + } + + if (action.type === 'setActiveTab') { + draft.activeTab = action.value; + return; + } + + if (action.type === 'setCurrentResponse') { + draft.currentResponse = action.value; + if (draft.currentResponse) { + // Default to the searches tab + draft.activeTab = 'searches'; + } + return; + } + + throw new Error(`Unknown action: ${action}`); + }); diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/store.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/store.ts new file mode 100644 index 00000000000000..7b5a1ce93583d6 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/store.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 { useReducer } from 'react'; +import { reducer } from './reducer'; +import { ShardSerialized, Targets } from '../types'; +import { OnHighlightChangeArgs } from '../components/profile_tree'; + +export interface State { + profiling: boolean; + pristine: boolean; + highlightDetails: OnHighlightChangeArgs | null; + activeTab: Targets | null; + currentResponse: ShardSerialized[] | null; +} + +export const initialState: State = { + profiling: false, + pristine: false, + highlightDetails: null, + activeTab: null, + currentResponse: null, +}; + +export const useStore = () => { + const [state, dispatch] = useReducer(reducer, initialState); + return { + state, + dispatch, + }; +}; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/_index.scss b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/_index.scss new file mode 100644 index 00000000000000..a72d079354f897 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/_index.scss @@ -0,0 +1,83 @@ +@import '@elastic/eui/src/components/header/variables'; +@import '@elastic/eui/src/components/panel/mixins'; + +@import 'mixins'; + +@import 'components/highlight_details_flyout'; +@import 'components/profile_tree'; +@import 'components/percentage_badge'; + +@import 'containers/main'; +@import 'containers/profile_query_editor'; + +#searchProfilerAppRoot { + height: 100%; + display: flex; + flex: 1 1 auto; +} + +.prfDevTool__licenseWarning { + &__container { + max-width: 1000px; + } +} + + +.prfDevTool__page { + height: 100%; + display: flex; + flex: 1 1 auto; + + &__body { + height: 100%; + flex: 1 1 auto; + } + + &__pageBody { + height: 100%; + flex: 1 1 auto; + } + + &__pageBodyContent { + height: 100%; + } + + &__pageBodyContentBody { + height: 100%; + } + + &__pageContentBodyContent { + height: 100%; + } + + &__bodyGroup { + height: 100%; + } +} + +.prfDevTool { + height: calc(100vh - #{$euiHeaderChildSize}); + overflow: hidden; + + .devApp__container { + height: 100%; + overflow: hidden; + flex-shrink: 1; + } + + &__container { + overflow: hidden; + } +} + +.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; + } +} diff --git a/x-pack/legacy/plugins/searchprofiler/public/directives/_mixins.scss b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/_mixins.scss similarity index 100% rename from x-pack/legacy/plugins/searchprofiler/public/directives/_mixins.scss rename to x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/_mixins.scss diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/components/_highlight_details_flyout.scss b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/components/_highlight_details_flyout.scss new file mode 100644 index 00000000000000..4b36e9833979d8 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/components/_highlight_details_flyout.scss @@ -0,0 +1,8 @@ +.prfDevTool__flyoutSubtitle { + margin-bottom: $euiSizeS; + display: inline-block; +} + +.prfDevTool__details { + max-width: map-get($euiBreakpoints, 's'); +} diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/components/_percentage_badge.scss b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/components/_percentage_badge.scss new file mode 100644 index 00000000000000..204ca3da17574b --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/components/_percentage_badge.scss @@ -0,0 +1,14 @@ +.prfDevTool__percentBadge { + &__progress--percent { + @include prfDevToolProgress; + width: $badgeSize; + } + + &__progress--time { + @include prfDevToolProgress(#FFAFAF); + background-color: #F5F5F5; // Must be light at all times + // Width of 3 badges, with the last child not having padding on the left + width: ($badgeSize * 3) - ($euiSizeXS * .75); + // Force text to always be dark on top of white -> pink color + } +} diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/components/_profile_tree.scss b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/components/_profile_tree.scss new file mode 100644 index 00000000000000..cc4d334f58fd33 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/components/_profile_tree.scss @@ -0,0 +1,99 @@ + +$badgeSize: $euiSize * 5.5; + +// Profile details treeview + +.prfDevTool__profileTree { + + &__container { + height: 100%; + } + + &__shardDetails--dim small { + color: $euiColorDarkShade; + } + + &__shardDetails { + line-height: 1; + overflow-wrap: break-word; + + h3 { + font-size: $euiSize; + } + + &:disabled { + text-decoration: none !important; + cursor: default; + } + } + + &__shard { + border: none; + + &__header-flex-item { + align-self: center; + } + } + + &__index { + width: 100%; + padding: $euiSize $euiSizeS; + } + + &__indexDetails { + align-self: center; + } + + &__tvRow--last { + cursor: pointer; + } + + &__tvRow, + &__tvHeader { + display: table; + width: 100%; + table-layout: fixed; + } + + &__tvHeader { + @include euiFontSizeXS; + color: $euiColorDarkShade; + } + + &__time, + &__totalTime, + &__percentage { + width: $badgeSize; + } + + // BADGES (and those used for progress) + &__badge { + border: none; + display: block; + // Force text to always be dark on top of white -> pink color + color: lightOrDarkTheme($euiColorDarkestShade, $euiColorLightestShade); + } + + &__panel { + border-bottom: $euiBorderThin; + } + + &__panelBody { + margin-top: $euiSizeS; + margin-left: $euiSizeL; + } + + &__cell { + display: table-cell; + vertical-align: middle; + padding: $euiSizeXS; + + &:first-of-type { + padding-left: 0; + } + + &:last-of-type { + padding-right: 0; + } + } +} diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/containers/_main.scss b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/containers/_main.scss new file mode 100644 index 00000000000000..09bcddef02cc34 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/containers/_main.scss @@ -0,0 +1,62 @@ + +@include euiBreakpoint('xs', 's') { + .prfDevTool__shardDetailsWrapper { + flex-direction: column; + align-items: flex-start; + } +} + +.prfDevTool__main { + height: 100%; + order: 2; + margin-left: $euiSize; + display: flex; + overflow: hidden; + flex-direction: column; + + // Make only the tab content scroll + .search-profiler-tabs { + flex-shrink: 0; + } + + &__profiletree { + height: 100%; + overflow-y: auto; + flex-grow: 1; + } + + &__emptyTreePlaceholder { + h1 { + font-size: $euiSizeL; + color: $euiColorMediumShade; + } + p { + font-size: $euiSize; + color: $euiColorMediumShade; + } + h1, p { + cursor: default; + user-select: none; + } + // Slight offset from the top. + margin: 5% 0 auto; + text-align: center; + } +} + +@include euiPanel('prfDevTool__main'); + +@include euiBreakpoint('xs', 's') { + .prfDevTool__container { + flex-direction: column; + } + + .prfDevTool__main { + flex: 0 0 auto; + } + + .prfDevTool__main { + margin: $euiSize 0; + } +} + diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/containers/_profile_query_editor.scss b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/containers/_profile_query_editor.scss new file mode 100644 index 00000000000000..035ff16c990bb1 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/containers/_profile_query_editor.scss @@ -0,0 +1,25 @@ + +.prfDevTool__sense { + order: 1; + // To anchor ace editor + position: relative; + + // Ace Editor overrides + .ace_editor { + min-height: $euiSize * 10; + flex-grow: 1; + margin-bottom: $euiSize; + margin-top: $euiSize; + outline: solid 1px $euiColorLightShade; + } + + .errorMarker { + position: absolute; + background: rgba($euiColorDanger, .5); + z-index: 20; + } +} + +.prfDevTool__profileButtonContainer { + flex-shrink: 1; +} 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 new file mode 100644 index 00000000000000..2b4dc01c45d6fc --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/types.ts @@ -0,0 +1,69 @@ +/* + * 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 interface BreakdownItem { + tip: string; + key: string; + time: number; + color: string; + relative: number; +} + +export type Targets = 'searches' | 'aggregations'; + +export interface Shard { + id: string[]; + relative: number | string; + time: number; + color: string; + aggregations?: Operation[]; + searches?: Operation[]; + rewrite_time?: number; +} + +export interface Index { + name: string; + time: number; + shards: Shard[]; + visible: boolean; +} + +export interface ShardSerialized { + id: string; + searches: Operation[]; + aggregations: Operation[]; +} + +export interface Operation { + description?: string; + hasChildren: boolean; + visible: boolean; + selfTime: number; + timePercentage: number; + absoluteColor: string; + time: number; + + parent: Operation | null; + children: Operation[]; + + // Only exists on top level + treeRoot?: Operation; + + depth?: number; + + // BWC + type?: string; + + lucene: string | null; + query_type: string | null; + + // Searches + query?: any[]; + rewrite_time?: number; + + // Aggregations + breakdown?: any; +} diff --git a/x-pack/legacy/plugins/searchprofiler/public/__tests__/app_util.js b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/check_for_json_errors.test.ts similarity index 61% rename from x-pack/legacy/plugins/searchprofiler/public/__tests__/app_util.js rename to x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/check_for_json_errors.test.ts index 164e7394dead71..9dece5a39e96c3 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/__tests__/app_util.js +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/check_for_json_errors.test.ts @@ -5,18 +5,18 @@ */ import expect from '@kbn/expect'; -import { checkForParseErrors } from '../app_util.js'; +import { checkForParseErrors } from '.'; -describe('checkForParseErrors', function () { - it('returns false from bad JSON', function () { +describe('checkForParseErrors', function() { + it('returns error from bad JSON', function() { const json = '{"foo": {"bar": {"baz": "buzz}}}'; const result = checkForParseErrors(json); - expect(result.status).to.be(false); + expect(result.error.message).to.be(`Unexpected end of JSON input`); }); - it('returns true from good JSON', function () { + it('returns parsed value from good JSON', function() { const json = '{"foo": {"bar": {"baz": "buzz"}}}'; const result = checkForParseErrors(json); - expect(result.status).to.be(true); + expect(!!result.parsed).to.be(true); }); }); diff --git a/x-pack/legacy/plugins/searchprofiler/public/app_util.js b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/check_for_json_errors.ts similarity index 71% rename from x-pack/legacy/plugins/searchprofiler/public/app_util.js rename to x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/check_for_json_errors.ts index 18eef2259bd6ac..4267fd0d2f9019 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/app_util.js +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/check_for_json_errors.ts @@ -5,18 +5,18 @@ */ // Convert triple quotes into regular quotes and escape internal quotes. -function collapseLiteralStrings(data) { - return data.replace(/"""(?:\s*\r?\n)?((?:.|\r?\n)*?)(?:\r?\n\s*)?"""/g, function (match, literal) { +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) { +export function checkForParseErrors(json: string) { const sanitizedJson = collapseLiteralStrings(json); try { const parsedJson = JSON.parse(sanitizedJson); - return { status: true, parsed: parsedJson }; + return { parsed: parsedJson, error: null }; } catch (error) { - return { status: false, 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 new file mode 100644 index 00000000000000..556a03fc96fe35 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/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; + * 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'; diff --git a/x-pack/legacy/plugins/searchprofiler/public/filters/ms_to_pretty.js b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/ms_to_pretty.ts similarity index 91% rename from x-pack/legacy/plugins/searchprofiler/public/filters/ms_to_pretty.js rename to x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/ms_to_pretty.ts index aa088ffc46c596..3df47cfdeb1770 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/filters/ms_to_pretty.js +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/ms_to_pretty.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export function msToPretty(ms, precision) { +export function msToPretty(ms: number, precision: number) { if (!precision) { precision = 1; } diff --git a/x-pack/legacy/plugins/searchprofiler/public/filters/__tests__/ns_to_pretty.js b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/ns_to_pretty.test.ts similarity index 60% rename from x-pack/legacy/plugins/searchprofiler/public/filters/__tests__/ns_to_pretty.js rename to x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/ns_to_pretty.test.ts index 85208495fc18e2..6c0e1124af176c 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/filters/__tests__/ns_to_pretty.js +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/ns_to_pretty.test.ts @@ -5,45 +5,45 @@ */ import expect from '@kbn/expect'; -import { nsToPretty } from '../ns_to_pretty.js'; +import { nsToPretty } from './ns_to_pretty'; -describe('nsToPretty', function () { - it('returns correct time for ns', function () { +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 () { + 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 () { + 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 () { + 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 () { + 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.6e+12 * 5, 1); + 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.6e+12 * 24 * 5, 1); + 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 () { + 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/public/filters/ns_to_pretty.js b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/ns_to_pretty.ts similarity index 89% rename from x-pack/legacy/plugins/searchprofiler/public/filters/ns_to_pretty.js rename to x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/ns_to_pretty.ts index 74737ed8e30d4b..bf0b487566f6fe 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/filters/ns_to_pretty.js +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/utils/ns_to_pretty.ts @@ -6,7 +6,7 @@ import { msToPretty } from './ms_to_pretty'; -export function nsToPretty(ns, precision) { +export function nsToPretty(ns: number, precision: number) { if (!precision) { precision = 1; } diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/index.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/index.ts new file mode 100644 index 00000000000000..3d77f703b42cd4 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/index.ts @@ -0,0 +1,12 @@ +/* + * 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 { 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/public/np_ready/plugin.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/plugin.ts new file mode 100644 index 00000000000000..ff98e706abf5cd --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/plugin.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. + */ + +import { + Plugin, + CoreStart, + CoreSetup, + PluginInitializerContext, + ToastsStart, +} from 'src/core/public'; + +import { boot } from './application/boot'; + +export class SearchProfilerUIPlugin implements Plugin { + constructor(ctx: PluginInitializerContext) {} + + async setup( + core: CoreSetup, + plugins: { + __LEGACY: { + I18nContext: any; + licenseEnabled: boolean; + notifications: ToastsStart; + formatAngularHttpError: any; + el: HTMLElement; + }; + } + ) { + const { http } = core; + const { + __LEGACY: { I18nContext, licenseEnabled, notifications, formatAngularHttpError, el }, + } = 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) {} + + async stop() {} +} diff --git a/x-pack/legacy/plugins/searchprofiler/public/range.js b/x-pack/legacy/plugins/searchprofiler/public/range.js deleted file mode 100644 index d52ed783d9720a..00000000000000 --- a/x-pack/legacy/plugins/searchprofiler/public/range.js +++ /dev/null @@ -1,257 +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. - */ - - -// Pulled from Ace because I can't for the life of me -// figure out how to import it. This needs to be fixed TODO - -const comparePoints = function (p1, p2) { - return p1.row - p2.row || p1.column - p2.column; -}; - -export function Range(startRow, startColumn, endRow, endColumn) { - this.start = { - row: startRow, - column: startColumn - }; - - this.end = { - row: endRow, - column: endColumn - }; -} - -(function () { - this.isEqual = function (range) { - return this.start.row === range.start.row && - this.end.row === range.end.row && - this.start.column === range.start.column && - this.end.column === range.end.column; - }; - this.toString = function () { - return ('Range: [' + this.start.row + '/' + this.start.column + - '] -> [' + this.end.row + '/' + this.end.column + ']'); - }; - - this.contains = function (row, column) { - return this.compare(row, column) === 0; - }; - this.compareRange = function (range) { - let cmp; - const end = range.end; - const start = range.start; - - cmp = this.compare(end.row, end.column); - if (cmp === 1) { - cmp = this.compare(start.row, start.column); - if (cmp === 1) { - return 2; - } else if (cmp === 0) { - return 1; - } else { - return 0; - } - } else if (cmp === -1) { - return -2; - } else { - cmp = this.compare(start.row, start.column); - if (cmp === -1) { - return -1; - } else if (cmp === 1) { - return 42; - } else { - return 0; - } - } - }; - this.comparePoint = function (p) { - return this.compare(p.row, p.column); - }; - this.containsRange = function (range) { - return this.comparePoint(range.start) === 0 && this.comparePoint(range.end) === 0; - }; - this.intersects = function (range) { - const cmp = this.compareRange(range); - return (cmp === -1 || cmp === 0 || cmp === 1); - }; - this.isEnd = function (row, column) { - return this.end.row === row && this.end.column === column; - }; - this.isStart = function (row, column) { - return this.start.row === row && this.start.column === column; - }; - this.setStart = function (row, column) { - if (typeof row === 'object') { - this.start.column = row.column; - this.start.row = row.row; - } else { - this.start.row = row; - this.start.column = column; - } - }; - this.setEnd = function (row, column) { - if (typeof row === 'object') { - this.end.column = row.column; - this.end.row = row.row; - } else { - this.end.row = row; - this.end.column = column; - } - }; - this.inside = function (row, column) { - if (this.compare(row, column) === 0) { - if (this.isEnd(row, column) || this.isStart(row, column)) { - return false; - } else { - return true; - } - } - return false; - }; - this.insideStart = function (row, column) { - if (this.compare(row, column) === 0) { - if (this.isEnd(row, column)) { - return false; - } else { - return true; - } - } - return false; - }; - this.insideEnd = function (row, column) { - if (this.compare(row, column) === 0) { - if (this.isStart(row, column)) { - return false; - } else { - return true; - } - } - return false; - }; - this.compare = function (row, column) { - if (!this.isMultiLine()) { - if (row === this.start.row) { - if (column < this.start.column) { - return -1; - } - return column > this.end.column ? 1 : 0; - } - } - - if (row < this.start.row) { - return -1; - } - - - if (row > this.end.row) { - return 1; - } - - if (this.start.row === row) { - return column >= this.start.column ? 0 : -1; - } - - if (this.end.row === row) { - return column <= this.end.column ? 0 : 1; - } - - return 0; - }; - this.compareStart = function (row, column) { - if (this.start.row === row && this.start.column === column) { - return -1; - } else { - return this.compare(row, column); - } - }; - this.compareEnd = function (row, column) { - if (this.end.row === row && this.end.column === column) { - return 1; - } else { - return this.compare(row, column); - } - }; - this.compareInside = function (row, column) { - if (this.end.row === row && this.end.column === column) { - return 1; - } else if (this.start.row === row && this.start.column === column) { - return -1; - } else { - return this.compare(row, column); - } - }; - this.clipRows = function (firstRow, lastRow) { - let end; - let start; - if (this.end.row > lastRow) { - end = { row: lastRow + 1, column: 0 }; - } else if (this.end.row < firstRow) { - end = { row: firstRow, column: 0 }; - } - - if (this.start.row > lastRow) { - start = { row: lastRow + 1, column: 0 }; - } else if (this.start.row < firstRow) { - start = { row: firstRow, column: 0 }; - } - return Range.fromPoints(start || this.start, end || this.end); - }; - this.extend = function (row, column) { - const cmp = this.compare(row, column); - - if (cmp === 0) { - return this; - } - let start; - let end; - if (cmp === -1) { - start = { row: row, column: column }; - } else { - end = { row: row, column: column }; - } - return Range.fromPoints(start || this.start, end || this.end); - }; - - this.isEmpty = function () { - return (this.start.row === this.end.row && this.start.column === this.end.column); - }; - this.isMultiLine = function () { - return (this.start.row !== this.end.row); - }; - this.clone = function () { - return Range.fromPoints(this.start, this.end); - }; - this.collapseRows = function () { - if (this.end.column === 0) { - return new Range(this.start.row, 0, Math.max(this.start.row, this.end.row - 1), 0); - } - return new Range(this.start.row, 0, this.end.row, 0); - }; - this.toScreenRange = function (session) { - const screenPosStart = session.documentToScreenPosition(this.start); - const screenPosEnd = session.documentToScreenPosition(this.end); - - return new Range( - screenPosStart.row, screenPosStart.column, - screenPosEnd.row, screenPosEnd.column - ); - }; - this.moveBy = function (row, column) { - this.start.row += row; - this.start.column += column; - this.end.row += row; - this.end.column += column; - }; - -}).call(Range.prototype); -Range.fromPoints = function (start, end) { - return new Range(start.row, start.column, end.row, end.column); -}; -Range.comparePoints = comparePoints; - -Range.comparePoints = function (p1, p2) { - return p1.row - p2.row || p1.column - p2.column; -}; diff --git a/x-pack/legacy/plugins/searchprofiler/public/templates/_index.scss b/x-pack/legacy/plugins/searchprofiler/public/templates/_index.scss deleted file mode 100644 index 495452735ac83e..00000000000000 --- a/x-pack/legacy/plugins/searchprofiler/public/templates/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'templates'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/searchprofiler/public/templates/_templates.scss b/x-pack/legacy/plugins/searchprofiler/public/templates/_templates.scss deleted file mode 100644 index 9acd5cf32051ec..00000000000000 --- a/x-pack/legacy/plugins/searchprofiler/public/templates/_templates.scss +++ /dev/null @@ -1,103 +0,0 @@ -@import '@elastic/eui/src/components/header/variables'; -@import '@elastic/eui/src/components/panel/mixins'; - -.prfDevTool { - height: calc(100vh - #{$euiHeaderChildSize}); - overflow: hidden; - - .devApp__container { - overflow: hidden; - flex-shrink: 1; - } -} - -.prfDevTool__container { - display: flex; - flex: 1 1 auto; - padding: $euiSize; - overflow: hidden; - - // SASSTODO/EUITODO/HACK: Reset font styles of headers - h1, h2, h3, h4, h5, h6 { - font: inherit; - } -} - -@include euiPanel('prfDevTool__main'); - -.prfDevTool__main { - flex: 3; - order: 2; - margin-left: $euiSize; - display: flex; - overflow: hidden; - flex-direction: column; - - // Make only the tab content scroll - search-profiler-tabs { - flex-shrink: 0; - } - - profiletree { - overflow-y: auto; - flex-grow: 1; - } -} - -.prfDevTool__details { - max-width: map-get($euiBreakpoints, 's'); -} - -highlightdetails { - flex-grow: 1; - display: flex; - flex-direction: column; - overflow: hidden; - height: 100%; -} - -.prfDevTool__sense { - flex: 1; - order: 1; - position: relative; - display: flex; - flex-direction: column; - - > * { - flex-shrink: 0; - } - - .kuiTextInput { - width: 100%; - } - - // Ace Editor overrides - .ace_editor { - min-height: $euiSize * 10; - flex-grow: 1; - margin-bottom: $euiSize; - margin-top: $euiSize; - outline: solid 1px $euiColorLightShade; - } - - .errorMarker { - position: absolute; - background: rgba($euiColorDanger, .5); - z-index: 20; - } -} - -@include euiBreakpoint('xs', 's') { - .prfDevTool__container { - flex-direction: column; - } - - .prfDevTool__sense, - .prfDevTool__main { - flex: 0 0 auto; - } - - .prfDevTool__main { - margin: $euiSize 0; - } -} diff --git a/x-pack/legacy/plugins/searchprofiler/public/templates/index.html b/x-pack/legacy/plugins/searchprofiler/public/templates/index.html deleted file mode 100644 index a2874af17817e2..00000000000000 --- a/x-pack/legacy/plugins/searchprofiler/public/templates/index.html +++ /dev/null @@ -1,113 +0,0 @@ - -
-
- - - - - - - - -
-

- -

- -

-
-
- -
- -
-
-
- -
- -
- -
-
- -
-
- -
- -
- -
-
-
- - -
- - -
-
- - -
-
diff --git a/x-pack/legacy/plugins/searchprofiler/server/np_ready/index.ts b/x-pack/legacy/plugins/searchprofiler/server/np_ready/index.ts new file mode 100644 index 00000000000000..9253a0d6b15242 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/server/np_ready/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { SearchProfilerServerPlugin } from './plugin'; + +export const plugin = () => { + return new SearchProfilerServerPlugin(); +}; diff --git a/x-pack/legacy/plugins/searchprofiler/server/lib/__tests__/check_license.js b/x-pack/legacy/plugins/searchprofiler/server/np_ready/lib/check_license.test.ts similarity index 80% rename from x-pack/legacy/plugins/searchprofiler/server/lib/__tests__/check_license.js rename to x-pack/legacy/plugins/searchprofiler/server/np_ready/lib/check_license.test.ts index f1e19b61cbe2b0..f473533fbd48ae 100644 --- a/x-pack/legacy/plugins/searchprofiler/server/lib/__tests__/check_license.js +++ b/x-pack/legacy/plugins/searchprofiler/server/np_ready/lib/check_license.test.ts @@ -6,15 +6,14 @@ import expect from '@kbn/expect'; import { set } from 'lodash'; -import { checkLicense } from '../check_license'; +import { checkLicense } from './check_license'; -describe('check_license', function () { - - let mockLicenseInfo; - beforeEach(() => mockLicenseInfo = {}); +describe('check_license', () => { + let mockLicenseInfo: any; + beforeEach(() => (mockLicenseInfo = {})); describe('license information is not available', () => { - beforeEach(() => mockLicenseInfo.isAvailable = () => false); + beforeEach(() => (mockLicenseInfo.isAvailable = () => false)); it('should set showLinks to true', () => { expect(checkLicense(mockLicenseInfo).showAppLink).to.be(true); @@ -37,11 +36,11 @@ describe('check_license', function () { describe('& license is active', () => { beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); - it ('should set showLinks to true', () => { + it('should set showLinks to true', () => { expect(checkLicense(mockLicenseInfo).showAppLink).to.be(true); }); - it ('should set enableLinks to true', () => { + it('should set enableLinks to true', () => { expect(checkLicense(mockLicenseInfo).enableAppLink).to.be(true); }); }); @@ -49,11 +48,11 @@ describe('check_license', function () { describe('& license is expired', () => { beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false)); - it ('should set showLinks to true', () => { + it('should set showLinks to true', () => { expect(checkLicense(mockLicenseInfo).showAppLink).to.be(true); }); - it ('should set enableLinks to false', () => { + it('should set enableLinks to false', () => { expect(checkLicense(mockLicenseInfo).enableAppLink).to.be(false); }); }); @@ -65,7 +64,7 @@ describe('check_license', function () { describe('& license is active', () => { beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); - it ('should set showLinks to false', () => { + it('should set showLinks to false', () => { expect(checkLicense(mockLicenseInfo).showAppLink).to.be(false); }); }); @@ -73,7 +72,7 @@ describe('check_license', function () { describe('& license is expired', () => { beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false)); - it ('should set showLinks to false', () => { + it('should set showLinks to false', () => { expect(checkLicense(mockLicenseInfo).showAppLink).to.be(false); }); }); diff --git a/x-pack/legacy/plugins/searchprofiler/server/lib/check_license.js b/x-pack/legacy/plugins/searchprofiler/server/np_ready/lib/check_license.ts similarity index 67% rename from x-pack/legacy/plugins/searchprofiler/server/lib/check_license.js rename to x-pack/legacy/plugins/searchprofiler/server/np_ready/lib/check_license.ts index 1093daaafe18a8..e41925da3e2688 100644 --- a/x-pack/legacy/plugins/searchprofiler/server/lib/check_license.js +++ b/x-pack/legacy/plugins/searchprofiler/server/np_ready/lib/check_license.ts @@ -5,43 +5,46 @@ */ import { i18n } from '@kbn/i18n'; +import { XPackInfo } from '../../../../xpack_main/server/lib/xpack_info'; -export function checkLicense(xpackLicenseInfo) { - +export function checkLicense( + xpackLicenseInfo: XPackInfo +): { showAppLink: boolean; enableAppLink: boolean; message: string | undefined } { if (!xpackLicenseInfo || !xpackLicenseInfo.isAvailable()) { return { showAppLink: true, enableAppLink: false, message: i18n.translate('xpack.searchProfiler.unavailableLicenseInformationMessage', { - defaultMessage: 'Search Profiler is unavailable - license information is not available at this time.', + defaultMessage: + 'Search Profiler is unavailable - license information is not available at this time.', }), }; } const isLicenseActive = xpackLicenseInfo.license.isActive(); - let message; + let message: string | undefined; if (!isLicenseActive) { message = i18n.translate('xpack.searchProfiler.licenseHasExpiredMessage', { defaultMessage: 'Search Profiler is unavailable - license has expired.', }); } - if (xpackLicenseInfo.license.isOneOf([ 'trial', 'basic', 'standard', 'gold', 'platinum' ])) { + if (xpackLicenseInfo.license.isOneOf(['trial', 'basic', 'standard', 'gold', 'platinum'])) { return { showAppLink: true, enableAppLink: isLicenseActive, - message + message, }; } message = i18n.translate('xpack.searchProfiler.upgradeLicenseMessage', { defaultMessage: 'Search Profiler is unavailable for the current {licenseInfo} license. Please upgrade your license.', - values: { licenseInfo: xpackLicenseInfo.license.getType() } + values: { licenseInfo: xpackLicenseInfo.license.getType() }, }); return { showAppLink: false, enableAppLink: false, - message + message, }; } diff --git a/x-pack/legacy/plugins/searchprofiler/server/np_ready/lib/index.ts b/x-pack/legacy/plugins/searchprofiler/server/np_ready/lib/index.ts new file mode 100644 index 00000000000000..f2c070fd44b6e6 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/server/np_ready/lib/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 { checkLicense } from './check_license'; diff --git a/x-pack/legacy/plugins/searchprofiler/server/np_ready/plugin.ts b/x-pack/legacy/plugins/searchprofiler/server/np_ready/plugin.ts new file mode 100644 index 00000000000000..e00e2829f980d2 --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/server/np_ready/plugin.ts @@ -0,0 +1,38 @@ +/* + * 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 { CoreSetup, Plugin } from 'src/core/server'; +import { LegacySetup } from './types'; +import { checkLicense } from './lib'; +// @ts-ignore +import { mirrorPluginStatus } from '../../../../server/lib/mirror_plugin_status'; + +import * as profileRoute from './routes/profile'; + +export class SearchProfilerServerPlugin implements Plugin { + async setup( + core: CoreSetup, + { + route, + plugins: { + __LEGACY: { thisPlugin, elasticsearch, xpackMain, commonRouteConfig }, + }, + }: LegacySetup + ) { + mirrorPluginStatus(xpackMain, thisPlugin); + (xpackMain as any).status.once('green', () => { + // Register a function that is called whenever the xpack info changes, + // to re-compute the license check results for this plugin + xpackMain.info.feature(thisPlugin.id).registerLicenseCheckResultsGenerator(checkLicense); + }); + + profileRoute.register({ elasticsearch }, route, commonRouteConfig); + } + + async start() {} + + stop(): void {} +} diff --git a/x-pack/legacy/plugins/searchprofiler/server/np_ready/routes/profile.ts b/x-pack/legacy/plugins/searchprofiler/server/np_ready/routes/profile.ts new file mode 100644 index 00000000000000..082307b5a7a2be --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/server/np_ready/routes/profile.ts @@ -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 Joi from 'joi'; +import { RequestShim, ServerShim, RegisterRoute } from '../types'; + +export const handler = async (server: ServerShim, request: RequestShim) => { + const { callWithRequest } = server.elasticsearch.getCluster('data'); + let parsed = request.payload.query; + parsed.profile = true; + parsed = JSON.stringify(parsed, null, 2); + + const body = { + index: request.payload.index, + body: parsed, + }; + try { + const resp = await callWithRequest(request, 'search', body); + return { + ok: true, + resp, + }; + } catch (err) { + return { + ok: false, + err, + }; + } +}; + +export const register = (server: ServerShim, route: RegisterRoute, commonConfig: any) => { + route({ + path: '/api/searchprofiler/profile', + method: 'POST', + config: { + ...commonConfig, + validate: { + payload: Joi.object() + .keys({ + query: Joi.object().required(), + index: Joi.string().required(), + type: Joi.string().optional(), + }) + .required(), + }, + }, + handler: req => { + return handler(server, { headers: req.headers, payload: req.payload as any }); + }, + }); +}; diff --git a/x-pack/legacy/plugins/searchprofiler/server/np_ready/types.ts b/x-pack/legacy/plugins/searchprofiler/server/np_ready/types.ts new file mode 100644 index 00000000000000..7862aa386785bd --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/server/np_ready/types.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 { ServerRoute } from 'hapi'; +import { ElasticsearchPlugin, Request } from 'src/legacy/core_plugins/elasticsearch'; +import { XPackMainPlugin } from '../../../xpack_main/xpack_main'; + +export type RegisterRoute = (args: ServerRoute & { config: any }) => void; + +export interface LegacyPlugins { + __LEGACY: { + thisPlugin: any; + elasticsearch: ElasticsearchPlugin; + xpackMain: XPackMainPlugin; + commonRouteConfig: any; + }; +} + +export interface LegacySetup { + route: RegisterRoute; + plugins: LegacyPlugins; +} + +export interface ServerShim { + elasticsearch: ElasticsearchPlugin; +} + +export interface RequestShim extends Request { + payload: any; +} diff --git a/x-pack/legacy/plugins/searchprofiler/server/routes/profile.js b/x-pack/legacy/plugins/searchprofiler/server/routes/profile.js deleted file mode 100644 index b1ba7527b69dd8..00000000000000 --- a/x-pack/legacy/plugins/searchprofiler/server/routes/profile.js +++ /dev/null @@ -1,51 +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 Joi from 'joi'; - -export function profileRoute(server, commonRouteConfig) { - - server.route({ - path: '/api/searchprofiler/profile', - method: 'POST', - config: { - ...commonRouteConfig, - validate: { - payload: Joi.object().keys({ - query: Joi.object().required(), - index: Joi.string().required(), - type: Joi.string().optional() - }).required() //Joi validation - } - }, - handler: async (request) => { - - const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); - let parsed = request.payload.query; - parsed.profile = true; - parsed = JSON.stringify(parsed, null, 2); - - const body = { - index: request.payload.index, - type: request.payload.type, - body: parsed - }; - try { - const resp = await callWithRequest(request, 'search', body); - return { - ok: true, - resp: resp - }; - } catch (err) { - return { - ok: false, - err: err - }; - } - } - }); - -} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index eb748a5739b34f..cd917b153f2673 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8348,7 +8348,6 @@ "xpack.searchProfiler.basicLicenseTitle": "ベーシック", "xpack.searchProfiler.formIndexLabel": "インデックス", "xpack.searchProfiler.formProfileButtonLabel": "プロフィール", - "xpack.searchProfiler.formTypeLabel": "タイプ", "xpack.searchProfiler.goldLicenseTitle": "ゴールド", "xpack.searchProfiler.highlightDetails.descriptionTitle": "説明", "xpack.searchProfiler.highlightDetails.selfTimeTitle": "セルフタイム", @@ -8358,7 +8357,7 @@ "xpack.searchProfiler.highlightDetails.totalTimeTooltip": "子を除き、このクエリコンポーネントだけに使用された合計時間です", "xpack.searchProfiler.highlightDetails.typeTitle": "タイプ", "xpack.searchProfiler.licenseErrorMessageDescription": "さらに可視化するには有効なライセンス ({licenseTypeList} または {platinumLicenseType}), が必要ですが、クラスターに見つかりませんでした。", - "xpack.searchProfiler.licenseErrorMessageTitle": "{warningIcon} ライセンスエラー", + "xpack.searchProfiler.licenseErrorMessageTitle": "ライセンスエラー", "xpack.searchProfiler.licenseHasExpiredMessage": "検索プロフィールを利用できません。ライセンスが期限切れです。", "xpack.searchProfiler.pageDisplayName": "検索プロファイラー", "xpack.searchProfiler.platinumLicenseTitle": "プラチナ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 053fe90b813792..80a1cbabdd3a28 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8503,7 +8503,6 @@ "xpack.searchProfiler.basicLicenseTitle": "基础级", "xpack.searchProfiler.formIndexLabel": "索引", "xpack.searchProfiler.formProfileButtonLabel": "配置文件", - "xpack.searchProfiler.formTypeLabel": "类型", "xpack.searchProfiler.goldLicenseTitle": "黄金级", "xpack.searchProfiler.highlightDetails.descriptionTitle": "描述", "xpack.searchProfiler.highlightDetails.selfTimeTitle": "独自时间", @@ -8513,7 +8512,7 @@ "xpack.searchProfiler.highlightDetails.totalTimeTooltip": "此查询组件花费的总时间(包括子项)", "xpack.searchProfiler.highlightDetails.typeTitle": "类型", "xpack.searchProfiler.licenseErrorMessageDescription": "分析器可视化需要有效的许可({licenseTypeList}或{platinumLicenseType},但在您的集群中未找到任何许可。", - "xpack.searchProfiler.licenseErrorMessageTitle": "{warningIcon} 许可错误", + "xpack.searchProfiler.licenseErrorMessageTitle": "许可错误", "xpack.searchProfiler.licenseHasExpiredMessage": "Search Profiler 不可用 - 许可已过期。", "xpack.searchProfiler.pageDisplayName": "Search Profiler", "xpack.searchProfiler.platinumLicenseTitle": "白金级",