From ceabc8604ebe692422e138dddd73c7771ed7ea1f Mon Sep 17 00:00:00 2001 From: Sonia Sanz Vivas Date: Wed, 4 Dec 2024 16:53:36 +0100 Subject: [PATCH] Check for indices before enabling get search profile in serverless (#201630) Closes [#195342](https://github.com/elastic/kibana/issues/195342) ## Summary In serverless, the default query for the search profiler fails if there is not indices. For avoiding this error, when there are no indices present, this PR disabled the "Profile" button and add a tooltip explaining why it is disabled. ### New strings This is the tooltip for string verification @kibana-docs [[Code](https://github.com/elastic/kibana/pull/201630/commits/5832a76683ad0cf55558655ca5981d623f344b72?diff=unified&w=0#diff-bf48cd9834b39a2a1634680225fc63c9a4ddb3ca881d9120f648006ad0277046R154-R1552?diff=unified&w=0#diff-bf48cd9834b39a2a1634680225fc63c9a4ddb3ca881d9120f648006ad0277046R155)]: Screenshot 2024-11-25 at 16 15 08 ### How to test * Run Kibana in serverless * Go to Index Management and verify you haven't indices (or delete them if you do have indices). * Go to Dev Tools and click the Search Profiler tab. Verify that the button is disabled and the tooltip displayed if you hover over it. * Go back to Index Management and create one or more indices. * Go back to Dev Tools > Search Profiler. Now the button should be enabled and the profile should be created if you click it. ### Demo https://github.com/user-attachments/assets/9bda072e-7897-4418-a906-14807e736c44 ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Elastic Machine --- .../searchprofiler/common/constants.ts | 2 + .../profile_query_editor.tsx | 50 ++++++++++++++----- .../public/application/hooks/index.ts | 1 + .../application/hooks/use_has_indices.ts | 22 ++++++++ .../searchprofiler/server/routes/profile.ts | 45 ++++++++++++++++- .../apis/management/index_management/index.ts | 1 + .../index_management/searchprofiler.ts | 21 ++++++++ .../common/dev_tools/search_profiler.ts | 2 + 8 files changed, 130 insertions(+), 14 deletions(-) create mode 100644 x-pack/plugins/searchprofiler/public/application/hooks/use_has_indices.ts create mode 100644 x-pack/test/api_integration/apis/management/index_management/searchprofiler.ts diff --git a/x-pack/plugins/searchprofiler/common/constants.ts b/x-pack/plugins/searchprofiler/common/constants.ts index a50ed281c2bd0..0d586e9b1fb68 100644 --- a/x-pack/plugins/searchprofiler/common/constants.ts +++ b/x-pack/plugins/searchprofiler/common/constants.ts @@ -9,6 +9,8 @@ import { LicenseType } from '@kbn/licensing-plugin/common/types'; const basicLicense: LicenseType = 'basic'; +export const API_BASE_PATH = '/api/searchprofiler'; + /** @internal */ export const PLUGIN = Object.freeze({ id: 'searchprofiler', diff --git a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/profile_query_editor.tsx b/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/profile_query_editor.tsx index a88f1040caa3a..d80cbc4f0394d 100644 --- a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/profile_query_editor.tsx +++ b/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/profile_query_editor.tsx @@ -16,11 +16,12 @@ import { EuiFlexGroup, EuiSpacer, EuiFlexItem, + EuiToolTip, } from '@elastic/eui'; import { decompressFromEncodedURIComponent } from 'lz-string'; -import { useRequestProfile } from '../../hooks'; +import { useHasIndices, useRequestProfile } from '../../hooks'; import { useAppContext } from '../../contexts/app_context'; import { useProfilerActionContext } from '../../contexts/profiler_context'; import { Editor, type EditorProps } from './editor'; @@ -46,6 +47,8 @@ export const ProfileQueryEditor = memo(() => { const { getLicenseStatus, notifications, location } = useAppContext(); + const { data: indicesData, isLoading, error: indicesDataError } = useHasIndices(); + const queryParams = new URLSearchParams(location.search); const indexName = queryParams.get('index'); const searchProfilerQueryURI = queryParams.get('load_from'); @@ -86,6 +89,32 @@ export const ProfileQueryEditor = memo(() => { ); const licenseEnabled = getLicenseStatus().valid; + const hasIndices = isLoading || indicesDataError ? false : indicesData?.hasIndices; + + const isDisabled = !licenseEnabled || !hasIndices; + const tooltipContent = !licenseEnabled + ? i18n.translate('xpack.searchProfiler.formProfileButton.noLicenseTooltip', { + defaultMessage: 'You need an active license to use Search Profiler', + }) + : i18n.translate('xpack.searchProfiler.formProfileButton.noIndicesTooltip', { + defaultMessage: 'You must have at least one index to use Search Profiler', + }); + + const button = ( + + + {i18n.translate('xpack.searchProfiler.formProfileButtonLabel', { + defaultMessage: 'Profile', + })} + + + ); + return ( {/* Form */} @@ -135,18 +164,13 @@ export const ProfileQueryEditor = memo(() => { - handleProfileClick()} - > - - {i18n.translate('xpack.searchProfiler.formProfileButtonLabel', { - defaultMessage: 'Profile', - })} - - + {isDisabled ? ( + + {button} + + ) : ( + button + )} diff --git a/x-pack/plugins/searchprofiler/public/application/hooks/index.ts b/x-pack/plugins/searchprofiler/public/application/hooks/index.ts index 9c1b3bfb8e9ed..156ad6bc8b163 100644 --- a/x-pack/plugins/searchprofiler/public/application/hooks/index.ts +++ b/x-pack/plugins/searchprofiler/public/application/hooks/index.ts @@ -6,3 +6,4 @@ */ export { useRequestProfile } from './use_request_profile'; +export { useHasIndices } from './use_has_indices'; diff --git a/x-pack/plugins/searchprofiler/public/application/hooks/use_has_indices.ts b/x-pack/plugins/searchprofiler/public/application/hooks/use_has_indices.ts new file mode 100644 index 0000000000000..43938d14a421f --- /dev/null +++ b/x-pack/plugins/searchprofiler/public/application/hooks/use_has_indices.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useRequest } from '@kbn/es-ui-shared-plugin/public'; +import { API_BASE_PATH } from '../../../common/constants'; +import { useAppContext } from '../contexts/app_context'; + +interface ReturnValue { + hasIndices: boolean; +} + +export const useHasIndices = () => { + const { http } = useAppContext(); + return useRequest(http, { + path: `${API_BASE_PATH}/has_indices`, + method: 'get', + }); +}; diff --git a/x-pack/plugins/searchprofiler/server/routes/profile.ts b/x-pack/plugins/searchprofiler/server/routes/profile.ts index 9e76bf0df96a1..7141a51c2c7f5 100644 --- a/x-pack/plugins/searchprofiler/server/routes/profile.ts +++ b/x-pack/plugins/searchprofiler/server/routes/profile.ts @@ -6,12 +6,13 @@ */ import { schema } from '@kbn/config-schema'; +import { API_BASE_PATH } from '../../common/constants'; import { RouteDependencies } from '../types'; export const register = ({ router, getLicenseStatus, log }: RouteDependencies) => { router.post( { - path: '/api/searchprofiler/profile', + path: `${API_BASE_PATH}/profile`, validate: { body: schema.object({ query: schema.object({}, { unknowns: 'allow' }), @@ -56,6 +57,48 @@ export const register = ({ router, getLicenseStatus, log }: RouteDependencies) = log.error(err); const { statusCode, body: errorBody } = err; + return response.customError({ + statusCode: statusCode || 500, + body: errorBody + ? { + message: errorBody.error?.reason, + attributes: errorBody, + } + : err, + }); + } + } + ); + router.get( + { + path: `${API_BASE_PATH}/has_indices`, + validate: false, + }, + async (ctx, _request, response) => { + const currentLicenseStatus = getLicenseStatus(); + if (!currentLicenseStatus.valid) { + return response.forbidden({ + body: { + message: currentLicenseStatus.message!, + }, + }); + } + + try { + const client = (await ctx.core).elasticsearch.client.asCurrentUser; + const resp = await client.cat.indices({ format: 'json' }); + + const hasIndices = resp.length > 0; + + return response.ok({ + body: { + hasIndices, + }, + }); + } catch (err) { + log.error(err); + const { statusCode, body: errorBody } = err; + return response.customError({ statusCode: statusCode || 500, body: errorBody diff --git a/x-pack/test/api_integration/apis/management/index_management/index.ts b/x-pack/test/api_integration/apis/management/index_management/index.ts index 63ab1f3371941..e17da6cae3b6f 100644 --- a/x-pack/test/api_integration/apis/management/index_management/index.ts +++ b/x-pack/test/api_integration/apis/management/index_management/index.ts @@ -22,5 +22,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./enrich_policies')); loadTestFile(require.resolve('./create_enrich_policy')); loadTestFile(require.resolve('./data_enrichers')); + loadTestFile(require.resolve('./searchprofiler')); }); } diff --git a/x-pack/test/api_integration/apis/management/index_management/searchprofiler.ts b/x-pack/test/api_integration/apis/management/index_management/searchprofiler.ts new file mode 100644 index 0000000000000..01c4347945118 --- /dev/null +++ b/x-pack/test/api_integration/apis/management/index_management/searchprofiler.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from 'expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +const API_BASE_PATH = '/api/searchprofiler'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('Searchprofiler', function () { + it('Can retrive has indices', async () => { + const { body } = await supertest.get(`${API_BASE_PATH}/has_indices`).expect(200); + expect(body).toStrictEqual({ hasIndices: true }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/dev_tools/search_profiler.ts b/x-pack/test_serverless/functional/test_suites/common/dev_tools/search_profiler.ts index 979943ffa602c..169f266e0c296 100644 --- a/x-pack/test_serverless/functional/test_suites/common/dev_tools/search_profiler.ts +++ b/x-pack/test_serverless/functional/test_suites/common/dev_tools/search_profiler.ts @@ -18,6 +18,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects(['svlCommonPage', 'common', 'searchProfiler']); const retry = getService('retry'); const es = getService('es'); + const browser = getService('browser'); describe('Search Profiler Editor', () => { before(async () => { @@ -81,6 +82,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('profiles a simple query', async () => { + await browser.refresh(); await PageObjects.searchProfiler.setIndexName(indexName); await PageObjects.searchProfiler.setQuery(testQuery);