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);