[SearchProfiler] Move out of legacy (#55331) (#55543)

* Initial move of searchprofiler into new platform directory, lots of things need testing

* Whitespace, clean up types and remove unused files

* First iteration of end-to-end plugin working

- Updated license check to only check for presence of basic license (not search profiler as a feature
- Updated the payload: removed types from validation
- Also added README in public regarding the location of styles

* Added extractProfilerErrorMessage function to interface with new error reporting from profiler endpoint

* Fix paths to test_utils

* Update I18n for search profiler

* Fix react hooks ordering bug with license status updates and fix test (wait for first license object before rendering)

* Added index.ts file to common in searchprofiler route
Marked types and values as internal
Removed unnecessary "async" from function
Update import to not use "src" alias

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Jean-Louis Leysens 2020-01-22 16:49:16 +01:00 committed by GitHub
parent 0c6ed2aa14
commit 582faa0eb1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
104 changed files with 527 additions and 509 deletions

156
.github/CODEOWNERS vendored Normal file
View file

@ -0,0 +1,156 @@
# GitHub CODEOWNERS definition
# Identify which groups will be pinged by changes to different parts of the codebase.
# For more info, see https://help.github.com/articles/about-codeowners/
# App
/x-pack/legacy/plugins/lens/ @elastic/kibana-app
/x-pack/legacy/plugins/graph/ @elastic/kibana-app
/src/plugins/share/ @elastic/kibana-app
/src/legacy/server/url_shortening/ @elastic/kibana-app
/src/legacy/server/sample_data/ @elastic/kibana-app
/src/legacy/core_plugins/kibana/public/dashboard/ @elastic/kibana-app
/src/legacy/core_plugins/kibana/public/discover/ @elastic/kibana-app
/src/legacy/core_plugins/kibana/public/visualize/ @elastic/kibana-app
/src/legacy/core_plugins/kibana/public/local_application_service/ @elastic/kibana-app
/src/legacy/core_plugins/kibana/public/home/ @elastic/kibana-app
/src/legacy/core_plugins/kibana/public/dev_tools/ @elastic/kibana-app
/src/legacy/core_plugins/metrics/ @elastic/kibana-app
/src/legacy/core_plugins/vis_type_vislib/ @elastic/kibana-app
/src/plugins/home/ @elastic/kibana-app
/src/plugins/kibana_legacy/ @elastic/kibana-app
/src/plugins/timelion/ @elastic/kibana-app
/src/plugins/dev_tools/ @elastic/kibana-app
# App Architecture
/src/plugins/data/ @elastic/kibana-app-arch
/src/plugins/embeddable/ @elastic/kibana-app-arch
/src/plugins/expressions/ @elastic/kibana-app-arch
/src/plugins/kibana_react/ @elastic/kibana-app-arch
/src/plugins/kibana_utils/ @elastic/kibana-app-arch
/src/plugins/navigation/ @elastic/kibana-app-arch
/src/plugins/ui_actions/ @elastic/kibana-app-arch
/src/plugins/visualizations/ @elastic/kibana-app-arch
/x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch
/src/legacy/core_plugins/data/ @elastic/kibana-app-arch
/src/legacy/core_plugins/elasticsearch/lib/create_proxy.js @elastic/kibana-app-arch
/src/legacy/core_plugins/embeddable_api/ @elastic/kibana-app-arch
/src/legacy/core_plugins/interpreter/ @elastic/kibana-app-arch
/src/legacy/core_plugins/kibana_react/ @elastic/kibana-app-arch
/src/legacy/core_plugins/kibana/public/management/ @elastic/kibana-app-arch
/src/legacy/core_plugins/kibana/server/field_formats/ @elastic/kibana-app-arch
/src/legacy/core_plugins/kibana/server/routes/api/management/ @elastic/kibana-app-arch
/src/legacy/core_plugins/kibana/server/routes/api/suggestions/ @elastic/kibana-app-arch
/src/legacy/core_plugins/visualizations/ @elastic/kibana-app-arch
/src/legacy/server/index_patterns/ @elastic/kibana-app-arch
# APM
/x-pack/legacy/plugins/apm/ @elastic/apm-ui
/x-pack/test/functional/apps/apm/ @elastic/apm-ui
/src/legacy/core_plugins/apm_oss/ @elastic/apm-ui
# Beats
/x-pack/legacy/plugins/beats_management/ @elastic/beats
# Canvas
/x-pack/legacy/plugins/canvas/ @elastic/kibana-canvas
# Logs & Metrics UI
/x-pack/legacy/plugins/infra/ @elastic/logs-metrics-ui
/x-pack/legacy/plugins/integrations_manager/ @elastic/epm
# Machine Learning
/x-pack/legacy/plugins/ml/ @elastic/ml-ui
/x-pack/test/functional/apps/machine_learning/ @elastic/ml-ui
/x-pack/test/functional/services/machine_learning/ @elastic/ml-ui
/x-pack/test/functional/services/ml.ts @elastic/ml-ui
# ML team owns the transform plugin, ES team added here for visibility
# because the plugin lives in Kibana's Elasticsearch management section.
/x-pack/legacy/plugins/transform/ @elastic/ml-ui @elastic/es-ui
/x-pack/test/functional/apps/transform/ @elastic/ml-ui
/x-pack/test/functional/services/transform_ui/ @elastic/ml-ui
/x-pack/test/functional/services/transform.ts @elastic/ml-ui
# Maps
/x-pack/legacy/plugins/maps/ @elastic/kibana-gis
/x-pack/test/api_integration/apis/maps/ @elastic/kibana-gis
/x-pack/test/functional/apps/maps/ @elastic/kibana-gis
/x-pack/test/functional/es_archives/maps/ @elastic/kibana-gis
/x-pack/test/visual_regression/tests/maps/index.js @elastic/kibana-gis
# Operations
/src/dev/ @elastic/kibana-operations
/src/setup_node_env/ @elastic/kibana-operations
/src/optimize/ @elastic/kibana-operations
/packages/*eslint*/ @elastic/kibana-operations
/packages/*babel*/ @elastic/kibana-operations
/packages/kbn-dev-utils*/ @elastic/kibana-operations
/packages/kbn-es/ @elastic/kibana-operations
/packages/kbn-pm/ @elastic/kibana-operations
/packages/kbn-test/ @elastic/kibana-operations
/packages/kbn-ui-shared-deps/ @elastic/kibana-operations
/src/legacy/server/keystore/ @elastic/kibana-operations
/src/legacy/server/pid/ @elastic/kibana-operations
/src/legacy/server/sass/ @elastic/kibana-operations
/src/legacy/server/utils/ @elastic/kibana-operations
/src/legacy/server/warnings/ @elastic/kibana-operations
# Platform
/src/core/ @elastic/kibana-platform
/config/kibana.yml @elastic/kibana-platform
/x-pack/plugins/features/ @elastic/kibana-platform
/x-pack/plugins/licensing/ @elastic/kibana-platform
/packages/kbn-config-schema/ @elastic/kibana-platform
/src/legacy/server/config/ @elastic/kibana-platform
/src/legacy/server/http/ @elastic/kibana-platform
/src/legacy/server/i18n/ @elastic/kibana-platform
/src/legacy/server/logging/ @elastic/kibana-platform
/src/legacy/server/saved_objects/ @elastic/kibana-platform
/src/legacy/server/status/ @elastic/kibana-platform
# Security
/src/core/server/csp/ @elastic/kibana-security @elastic/kibana-platform
/x-pack/legacy/plugins/security/ @elastic/kibana-security
/x-pack/legacy/plugins/spaces/ @elastic/kibana-security
/x-pack/plugins/spaces/ @elastic/kibana-security
/x-pack/legacy/plugins/encrypted_saved_objects/ @elastic/kibana-security
/x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security
/x-pack/plugins/security/ @elastic/kibana-security
/x-pack/test/api_integration/apis/security/ @elastic/kibana-security
# Kibana Localization
/src/dev/i18n/ @elastic/kibana-localization
# Pulse
/packages/kbn-analytics/ @elastic/pulse
/src/legacy/core_plugins/ui_metric/ @elastic/pulse
/src/plugins/usage_collection/ @elastic/pulse
/x-pack/legacy/plugins/telemetry/ @elastic/pulse
# Kibana Alerting Services
/x-pack/legacy/plugins/alerting/ @elastic/kibana-alerting-services
/x-pack/legacy/plugins/actions/ @elastic/kibana-alerting-services
/x-pack/plugins/event_log/ @elastic/kibana-alerting-services
/x-pack/plugins/task_manager/ @elastic/kibana-alerting-services
/x-pack/test/alerting_api_integration/ @elastic/kibana-alerting-services
/x-pack/test/plugin_api_integration/plugins/task_manager/ @elastic/kibana-alerting-services
/x-pack/test/plugin_api_integration/test_suites/task_manager/ @elastic/kibana-alerting-services
/x-pack/legacy/plugins/triggers_actions_ui/ @elastic/kibana-alerting-services
/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/ @elastic/kibana-alerting-services
/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/ @elastic/kibana-alerting-services
# Design
**/*.scss @elastic/kibana-design
# Elasticsearch UI
/src/legacy/core_plugins/console/ @elastic/es-ui
/src/plugins/es_ui_shared/ @elastic/es-ui
/x-pack/legacy/plugins/console_extensions/ @elastic/es-ui
/x-pack/legacy/plugins/cross_cluster_replication/ @elastic/es-ui
/x-pack/legacy/plugins/index_lifecycle_management/ @elastic/es-ui
/x-pack/legacy/plugins/index_management/ @elastic/es-ui
/x-pack/legacy/plugins/license_management/ @elastic/es-ui
/x-pack/legacy/plugins/remote_clusters/ @elastic/es-ui
/x-pack/legacy/plugins/rollup/ @elastic/es-ui
/x-pack/plugins/searchprofiler/ @elastic/es-ui
/x-pack/legacy/plugins/snapshot_restore/ @elastic/es-ui
/x-pack/legacy/plugins/watcher/ @elastic/es-ui

View file

@ -27,9 +27,9 @@
"xpack.main": "legacy/plugins/xpack_main",
"xpack.monitoring": "legacy/plugins/monitoring",
"xpack.remoteClusters": "legacy/plugins/remote_clusters",
"xpack.reporting": [ "plugins/reporting", "legacy/plugins/reporting" ],
"xpack.reporting": ["plugins/reporting", "legacy/plugins/reporting"],
"xpack.rollupJobs": "legacy/plugins/rollup",
"xpack.searchProfiler": "legacy/plugins/searchprofiler",
"xpack.searchProfiler": "plugins/searchprofiler",
"xpack.siem": "legacy/plugins/siem",
"xpack.security": ["legacy/plugins/security", "plugins/security"],
"xpack.server": "legacy/server",

View file

@ -1,26 +0,0 @@
# Profiler
A UI for the query and aggregation profiler in Elasticsearch
## Development
Assuming you've checked out x-plugins next to kibana...
- Run `yarn kbn bootstrap`
- Run `yarn start` to watch for and sync files on change
- Open a new terminal to run Kibana - use `yarn start` to launch it in dev mode
- Kibana will automatically restart as files are synced
- If you need debugging output, run `DEBUG=reporting yarn start` instead
If you have installed this somewhere other than via x-plugins, and next to the kibana repo, you'll need to change the `pathToKibana` setting in `gulpfile.js`
## Testing
To run the server tests, change into `x-plugins/kibana` and run:
```bash
mocha --debug --compilers js:@babel/register plugins/profiler/**/__tests__/**/*.js
```
--kbnServer.tests_bundle.pluginId

View file

@ -5,12 +5,10 @@
*/
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';
// TODO:
// Until we can process SCSS in new platform, this part of Searchprofiler
// legacy must remain here.
export const searchprofiler = (kibana: any) => {
const publicSrc = resolve(__dirname, 'public');
@ -22,43 +20,8 @@ export const searchprofiler = (kibana: any) => {
publicDir: publicSrc,
uiExports: {
// NP Ready
devTools: [`${publicSrc}/legacy`],
styleSheetPaths: `${publicSrc}/np_ready/application/index.scss`,
// Legacy
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<typeof server.route>[0]) => server.route(args),
plugins: {
__LEGACY: {
thisPlugin,
xpackMain: server.plugins.xpack_main,
elasticsearch: server.plugins.elasticsearch,
commonRouteConfig,
},
},
};
serverPlugin.setup({} as CoreSetup, legacySetup);
styleSheetPaths: `${publicSrc}/index.scss`,
},
init() {},
});
};

View file

@ -1,28 +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.
*/
/* eslint-disable @kbn/eslint/no-restricted-paths */
import { npSetup } from 'ui/new_platform';
import { I18nContext } from 'ui/i18n';
// @ts-ignore
import { xpackInfo } from 'plugins/xpack_main/services/xpack_info';
// @ts-ignore
import { formatAngularHttpError } from 'ui/notify/lib';
import 'ui/autoload/all';
import { plugin } from './np_ready';
const pluginInstance = plugin({} as any);
pluginInstance.setup(npSetup.core, {
...npSetup.plugins,
__LEGACY: {
I18nContext,
licenseEnabled: xpackInfo.get('features.searchprofiler.enableAppLink'),
notifications: npSetup.core.notifications.toasts,
formatAngularHttpError,
},
});

View file

@ -1,62 +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 { i18n } from '@kbn/i18n';
import {
Plugin,
CoreStart,
CoreSetup,
PluginInitializerContext,
ToastsStart,
} from 'src/core/public';
import { DevToolsSetup } from '../../../../../../src/plugins/dev_tools/public';
export class SearchProfilerUIPlugin implements Plugin {
constructor(ctx: PluginInitializerContext) {}
async setup(
core: CoreSetup,
plugins: {
__LEGACY: {
I18nContext: any;
licenseEnabled: boolean;
notifications: ToastsStart;
formatAngularHttpError: any;
};
dev_tools: DevToolsSetup;
}
) {
const { http } = core;
const {
__LEGACY: { I18nContext, licenseEnabled, notifications, formatAngularHttpError },
dev_tools,
} = plugins;
dev_tools.register({
id: 'searchprofiler',
title: i18n.translate('xpack.searchProfiler.pageDisplayName', {
defaultMessage: 'Search Profiler',
}),
order: 5,
enableRouting: false,
async mount(ctx, params) {
const { boot } = await import('./application/boot');
return boot({
http,
licenseEnabled,
el: params.element,
I18nContext,
notifications,
formatAngularHttpError,
});
},
});
}
async start(core: CoreStart, plugins: any) {}
async stop() {}
}

View file

@ -1,28 +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 {
FeatureCatalogueRegistryProvider,
FeatureCatalogueCategory,
} from 'ui/registry/feature_catalogue';
import { i18n } from '@kbn/i18n';
FeatureCatalogueRegistryProvider.register(() => {
return {
id: 'searchprofiler',
title: i18n.translate('xpack.searchProfiler.registryProviderTitle', {
defaultMessage: 'Search Profiler',
}),
description: i18n.translate('xpack.searchProfiler.registryProviderDescription', {
defaultMessage: 'Quickly check the performance of any Elasticsearch query.',
}),
icon: 'searchProfilerApp',
path: '/app/kibana#/dev_tools/searchprofiler',
showOnHomePage: false,
category: FeatureCatalogueCategory.ADMIN,
};
});

View file

@ -1,81 +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 expect from '@kbn/expect';
import { set } from 'lodash';
import { checkLicense } from './check_license';
describe('check_license', () => {
let mockLicenseInfo: any;
beforeEach(() => (mockLicenseInfo = {}));
describe('license information is not available', () => {
beforeEach(() => (mockLicenseInfo.isAvailable = () => false));
it('should set showLinks to true', () => {
expect(checkLicense(mockLicenseInfo).showAppLink).to.be(true);
});
it('should set enableLinks to false', () => {
expect(checkLicense(mockLicenseInfo).enableAppLink).to.be(false);
});
});
describe('license information is available', () => {
beforeEach(() => {
mockLicenseInfo.isAvailable = () => true;
set(mockLicenseInfo, 'license.getType', () => 'basic');
});
describe('& license is > basic', () => {
beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => true));
describe('& license is active', () => {
beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true));
it('should set showLinks to true', () => {
expect(checkLicense(mockLicenseInfo).showAppLink).to.be(true);
});
it('should set enableLinks to true', () => {
expect(checkLicense(mockLicenseInfo).enableAppLink).to.be(true);
});
});
describe('& license is expired', () => {
beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false));
it('should set showLinks to true', () => {
expect(checkLicense(mockLicenseInfo).showAppLink).to.be(true);
});
it('should set enableLinks to false', () => {
expect(checkLicense(mockLicenseInfo).enableAppLink).to.be(false);
});
});
});
describe('& license is basic', () => {
beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => false));
describe('& license is active', () => {
beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true));
it('should set showLinks to false', () => {
expect(checkLicense(mockLicenseInfo).showAppLink).to.be(false);
});
});
describe('& license is expired', () => {
beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false));
it('should set showLinks to false', () => {
expect(checkLicense(mockLicenseInfo).showAppLink).to.be(false);
});
});
});
});
});

View file

@ -1,59 +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 { i18n } from '@kbn/i18n';
import { XPackInfo } from '../../../../xpack_main/server/lib/xpack_info';
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.',
}),
};
}
const isLicenseActive = xpackLicenseInfo.license.isActive();
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',
'enterprise',
])
) {
return {
showAppLink: true,
enableAppLink: isLicenseActive,
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() },
});
return {
showAppLink: false,
enableAppLink: false,
message,
};
}

View file

@ -1,38 +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 { 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 {}
}

View file

@ -1,53 +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';
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 });
},
});
};

View file

@ -1,33 +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 { ServerRoute } from 'hapi';
import { ElasticsearchPlugin, Request } from 'src/legacy/core_plugins/elasticsearch';
import { XPackMainPlugin } from '../../../xpack_main/server/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;
}

View file

@ -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 { LicenseType } from '../../licensing/common/types';
const basicLicense: LicenseType = 'basic';
/** @internal */
export const PLUGIN = Object.freeze({
id: 'searchprofiler',
minimumLicenseType: basicLicense,
});

View file

@ -4,4 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { checkLicense } from './check_license';
export { PLUGIN } from './constants';
export { LicenseStatus } from './types';

View file

@ -3,8 +3,9 @@
* 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();
};
/** @internal */
export interface LicenseStatus {
valid: boolean;
message?: string;
}

View file

@ -0,0 +1,8 @@
{
"id": "searchprofiler",
"version": "8.0.0",
"kibanaVersion": "kibana",
"requiredPlugins": ["dev_tools", "home", "licensing"],
"server": true,
"ui": true
}

View file

@ -0,0 +1,3 @@
## Please note
See x-pack/legacy/plugins/searchprofiler/public for styles.

View file

@ -4,17 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { render, unmountComponentAtNode } from 'react-dom';
import { HttpStart as Http, ToastsSetup } from 'kibana/public';
import React from 'react';
import { HttpStart as Http, ToastsSetup } from 'src/core/public';
import { LicenseStatus } from '../../common';
import { App } from '.';
export interface Dependencies {
el: HTMLElement;
http: Http;
licenseEnabled: boolean;
I18nContext: any;
notifications: ToastsSetup;
formatAngularHttpError: any;
initialLicenseStatus: LicenseStatus;
}
export type AppDependencies = Omit<Dependencies, 'el'>;

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { registerTestBed } from '../../../../../../../../test_utils';
import { registerTestBed } from '../../../../../../test_utils';
import { HighlightDetailsFlyout, Props } from '.';
describe('Highlight Details Flyout', () => {

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { registerTestBed } from '../../../../../../../test_utils';
import { registerTestBed } from '../../../../../test_utils';
import { LicenseWarningNotice } from './license_warning_notice';

View file

@ -36,7 +36,7 @@ export const LicenseWarningNotice = () => {
title={i18n.translate('xpack.searchProfiler.licenseErrorMessageTitle', {
defaultMessage: 'License error',
})}
color="warning"
color="danger"
iconType="alert"
style={{ padding: '16px' }}
>

View file

@ -1,3 +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 const breakdown = {
advance: 0,
advance_count: 0,

View file

@ -1,3 +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 const inputTimes = [
{
type: 'BooleanQuery',

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { registerTestBed } from '../../../../../../../../../test_utils';
import { registerTestBed } from '../../../../../../../test_utils';
import { searchResponse } from './fixtures/search_response';
import { ProfileTree, Props } from '../profile_tree';

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { registerTestBed } from '../../../../../../../test_utils';
import { registerTestBed } from '../../../../../test_utils';
import { SearchProfilerTabs, Props } from './searchprofiler_tabs';

View file

@ -3,7 +3,7 @@
* 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 { registerTestBed } from '../../../../../../../test_utils';
import { EmptyTreePlaceHolder } from '.';
describe('EmptyTreePlaceholder', () => {

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { registerTestBed } from '../../../../../../../../../test_utils';
import { registerTestBed } from '../../../../../../../test_utils';
import { ProfileLoadingPlaceholder } from '.';
describe('Profile Loading Placeholder', () => {

View file

@ -42,7 +42,7 @@ function hasAggregations(profileResponse: ShardSerialized[]) {
}
export const Main = () => {
const { licenseEnabled } = useAppContext();
const { getLicenseStatus } = useAppContext();
const {
activeTab,
@ -63,7 +63,7 @@ export const Main = () => {
]);
const renderLicenseWarning = () => {
return !licenseEnabled ? (
return !getLicenseStatus().valid ? (
<>
<LicenseWarningNotice />
<EuiSpacer size="s" />
@ -84,7 +84,7 @@ export const Main = () => {
);
}
if (licenseEnabled && pristine) {
if (getLicenseStatus().valid && pristine) {
return <EmptyTreePlaceHolder />;
}

View file

@ -3,6 +3,7 @@
* 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 {
@ -39,7 +40,7 @@ export const ProfileQueryEditor = memo(() => {
const dispatch = useProfilerActionContext();
const { licenseEnabled, notifications } = useAppContext();
const { getLicenseStatus, notifications } = useAppContext();
const requestProfile = useRequestProfile();
const handleProfileClick = async () => {
@ -65,6 +66,7 @@ export const ProfileQueryEditor = memo(() => {
};
const onEditorReady = useCallback(editorInstance => (editorRef.current = editorInstance), []);
const licenseEnabled = getLicenseStatus().valid;
return (
<EuiFlexGroup

View file

@ -4,26 +4,45 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useContext, createContext } from 'react';
import React, { useContext, createContext, useCallback } from 'react';
import { HttpSetup, ToastsSetup } from 'kibana/public';
import { LicenseStatus } from '../../../common';
export interface ContextArgs {
http: HttpSetup;
notifications: ToastsSetup;
initialLicenseStatus: LicenseStatus;
}
export interface ContextValue {
http: HttpSetup;
notifications: ToastsSetup;
licenseEnabled: boolean;
formatAngularHttpError: (error: any) => string;
getLicenseStatus: () => LicenseStatus;
}
const AppContext = createContext<ContextValue>(null as any);
export const AppContextProvider = ({
children,
value,
args: { http, notifications, initialLicenseStatus },
}: {
children: React.ReactNode;
value: ContextValue;
args: ContextArgs;
}) => {
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
const getLicenseStatus = useCallback(() => initialLicenseStatus, [initialLicenseStatus]);
return (
<AppContext.Provider
value={{
http,
notifications,
getLicenseStatus,
}}
>
{children}
</AppContext.Provider>
);
};
export const useAppContext = () => {

View file

@ -10,7 +10,7 @@ jest.mock('./worker', () => {
return { workerModule: { id: 'ace/mode/json_worker', src: '' } };
});
import { registerTestBed } from '../../../../../../../test_utils';
import { registerTestBed } from '../../../../../test_utils';
import { Editor, Props } from '.';
describe('Editor Component', () => {

View file

@ -10,10 +10,7 @@ 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;
}
type EditorShim = ReturnType<typeof createEditorShim>;
export type EditorInstance = EditorShim;
@ -23,7 +20,7 @@ export interface Props {
onEditorReady: (editor: EditorShim) => void;
}
const createEditorShim = (aceEditor: AceEditor): EditorShim => {
const createEditorShim = (aceEditor: AceEditor) => {
return {
getValue() {
return aceEditor.getValue();
@ -40,15 +37,13 @@ export const Editor = memo(({ licenseEnabled, initialValue, onEditorReady }: Pro
const [textArea, setTextArea] = useState<HTMLTextAreaElement | null>(null);
if (licenseEnabled) {
useUIAceKeyboardMode(textArea);
}
useUIAceKeyboardMode(textArea);
useEffect(() => {
const divEl = containerRef.current;
editorInstanceRef.current = initializeEditor({ el: divEl, licenseEnabled });
editorInstanceRef.current.setValue(initialValue, 1);
setTextArea(containerRef.current!.querySelector('textarea'));
setTextArea(licenseEnabled ? containerRef.current!.querySelector('textarea') : null);
onEditorReady(createEditorShim(editorInstanceRef.current));
}, [initialValue, onEditorReady, licenseEnabled]);

View file

@ -19,8 +19,22 @@ interface ReturnValue {
error?: string;
}
const extractProfilerErrorMessage = (e: any): string | undefined => {
if (e.body?.attributes?.error?.reason) {
const { reason, line, col } = e.body.attributes.error;
return `${reason} at line: ${line - 1} col: ${col}`;
}
if (e.body?.message) {
return e.body.message;
}
return;
};
export const useRequestProfile = () => {
const { http, notifications, formatAngularHttpError, licenseEnabled } = useAppContext();
const { http, notifications, getLicenseStatus } = useAppContext();
const licenseEnabled = getLicenseStatus().valid;
return async ({ query, index }: Args): Promise<ReturnValue> => {
if (!licenseEnabled) {
return { data: null };
@ -39,7 +53,7 @@ export const useRequestProfile = () => {
return { data: parsed.profile.shards };
}
const payload: Record<string, any> = { query };
const payload: Record<string, any> = { query: parsed };
if (index == null || index === '') {
payload.index = '_all';
@ -59,11 +73,13 @@ export const useRequestProfile = () => {
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 (_) {
const profilerErrorMessage = extractProfilerErrorMessage(e);
if (profilerErrorMessage) {
notifications.addError(e, {
title: e.message,
toastMessage: profilerErrorMessage,
});
} else {
// Otherwise just report the original error
notifications.addError(e, {
title: i18n.translate('xpack.searchProfiler.errorSomethingWentWrongTitle', {

View file

@ -10,16 +10,10 @@ import { ProfileContextProvider } from './contexts/profiler_context';
import { AppDependencies } from './boot';
export function App({
I18nContext,
licenseEnabled,
notifications,
http,
formatAngularHttpError,
}: AppDependencies) {
export function App({ I18nContext, initialLicenseStatus, notifications, http }: AppDependencies) {
return (
<I18nContext>
<AppContextProvider value={{ licenseEnabled, notifications, http, formatAngularHttpError }}>
<AppContextProvider args={{ initialLicenseStatus, notifications, http }}>
<ProfileContextProvider>
<Main />
</ProfileContextProvider>

View file

@ -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.
*/
import { i18n } from '@kbn/i18n';
import { Plugin, CoreStart, CoreSetup, PluginInitializerContext } from 'kibana/public';
import { first } from 'rxjs/operators';
import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public';
import { LICENSE_CHECK_STATE } from '../../licensing/public';
import { PLUGIN } from '../common';
import { AppPublicPluginDependencies } from './types';
export class SearchProfilerUIPlugin implements Plugin<void, void, AppPublicPluginDependencies> {
constructor(ctx: PluginInitializerContext) {}
async setup(
{ http, getStartServices }: CoreSetup,
{ dev_tools, home, licensing }: AppPublicPluginDependencies
) {
home.featureCatalogue.register({
id: PLUGIN.id,
title: i18n.translate('xpack.searchProfiler.registryProviderTitle', {
defaultMessage: 'Search Profiler',
}),
description: i18n.translate('xpack.searchProfiler.registryProviderDescription', {
defaultMessage: 'Quickly check the performance of any Elasticsearch query.',
}),
icon: 'searchProfilerApp',
path: '/app/kibana#/dev_tools/searchprofiler',
showOnHomePage: false,
category: FeatureCatalogueCategory.ADMIN,
});
dev_tools.register({
id: 'searchprofiler',
title: i18n.translate('xpack.searchProfiler.pageDisplayName', {
defaultMessage: 'Search Profiler',
}),
order: 5,
enableRouting: false,
mount: async (ctx, params) => {
const [coreStart] = await getStartServices();
const { notifications, i18n: i18nDep } = coreStart;
const { boot } = await import('./application/boot');
const license = await licensing.license$.pipe(first()).toPromise();
const { state, message } = license.check(PLUGIN.id, PLUGIN.minimumLicenseType);
const initialLicenseStatus =
state === LICENSE_CHECK_STATE.Valid ? { valid: true } : { valid: false, message };
return boot({
http,
initialLicenseStatus,
el: params.element,
I18nContext: i18nDep.Context,
notifications: notifications.toasts,
});
},
});
}
async start(core: CoreStart, plugins: any) {}
async stop() {}
}

View file

@ -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 { HomePublicPluginSetup } from '../../../../src/plugins/home/public';
import { DevToolsSetup } from '../../../../src/plugins/dev_tools/public';
import { LicensingPluginSetup } from '../../licensing/public';
export interface AppPublicPluginDependencies {
licensing: LicensingPluginSetup;
home: HomePublicPluginSetup;
dev_tools: DevToolsSetup;
}

View file

@ -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 { PluginInitializerContext } from 'kibana/server';
import { SearchProfilerServerPlugin } from './plugin';
export const plugin = (ctx: PluginInitializerContext) => {
return new SearchProfilerServerPlugin(ctx);
};

View file

@ -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 { i18n } from '@kbn/i18n';
import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/server';
import { LICENSE_CHECK_STATE } from '../../licensing/common/types';
import { LicenseStatus, PLUGIN } from '../common';
import { AppServerPluginDependencies } from './types';
import * as profileRoute from './routes/profile';
export class SearchProfilerServerPlugin implements Plugin {
licenseStatus: LicenseStatus;
log: Logger;
constructor({ logger }: PluginInitializerContext) {
this.log = logger.get();
this.licenseStatus = { valid: false };
}
async setup({ http }: CoreSetup, { licensing, elasticsearch }: AppServerPluginDependencies) {
const router = http.createRouter();
profileRoute.register({
elasticsearch,
router,
getLicenseStatus: () => this.licenseStatus,
log: this.log,
});
licensing.license$.subscribe(license => {
const { state, message } = license.check(PLUGIN.id, PLUGIN.minimumLicenseType);
const hasRequiredLicense = state === LICENSE_CHECK_STATE.Valid;
if (hasRequiredLicense) {
this.licenseStatus = { valid: true };
} else {
this.licenseStatus = {
valid: false,
message:
message ||
// Ensure that there is a message when license check fails
i18n.translate('xpack.searchProfiler.licenseCheckErrorMessage', {
defaultMessage: 'License check failed',
}),
};
if (message) {
this.log.info(message);
}
}
});
}
start() {}
stop() {}
}

View file

@ -0,0 +1,70 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { RouteDependencies } from '../types';
export const register = ({ router, getLicenseStatus, log }: RouteDependencies) => {
router.post(
{
path: '/api/searchprofiler/profile',
validate: {
body: schema.object({
query: schema.object({}, { allowUnknowns: true }),
index: schema.string(),
}),
},
},
async (ctx, request, response) => {
const currentLicenseStatus = getLicenseStatus();
if (!currentLicenseStatus.valid) {
return response.forbidden({
body: {
message: currentLicenseStatus.message!,
},
});
}
const {
core: { elasticsearch },
} = ctx;
const {
body: { query, index },
} = request;
const parsed = {
// Activate profiler mode for this query.
profile: true,
...query,
};
const body = {
index,
body: JSON.stringify(parsed, null, 2),
};
try {
const resp = await elasticsearch.dataClient.callAsCurrentUser('search', body);
return response.ok({
body: {
ok: true,
resp,
},
});
} catch (err) {
log.error(err);
return response.customError({
statusCode: err.status || 500,
body: err.body
? {
message: err.message,
attributes: err.body,
}
: err,
});
}
}
);
};

Some files were not shown because too many files have changed in this diff Show more