diff --git a/.buildkite/ftr_platform_stateful_configs.yml b/.buildkite/ftr_platform_stateful_configs.yml index 02d6355c212b..301670605a0d 100644 --- a/.buildkite/ftr_platform_stateful_configs.yml +++ b/.buildkite/ftr_platform_stateful_configs.yml @@ -351,6 +351,7 @@ enabled: - x-pack/performance/journeys_e2e/tsdb_logs_data_visualizer.ts - x-pack/performance/journeys_e2e/promotion_tracking_dashboard.ts - x-pack/performance/journeys_e2e/web_logs_dashboard.ts + - x-pack/performance/journeys_e2e/web_logs_dashboard_esql.ts - x-pack/performance/journeys_e2e/data_stress_test_lens.ts - x-pack/performance/journeys_e2e/ecommerce_dashboard_saved_search_only.ts - x-pack/performance/journeys_e2e/ecommerce_dashboard_tsvb_gauge_only.ts diff --git a/.buildkite/scripts/steps/esql_generate_function_metadata.sh b/.buildkite/scripts/steps/esql_generate_function_metadata.sh index 837a962b3c42..07de4bc9bd04 100755 --- a/.buildkite/scripts/steps/esql_generate_function_metadata.sh +++ b/.buildkite/scripts/steps/esql_generate_function_metadata.sh @@ -2,7 +2,7 @@ set -euo pipefail VALIDATION_PACKAGE_DIR="packages/kbn-esql-validation-autocomplete" -EDITOR_PACKAGE_DIR="packages/kbn-text-based-editor" +EDITOR_PACKAGE_DIR="packages/kbn-language-documentation-popover" GIT_SCOPE="$VALIDATION_PACKAGE_DIR/**/* $EDITOR_PACKAGE_DIR/**/*" report_main_step () { diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index a523935afdba..5fd690a2f6ce 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -1493,6 +1493,15 @@ "responses": { "204": { "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema or parameters." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "404": { + "description": "Indicates a rule with the given ID does not exist." } }, "summary": "Delete a rule", @@ -2399,6 +2408,15 @@ } }, "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema or parameters." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "404": { + "description": "Indicates a rule with the given ID does not exist." } }, "summary": "Get rule details", @@ -3586,6 +3604,15 @@ } }, "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema or parameters." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "409": { + "description": "Indicates that the rule id is already in use." } }, "summary": "Create a rule", @@ -4756,6 +4783,18 @@ } }, "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema or parameters." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "404": { + "description": "Indicates a rule with the given ID does not exist." + }, + "409": { + "description": "Indicates that the rule has already been updated by another user." } }, "summary": "Update a rule", @@ -4821,6 +4860,15 @@ "responses": { "204": { "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "404": { + "description": "Indicates a rule with the given ID does not exist." } }, "summary": "Disable a rule", @@ -4868,6 +4916,15 @@ "responses": { "204": { "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema or parameters." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "404": { + "description": "Indicates a rule with the given ID does not exist." } }, "summary": "Enable a rule", @@ -5009,6 +5066,18 @@ "responses": { "204": { "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema or parameters." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "404": { + "description": "Indicates a rule with the given ID does not exist." + }, + "409": { + "description": "Indicates that the rule has already been updated by another user." } }, "summary": "Update the API key for a rule", @@ -5065,6 +5134,15 @@ "responses": { "204": { "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema or parameters." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "404": { + "description": "Indicates a rule or alert with the given ID does not exist." } }, "summary": "Mute an alert", @@ -5121,6 +5199,15 @@ "responses": { "204": { "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema or parameters." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "404": { + "description": "Indicates a rule or alert with the given ID does not exist." } }, "summary": "Unmute an alert", @@ -6162,6 +6249,12 @@ } }, "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema or parameters." + }, + "403": { + "description": "Indicates that this call is forbidden." } }, "summary": "Get information about rules", diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index a523935afdba..5fd690a2f6ce 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -1493,6 +1493,15 @@ "responses": { "204": { "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema or parameters." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "404": { + "description": "Indicates a rule with the given ID does not exist." } }, "summary": "Delete a rule", @@ -2399,6 +2408,15 @@ } }, "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema or parameters." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "404": { + "description": "Indicates a rule with the given ID does not exist." } }, "summary": "Get rule details", @@ -3586,6 +3604,15 @@ } }, "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema or parameters." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "409": { + "description": "Indicates that the rule id is already in use." } }, "summary": "Create a rule", @@ -4756,6 +4783,18 @@ } }, "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema or parameters." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "404": { + "description": "Indicates a rule with the given ID does not exist." + }, + "409": { + "description": "Indicates that the rule has already been updated by another user." } }, "summary": "Update a rule", @@ -4821,6 +4860,15 @@ "responses": { "204": { "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "404": { + "description": "Indicates a rule with the given ID does not exist." } }, "summary": "Disable a rule", @@ -4868,6 +4916,15 @@ "responses": { "204": { "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema or parameters." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "404": { + "description": "Indicates a rule with the given ID does not exist." } }, "summary": "Enable a rule", @@ -5009,6 +5066,18 @@ "responses": { "204": { "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema or parameters." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "404": { + "description": "Indicates a rule with the given ID does not exist." + }, + "409": { + "description": "Indicates that the rule has already been updated by another user." } }, "summary": "Update the API key for a rule", @@ -5065,6 +5134,15 @@ "responses": { "204": { "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema or parameters." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "404": { + "description": "Indicates a rule or alert with the given ID does not exist." } }, "summary": "Mute an alert", @@ -5121,6 +5199,15 @@ "responses": { "204": { "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema or parameters." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "404": { + "description": "Indicates a rule or alert with the given ID does not exist." } }, "summary": "Unmute an alert", @@ -6162,6 +6249,12 @@ } }, "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema or parameters." + }, + "403": { + "description": "Indicates that this call is forbidden." } }, "summary": "Get information about rules", diff --git a/packages/cloud/connection_details/kibana/kibana_connection_details_provider.tsx b/packages/cloud/connection_details/kibana/kibana_connection_details_provider.tsx index 444ff70ff912..d400f67993f8 100644 --- a/packages/cloud/connection_details/kibana/kibana_connection_details_provider.tsx +++ b/packages/cloud/connection_details/kibana/kibana_connection_details_provider.tsx @@ -21,6 +21,7 @@ const createOpts = async (props: KibanaConnectionDetailsProviderProps) => { const { http, docLinks, analytics } = start.core; const locator = start.plugins?.share?.url?.locators.get('MANAGEMENT_APP_LOCATOR'); const manageKeysLink = await locator?.getUrl({ sectionId: 'security', appId: 'api_keys' }); + const elasticsearchConfig = await start.plugins?.cloud?.fetchElasticsearchConfig(); const result: ConnectionDetailsOpts = { ...options, navigateToUrl: start.core.application @@ -35,7 +36,7 @@ const createOpts = async (props: KibanaConnectionDetailsProviderProps) => { }, endpoints: { id: start.plugins?.cloud?.cloudId, - url: start.plugins?.cloud?.elasticsearchUrl, + url: elasticsearchConfig?.elasticsearchUrl, cloudIdLearMoreLink: docLinks?.links?.cloud?.beatsAndLogstashConfiguration, ...options?.endpoints, }, diff --git a/packages/cloud/deployment_details/services.tsx b/packages/cloud/deployment_details/services.tsx index d3ca0a340b60..73959627e98e 100644 --- a/packages/cloud/deployment_details/services.tsx +++ b/packages/cloud/deployment_details/services.tsx @@ -6,8 +6,7 @@ * your election, the "Elastic License 2.0", the "GNU Affero General Public * License v3.0 only", or the "Server Side Public License, v 1". */ - -import React, { FC, PropsWithChildren, useContext } from 'react'; +import React, { FC, PropsWithChildren, useContext, useEffect } from 'react'; export interface DeploymentDetailsContextValue { cloudId?: string; @@ -58,7 +57,7 @@ export interface DeploymentDetailsKibanaDependencies { cloud: { isCloudEnabled: boolean; cloudId?: string; - elasticsearchUrl?: string; + fetchElasticsearchConfig: () => Promise<{ elasticsearchUrl?: string }>; }; /** DocLinksStart contract */ docLinks: { @@ -79,11 +78,19 @@ export interface DeploymentDetailsKibanaDependencies { export const DeploymentDetailsKibanaProvider: FC< PropsWithChildren > = ({ children, ...services }) => { + const [elasticsearchUrl, setElasticsearchUrl] = React.useState(''); + + useEffect(() => { + services.cloud.fetchElasticsearchConfig().then((config) => { + setElasticsearchUrl(config.elasticsearchUrl || ''); + }); + }, [services.cloud]); + const { core: { application: { navigateToUrl }, }, - cloud: { isCloudEnabled, cloudId, elasticsearchUrl }, + cloud: { isCloudEnabled, cloudId }, share: { url: { locators }, }, diff --git a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.test.ts b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.test.ts index 34a8bc07e0b5..b7d7b40c4980 100644 --- a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.test.ts +++ b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.test.ts @@ -47,6 +47,7 @@ test('set correct defaults', () => { "maxSockets": 800, "password": undefined, "pingTimeout": "PT30S", + "publicBaseUrl": undefined, "requestHeadersWhitelist": Array [ "authorization", "es-client-authentication", diff --git a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.ts b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.ts index 46b7a02768e7..93fb64baf46d 100644 --- a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.ts +++ b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.ts @@ -359,6 +359,13 @@ export class ElasticsearchConfig implements IElasticsearchConfig { */ public readonly hosts: string[]; + /** + * Optional host that users can use to connect to your Elasticsearch cluster, + * this URL will be shown in Kibana as the Elasticsearch URL + */ + + public readonly publicBaseUrl?: string; + /** * List of Kibana client-side headers to send to Elasticsearch when request * scoped cluster client is used. If this is an empty array then *no* client-side @@ -473,6 +480,7 @@ export class ElasticsearchConfig implements IElasticsearchConfig { this.skipStartupConnectionCheck = rawConfig.skipStartupConnectionCheck; this.apisToRedactInLogs = rawConfig.apisToRedactInLogs; this.dnsCacheTtl = rawConfig.dnsCacheTtl; + this.publicBaseUrl = rawConfig.publicBaseUrl; const { alwaysPresentCertificate, verificationMode } = rawConfig.ssl; const { key, keyPassphrase, certificate, certificateAuthorities } = readKeyAndCerts(rawConfig); diff --git a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_service.ts b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_service.ts index 5a3f34d0565f..83eb04832121 100644 --- a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_service.ts +++ b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_service.ts @@ -135,6 +135,7 @@ export class ElasticsearchService agentStatsProvider: { getAgentsStats: agentManager.getAgentsStats.bind(agentManager), }, + publicBaseUrl: config.publicBaseUrl, }; } @@ -194,6 +195,7 @@ export class ElasticsearchService metrics: { elasticsearchWaitTime, }, + publicBaseUrl: config.publicBaseUrl, }; } diff --git a/packages/core/elasticsearch/core-elasticsearch-server/src/contracts.ts b/packages/core/elasticsearch/core-elasticsearch-server/src/contracts.ts index f0a3a62d08f1..bc712a61a535 100644 --- a/packages/core/elasticsearch/core-elasticsearch-server/src/contracts.ts +++ b/packages/core/elasticsearch/core-elasticsearch-server/src/contracts.ts @@ -138,6 +138,12 @@ export interface ElasticsearchServiceStart { * Returns the capabilities for the default cluster. */ getCapabilities: () => ElasticsearchCapabilities; + + /** + * The public base URL (if any) that should be used by end users to access the Elasticsearch cluster. + */ + + readonly publicBaseUrl?: string; } /** diff --git a/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts b/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts index 539b62997498..2fcdf384cb89 100644 --- a/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts +++ b/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts @@ -212,6 +212,7 @@ export function createPluginSetupContext({ docLinks: deps.docLinks, elasticsearch: { legacy: deps.elasticsearch.legacy, + publicBaseUrl: deps.elasticsearch.publicBaseUrl, setUnauthorizedErrorHandler: deps.elasticsearch.setUnauthorizedErrorHandler, }, executionContext: { diff --git a/packages/kbn-esql-utils/src/utils/append_to_query.ts b/packages/kbn-esql-utils/src/utils/append_to_query.ts index 76f317d55aa5..f4161be073a8 100644 --- a/packages/kbn-esql-utils/src/utils/append_to_query.ts +++ b/packages/kbn-esql-utils/src/utils/append_to_query.ts @@ -36,7 +36,7 @@ export function appendWhereClauseToESQLQuery( default: operator = '=='; } - let filterValue = typeof value === 'string' ? `"${value.replace(/"/g, '\\"')}"` : value; + let filterValue = typeof value === 'string' ? `"${value.replace(/\"/g, '\\"')}"` : value; // Adding the backticks here are they are needed for special char fields let fieldName = `\`${field}\``; diff --git a/packages/kbn-language-documentation-popover/index.ts b/packages/kbn-language-documentation-popover/index.ts index a417382773a5..c12962fdf22b 100644 --- a/packages/kbn-language-documentation-popover/index.ts +++ b/packages/kbn-language-documentation-popover/index.ts @@ -6,7 +6,8 @@ * your election, the "Elastic License 2.0", the "GNU Affero General Public * License v3.0 only", or the "Server Side Public License, v 1". */ - -export { LanguageDocumentationPopover } from './src/components/documentation_popover'; -export { LanguageDocumentationPopoverContent } from './src/components/documentation_content'; -export type { LanguageDocumentationSections } from './src/components/documentation_content'; +export { LanguageDocumentationPopover } from './src/components/as_popover'; +export { LanguageDocumentationPopoverContent } from './src/components/as_popover/popover_content'; +export { LanguageDocumentationFlyout } from './src/components/as_flyout'; +export { LanguageDocumentationInline } from './src/components/as_inline'; +export type { LanguageDocumentationSections } from './src/types'; diff --git a/packages/kbn-language-documentation-popover/package.json b/packages/kbn-language-documentation-popover/package.json index a756b25061b6..002c3c4ee51b 100644 --- a/packages/kbn-language-documentation-popover/package.json +++ b/packages/kbn-language-documentation-popover/package.json @@ -5,5 +5,10 @@ "private": true, "sideEffects": [ "*.scss" - ] -} \ No newline at end of file + ], + "scripts": { + "make:docs": "ts-node --transpileOnly scripts/generate_esql_docs.ts", + "postmake:docs": "yarn run lint:fix", + "lint:fix": "cd ../.. && node ./scripts/eslint --fix ./packages/kbn-language-documentation-popover/src/sections/generated" + } +} diff --git a/packages/kbn-text-based-editor/scripts/generate_esql_docs.ts b/packages/kbn-language-documentation-popover/scripts/generate_esql_docs.ts similarity index 92% rename from packages/kbn-text-based-editor/scripts/generate_esql_docs.ts rename to packages/kbn-language-documentation-popover/scripts/generate_esql_docs.ts index 8a38908e2b21..4fad23e2e25f 100644 --- a/packages/kbn-text-based-editor/scripts/generate_esql_docs.ts +++ b/packages/kbn-language-documentation-popover/scripts/generate_esql_docs.ts @@ -11,18 +11,18 @@ import * as recast from 'recast'; const n = recast.types.namedTypes; import fs from 'fs'; import path from 'path'; -import { functions } from '../src/inline_documentation/generated/scalar_functions'; +import { functions } from '../src/sections/generated/scalar_functions'; (function () { const pathToElasticsearch = process.argv[2]; const { scalarFunctions, aggregationFunctions } = loadFunctionDocs(pathToElasticsearch); writeFunctionDocs( scalarFunctions, - path.join(__dirname, '../src/inline_documentation/generated/scalar_functions.tsx') + path.join(__dirname, '../src/sections/generated/scalar_functions.tsx') ); writeFunctionDocs( aggregationFunctions, - path.join(__dirname, '../src/inline_documentation/generated/aggregation_functions.tsx') + path.join(__dirname, '../src/sections/generated/aggregation_functions.tsx') ); })(); @@ -86,7 +86,7 @@ function writeFunctionDocs(functionDocs: Map, pathToDocsFile: st // Do not edit manually... automatically generated by scripts/generate_esql_docs.ts { label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.${name}', + 'languageDocumentationPopover.documentationESQL.${name}', { defaultMessage: '${name.toUpperCase()}', } @@ -97,7 +97,7 @@ function writeFunctionDocs(functionDocs: Map, pathToDocsFile: st readOnly enableSoftLineBreaks markdownContent={i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.${name}.markdown', + 'languageDocumentationPopover.documentationESQL.${name}.markdown', { defaultMessage: \`${docWithoutLinks.replaceAll('`', '\\`')}\`, description: diff --git a/src/plugins/controls/public/services/index.ts b/packages/kbn-language-documentation-popover/setup_tests.ts similarity index 82% rename from src/plugins/controls/public/services/index.ts rename to packages/kbn-language-documentation-popover/setup_tests.ts index a7cc0715e08d..5ebc6d3dac1c 100644 --- a/src/plugins/controls/public/services/index.ts +++ b/packages/kbn-language-documentation-popover/setup_tests.ts @@ -7,4 +7,5 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { pluginServices } from './plugin_services'; +// eslint-disable-next-line import/no-extraneous-dependencies +import '@testing-library/jest-dom'; diff --git a/packages/kbn-language-documentation-popover/src/__stories__/language_documentation_popover.stories.tsx b/packages/kbn-language-documentation-popover/src/__stories__/language_documentation_popover.stories.tsx index 658f1fe81129..06ace5c91620 100644 --- a/packages/kbn-language-documentation-popover/src/__stories__/language_documentation_popover.stories.tsx +++ b/packages/kbn-language-documentation-popover/src/__stories__/language_documentation_popover.stories.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; -import { LanguageDocumentationPopover } from '../components/documentation_popover'; +import { LanguageDocumentationPopover } from '../components/as_popover'; const sections = { groups: [ diff --git a/packages/kbn-language-documentation-popover/src/components/as_flyout/index.test.tsx b/packages/kbn-language-documentation-popover/src/components/as_flyout/index.test.tsx new file mode 100644 index 000000000000..5dd66386c418 --- /dev/null +++ b/packages/kbn-language-documentation-popover/src/components/as_flyout/index.test.tsx @@ -0,0 +1,95 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { screen, render, fireEvent, waitFor } from '@testing-library/react'; +import { LanguageDocumentationFlyout } from '.'; + +jest.mock('../../sections', () => { + const module = jest.requireActual('../../sections'); + return { + ...module, + getESQLDocsSections: () => ({ + groups: [ + { + label: 'Section one', + description: 'Section 1 description', + items: [], + }, + { + label: 'Section two', + items: [ + { + label: 'Section two item 1', + description: 'Section two item 1 description', + }, + { + label: 'Section two item 2', + description: 'Section two item 2 description', + }, + ], + }, + { + label: 'Section three', + items: [ + { + label: 'Section three item 1', + description: 'Section three item 1 description', + }, + { + label: 'Section three item 2', + description: 'Section three item 2 description', + }, + ], + }, + ], + initialSection: Here is the initial section, + }), + }; +}); + +describe('###Documentation flyout component', () => { + const renderFlyout = (linkToDocumentation?: string) => { + return render( + + ); + }; + it('has a header element for navigation through the sections', () => { + renderFlyout(); + expect(screen.getByTestId('language-documentation-navigation-search')).toBeInTheDocument(); + expect(screen.getByTestId('language-documentation-navigation-dropdown')).toBeInTheDocument(); + expect(screen.queryByTestId('language-documentation-navigation-link')).not.toBeInTheDocument(); + }); + + it('has a link if linkToDocumentation prop is given', () => { + renderFlyout('meow'); + expect(screen.getByTestId('language-documentation-navigation-link')).toBeInTheDocument(); + }); + + it('contains the two last sections', async () => { + renderFlyout(); + await waitFor(() => { + expect(screen.getByText('Section two')).toBeInTheDocument(); + expect(screen.getByText('Section three')).toBeInTheDocument(); + }); + }); + + it('contains the correct section if user updates the search input', async () => { + renderFlyout(); + const input = screen.getByTestId('language-documentation-navigation-search'); + fireEvent.change(input, { target: { value: 'two' } }); + await waitFor(() => { + expect(screen.getByText('Section two')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/kbn-language-documentation-popover/src/components/as_flyout/index.tsx b/packages/kbn-language-documentation-popover/src/components/as_flyout/index.tsx new file mode 100644 index 000000000000..0a617165f766 --- /dev/null +++ b/packages/kbn-language-documentation-popover/src/components/as_flyout/index.tsx @@ -0,0 +1,117 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import React, { useCallback, useEffect, useState, useRef, useMemo } from 'react'; +import { + EuiFlyout, + useEuiTheme, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { getFilteredGroups } from '../../utils/get_filtered_groups'; +import { DocumentationMainContent, DocumentationNavigation } from '../shared'; +import { getESQLDocsSections } from '../../sections'; +import type { LanguageDocumentationSections } from '../../types'; + +interface DocumentationFlyoutProps { + isHelpMenuOpen: boolean; + onHelpMenuVisibilityChange: (status: boolean) => void; + searchInDescription?: boolean; + linkToDocumentation?: string; +} + +function DocumentationFlyout({ + searchInDescription, + linkToDocumentation, + isHelpMenuOpen, + onHelpMenuVisibilityChange, +}: DocumentationFlyoutProps) { + const [documentationSections, setDocumentationSections] = + useState(); + + const { euiTheme } = useEuiTheme(); + const DEFAULT_WIDTH = euiTheme.base * 34; + + const [selectedSection, setSelectedSection] = useState(); + const [searchText, setSearchText] = useState(''); + + const scrollTargets = useRef>({}); + + const onNavigationChange = useCallback((selectedOptions) => { + setSelectedSection(selectedOptions.length ? selectedOptions[0].label : undefined); + if (selectedOptions.length) { + const scrollToElement = scrollTargets.current[selectedOptions[0].label]; + scrollToElement.scrollIntoView(); + } + }, []); + + useEffect(() => { + onHelpMenuVisibilityChange(isHelpMenuOpen ?? false); + }, [isHelpMenuOpen, onHelpMenuVisibilityChange]); + + useEffect(() => { + async function getDocumentation() { + const sections = await getESQLDocsSections(); + setDocumentationSections(sections); + } + if (!documentationSections) { + getDocumentation(); + } + }, [documentationSections]); + + const filteredGroups = useMemo(() => { + return getFilteredGroups(searchText, searchInDescription, documentationSections, 1); + }, [documentationSections, searchText, searchInDescription]); + + return ( + <> + {isHelpMenuOpen && ( + onHelpMenuVisibilityChange(false)} + aria-labelledby="esqlInlineDocumentationFlyout" + type="push" + size={DEFAULT_WIDTH} + paddingSize="m" + > + + +

+ {i18n.translate('languageDocumentationPopover.documentationFlyoutTitle', { + defaultMessage: 'ES|QL quick reference', + })} +

+
+ + +
+ + + +
+ )} + + ); +} + +export const LanguageDocumentationFlyout = React.memo(DocumentationFlyout); diff --git a/packages/kbn-language-documentation-popover/src/components/as_inline/index.test.tsx b/packages/kbn-language-documentation-popover/src/components/as_inline/index.test.tsx new file mode 100644 index 000000000000..4ba873614f9b --- /dev/null +++ b/packages/kbn-language-documentation-popover/src/components/as_inline/index.test.tsx @@ -0,0 +1,97 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { screen, render, fireEvent, waitFor } from '@testing-library/react'; +import { Markdown } from '@kbn/shared-ux-markdown'; +import { LanguageDocumentationInline } from '.'; + +const mockMarkDownDescription = () => ( + +); + +jest.mock('../../sections', () => { + const module = jest.requireActual('../../sections'); + return { + ...module, + getESQLDocsSections: () => ({ + groups: [ + { + label: 'Section one', + description: 'Section 1 description', + items: [], + }, + { + label: 'Section two', + items: [ + { + label: 'Section two item 1', + description: 'Section two item 1 description', + }, + { + label: 'Section two item 2', + description: 'Section two item 2 description', + }, + ], + }, + { + label: 'Section three', + items: [ + { + label: 'Section three item 1', + description: mockMarkDownDescription(), + }, + { + label: 'Section three item 2', + description: 'Section three item 2 description', + }, + ], + }, + ], + initialSection: Here is the initial section, + }), + }; +}); + +describe('###Documentation flyout component', () => { + const renderInlineComponent = (searchInDescription = false) => { + return render(); + }; + it('has a header element for navigation through the sections', () => { + renderInlineComponent(); + expect(screen.getByTestId('language-documentation-navigation-search')).toBeInTheDocument(); + expect(screen.getByTestId('language-documentation-navigation-dropdown')).toBeInTheDocument(); + }); + + it('contains the two last sections', async () => { + renderInlineComponent(); + await waitFor(() => { + expect(screen.getByText('Section two')).toBeInTheDocument(); + expect(screen.getByText('Section three')).toBeInTheDocument(); + }); + }); + + it('contains the correct section if user updates the search input', async () => { + renderInlineComponent(); + const input = screen.getByTestId('language-documentation-navigation-search'); + fireEvent.change(input, { target: { value: 'two' } }); + await waitFor(() => { + expect(screen.getByText('Section two')).toBeInTheDocument(); + }); + }); + + it('contains the correct section if user updates the search input with a text that exist in the description', async () => { + renderInlineComponent(true); + const input = screen.getByTestId('language-documentation-navigation-search'); + fireEvent.change(input, { target: { value: 'blah' } }); + await waitFor(() => { + expect(screen.getByText('Section three')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/kbn-language-documentation-popover/src/components/as_inline/index.tsx b/packages/kbn-language-documentation-popover/src/components/as_inline/index.tsx new file mode 100644 index 000000000000..dcc860aa70db --- /dev/null +++ b/packages/kbn-language-documentation-popover/src/components/as_inline/index.tsx @@ -0,0 +1,82 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import React, { useCallback, useState, useRef, useMemo, useEffect } from 'react'; +import { css } from '@emotion/react'; +import { useEuiTheme, euiScrollBarStyles, EuiSpacer } from '@elastic/eui'; +import { getFilteredGroups } from '../../utils/get_filtered_groups'; +import { DocumentationMainContent, DocumentationNavigation } from '../shared'; +import type { LanguageDocumentationSections } from '../../types'; +import { getESQLDocsSections } from '../../sections'; + +interface DocumentationInlineProps { + searchInDescription?: boolean; +} + +const MAX_HEIGHT = 250; + +function DocumentationInline({ searchInDescription }: DocumentationInlineProps) { + const theme = useEuiTheme(); + const [documentationSections, setDocumentationSections] = + useState(); + const scrollBarStyles = euiScrollBarStyles(theme); + const [selectedSection, setSelectedSection] = useState(); + const [searchText, setSearchText] = useState(''); + + const scrollTargets = useRef>({}); + + useEffect(() => { + async function getDocumentation() { + const sections = await getESQLDocsSections(); + setDocumentationSections(sections); + } + if (!documentationSections) { + getDocumentation(); + } + }, [documentationSections]); + + const filteredGroups = useMemo(() => { + return getFilteredGroups(searchText, searchInDescription, documentationSections, 1); + }, [documentationSections, searchText, searchInDescription]); + + const onNavigationChange = useCallback((selectedOptions) => { + setSelectedSection(selectedOptions.length ? selectedOptions[0].label : undefined); + if (selectedOptions.length) { + const scrollToElement = scrollTargets.current[selectedOptions[0].label]; + scrollToElement.scrollIntoView(); + } + }, []); + + return ( +
+ + + +
+ ); +} + +export const LanguageDocumentationInline = React.memo(DocumentationInline); diff --git a/packages/kbn-language-documentation-popover/src/components/documentation.scss b/packages/kbn-language-documentation-popover/src/components/as_popover/documentation.scss similarity index 100% rename from packages/kbn-language-documentation-popover/src/components/documentation.scss rename to packages/kbn-language-documentation-popover/src/components/as_popover/documentation.scss diff --git a/packages/kbn-language-documentation-popover/src/components/documentation_popover.tsx b/packages/kbn-language-documentation-popover/src/components/as_popover/index.tsx similarity index 95% rename from packages/kbn-language-documentation-popover/src/components/documentation_popover.tsx rename to packages/kbn-language-documentation-popover/src/components/as_popover/index.tsx index 265e3304f7d9..9a1432e938f0 100644 --- a/packages/kbn-language-documentation-popover/src/components/documentation_popover.tsx +++ b/packages/kbn-language-documentation-popover/src/components/as_popover/index.tsx @@ -16,10 +16,8 @@ import { EuiButtonIconProps, EuiOutsideClickDetector, } from '@elastic/eui'; -import { - type LanguageDocumentationSections, - LanguageDocumentationPopoverContent, -} from './documentation_content'; +import { LanguageDocumentationPopoverContent } from './popover_content'; +import type { LanguageDocumentationSections } from '../../types'; interface DocumentationPopoverProps { language: string; diff --git a/packages/kbn-language-documentation-popover/src/components/documentation_content.test.tsx b/packages/kbn-language-documentation-popover/src/components/as_popover/popover_content.test.tsx similarity index 97% rename from packages/kbn-language-documentation-popover/src/components/documentation_content.test.tsx rename to packages/kbn-language-documentation-popover/src/components/as_popover/popover_content.test.tsx index d5acdad75dae..a7a4bce8c2cd 100644 --- a/packages/kbn-language-documentation-popover/src/components/documentation_content.test.tsx +++ b/packages/kbn-language-documentation-popover/src/components/as_popover/popover_content.test.tsx @@ -11,7 +11,7 @@ import React from 'react'; import { mountWithIntl, findTestSubject } from '@kbn/test-jest-helpers'; import { act } from 'react-dom/test-utils'; import { Markdown } from '@kbn/shared-ux-markdown'; -import { LanguageDocumentationPopoverContent } from './documentation_content'; +import { LanguageDocumentationPopoverContent } from './popover_content'; describe('###Documentation popover content', () => { const sections = { diff --git a/packages/kbn-language-documentation-popover/src/components/documentation_content.tsx b/packages/kbn-language-documentation-popover/src/components/as_popover/popover_content.tsx similarity index 84% rename from packages/kbn-language-documentation-popover/src/components/documentation_content.tsx rename to packages/kbn-language-documentation-popover/src/components/as_popover/popover_content.tsx index d3fedfee9f1e..ec622eccf604 100644 --- a/packages/kbn-language-documentation-popover/src/components/documentation_content.tsx +++ b/packages/kbn-language-documentation-popover/src/components/as_popover/popover_content.tsx @@ -6,8 +6,7 @@ * your election, the "Elastic License 2.0", the "GNU Affero General Public * License v3.0 only", or the "Server Side Public License, v 1". */ - -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, @@ -22,19 +21,11 @@ import { EuiSpacer, EuiLink, } from '@elastic/eui'; -import { elementToString } from '../utils/element_to_string'; +import { getFilteredGroups } from '../../utils/get_filtered_groups'; +import type { LanguageDocumentationSections } from '../../types'; import './documentation.scss'; -export interface LanguageDocumentationSections { - groups: Array<{ - label: string; - description?: string; - items: Array<{ label: string; description?: JSX.Element }>; - }>; - initialSection: JSX.Element; -} - interface DocumentationProps { language: string; sections?: LanguageDocumentationSections; @@ -61,29 +52,9 @@ function DocumentationContent({ const [searchText, setSearchText] = useState(''); - const normalizedSearchText = searchText.trim().toLocaleLowerCase(); - - const filteredGroups = sections?.groups - .map((group) => { - const items = group.items.filter((helpItem) => { - return ( - !normalizedSearchText || - helpItem.label.toLocaleLowerCase().includes(normalizedSearchText) || - // Converting the JSX element to a string first - (searchInDescription && - elementToString(helpItem.description) - ?.toLocaleLowerCase() - .includes(normalizedSearchText)) - ); - }); - return { ...group, items }; - }) - .filter((group) => { - if (group.items.length > 0 || !normalizedSearchText) { - return true; - } - return group.label.toLocaleLowerCase().includes(normalizedSearchText); - }); + const filteredGroups = useMemo(() => { + return getFilteredGroups(searchText, searchInDescription, sections); + }, [sections, searchText, searchInDescription]); return ( <> @@ -158,12 +129,12 @@ function DocumentationContent({ - {helpGroup.items.length ? ( + {helpGroup.options.length ? ( <> - {helpGroup.items.map((helpItem) => { + {helpGroup.options.map((helpItem) => { return ( ; + filteredGroups?: Array<{ + label: string; + description?: string; + options: Array<{ label: string; description?: JSX.Element | undefined }>; + }>; + sections?: LanguageDocumentationSections; +} + +function DocumentationContent({ + searchText, + scrollTargets, + filteredGroups, + sections, +}: DocumentationContentProps) { + return ( + <> + + + {!searchText && ( +
{ + if (el && sections?.groups?.length) { + scrollTargets.current[sections.groups[0].label] = el; + } + }} + > + {sections?.initialSection} +
+ )} + {filteredGroups?.map((helpGroup, index) => { + return ( +
{ + if (el) { + scrollTargets.current[helpGroup.label] = el; + } + }} + > +

{helpGroup.label}

+ +

{helpGroup.description}

+ + {filteredGroups?.[index].options.map((helpItem) => { + return ( +
{ + if (el) { + scrollTargets.current[helpItem.label] = el; + } + }} + > + {helpItem.description} +
+ ); + })} +
+ ); + })} +
+
+ + ); +} + +export const DocumentationMainContent = React.memo(DocumentationContent); diff --git a/packages/kbn-language-documentation-popover/src/components/shared/documentation_navigation.tsx b/packages/kbn-language-documentation-popover/src/components/shared/documentation_navigation.tsx new file mode 100644 index 000000000000..c8202d0ea448 --- /dev/null +++ b/packages/kbn-language-documentation-popover/src/components/shared/documentation_navigation.tsx @@ -0,0 +1,109 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import React from 'react'; +import { css } from '@emotion/react'; +import { + EuiFlexItem, + EuiFlexGroup, + EuiFormRow, + EuiLink, + EuiText, + useEuiTheme, + EuiFieldSearch, + EuiComboBox, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface DocumentationNavProps { + searchText: string; + setSearchText: (text: string) => void; + onNavigationChange: (selectedOptions: Array<{ label: string }>) => void; + filteredGroups?: Array<{ label: string }>; + linkToDocumentation?: string; + selectedSection?: string; +} + +function DocumentationNav({ + searchText, + setSearchText, + onNavigationChange, + filteredGroups, + linkToDocumentation, + selectedSection, +}: DocumentationNavProps) { + const { euiTheme } = useEuiTheme(); + + return ( + <> + + + + + {i18n.translate('languageDocumentationPopover.esqlDocsLinkLabel', { + defaultMessage: 'View full ES|QL documentation', + })} + + + ) + } + > + + + + + { + setSearchText(e.target.value); + }} + data-test-subj="language-documentation-navigation-search" + placeholder={i18n.translate('languageDocumentationPopover.searchPlaceholder', { + defaultMessage: 'Search', + })} + fullWidth + compressed + /> + + + + ); +} + +export const DocumentationNavigation = React.memo(DocumentationNav); diff --git a/src/plugins/controls/public/services/storage/types.ts b/packages/kbn-language-documentation-popover/src/components/shared/index.ts similarity index 75% rename from src/plugins/controls/public/services/storage/types.ts rename to packages/kbn-language-documentation-popover/src/components/shared/index.ts index 2d1e8b08e836..f00df2358d39 100644 --- a/src/plugins/controls/public/services/storage/types.ts +++ b/packages/kbn-language-documentation-popover/src/components/shared/index.ts @@ -7,7 +7,5 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export interface ControlsStorageService { - getShowInvalidSelectionWarning: () => boolean; - setShowInvalidSelectionWarning: (value: boolean) => void; -} +export { DocumentationNavigation } from './documentation_navigation'; +export { DocumentationMainContent } from './documentation_content'; diff --git a/packages/kbn-text-based-editor/src/inline_documentation/esql_documentation_sections.tsx b/packages/kbn-language-documentation-popover/src/sections/esql_documentation_sections.tsx similarity index 79% rename from packages/kbn-text-based-editor/src/inline_documentation/esql_documentation_sections.tsx rename to packages/kbn-language-documentation-popover/src/sections/esql_documentation_sections.tsx index d0c136cf6d01..21e550f4ca5e 100644 --- a/packages/kbn-text-based-editor/src/inline_documentation/esql_documentation_sections.tsx +++ b/packages/kbn-language-documentation-popover/src/sections/esql_documentation_sections.tsx @@ -17,11 +17,8 @@ const Markdown = (props: Parameters[0]) => ( export const initialSection = ( ); export const sourceCommands = { - label: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.sourceCommands', { + label: i18n.translate('languageDocumentationPopover.documentationESQL.sourceCommands', { defaultMessage: 'Source commands', }), description: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.commandsDescription', + 'languageDocumentationPopover.documentationESQL.commandsDescription', { defaultMessage: `A source command produces a table, typically with data from Elasticsearch. ES|QL supports the following source commands.`, } ), items: [ { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.from', - { - defaultMessage: 'FROM', - } - ), + label: i18n.translate('languageDocumentationPopover.documentationESQL.from', { + defaultMessage: 'FROM', + }), description: ( \` source command returns information about the deployment and its capabilities: @@ -186,28 +173,25 @@ The \`SHOW \` source command returns information about the deployment and }; export const processingCommands = { - label: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.processingCommands', { + label: i18n.translate('languageDocumentationPopover.documentationESQL.processingCommands', { defaultMessage: 'Processing commands', }), description: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.processingCommandsDescription', + 'languageDocumentationPopover.documentationESQL.processingCommandsDescription', { defaultMessage: `Processing commands change an input table by adding, removing, or changing rows and columns. ES|QL supports the following processing commands.`, } ), items: [ { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.dissect', - { - defaultMessage: 'DISSECT', - } - ), + label: i18n.translate('languageDocumentationPopover.documentationESQL.dissect', { + defaultMessage: 'DISSECT', + }), description: ( \` type conversion functions. +The \`::\` operator provides a convenient alternative syntax to the \`TO_\` type conversion functions. Example: \`\`\` @@ -943,16 +882,13 @@ ROW ver = CONCAT(("0"::INT + 1)::STRING, ".2.3")::VERSION ), }, { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.inOperator', - { - defaultMessage: 'IN', - } - ), + label: i18n.translate('languageDocumentationPopover.documentationESQL.inOperator', { + defaultMessage: 'IN', + }), description: ( { + const groups: Array<{ + label: string; + description?: string; + items: Array<{ label: string; description?: JSX.Element }>; + }> = []; + const { + sourceCommands, + processingCommands, + initialSection, + scalarFunctions, + aggregationFunctions, + groupingFunctions, + operators, + } = await import('./esql_documentation_sections'); + groups.push({ + label: i18n.translate('languageDocumentationPopover.esqlSections.initialSectionLabel', { + defaultMessage: 'ES|QL', + }), + items: [], + }); + groups.push( + sourceCommands, + processingCommands, + scalarFunctions, + aggregationFunctions, + groupingFunctions, + operators + ); + return { + groups, + initialSection, + }; +}; diff --git a/src/plugins/controls/public/services/embeddable/types.ts b/packages/kbn-language-documentation-popover/src/types.ts similarity index 68% rename from src/plugins/controls/public/services/embeddable/types.ts rename to packages/kbn-language-documentation-popover/src/types.ts index 917f03fb55e9..863c00b94e21 100644 --- a/src/plugins/controls/public/services/embeddable/types.ts +++ b/packages/kbn-language-documentation-popover/src/types.ts @@ -7,8 +7,11 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { EmbeddableStart } from '@kbn/embeddable-plugin/public'; - -export interface ControlsEmbeddableService { - getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; +export interface LanguageDocumentationSections { + groups: Array<{ + label: string; + description?: string; + items: Array<{ label: string; description?: JSX.Element }>; + }>; + initialSection: JSX.Element; } diff --git a/packages/kbn-language-documentation-popover/src/utils/get_filtered_groups.test.tsx b/packages/kbn-language-documentation-popover/src/utils/get_filtered_groups.test.tsx new file mode 100644 index 000000000000..9dfe443f10b0 --- /dev/null +++ b/packages/kbn-language-documentation-popover/src/utils/get_filtered_groups.test.tsx @@ -0,0 +1,85 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import React from 'react'; +import { Markdown } from '@kbn/shared-ux-markdown'; +import { getFilteredGroups } from './get_filtered_groups'; + +describe('getFilteredGroups', () => { + const sections = { + groups: [ + { + label: 'Section one', + description: 'Section 1 description', + items: [], + }, + { + label: 'Section two', + items: [ + { + label: 'Section two item 1 blah blah', + description: ( + + ), + }, + { + label: 'Section two item 2', + description: ( + + ), + }, + ], + }, + { + label: 'Section three ', + items: [ + { + label: 'Section three item 1', + description: ( + + ), + }, + { + label: 'Section three item 2', + description: ( + + ), + }, + ], + }, + ], + initialSection: Here is the initial section, + }; + test('Should return the sections as it gets them if the search string is empty', () => { + const filteredSections = getFilteredGroups('', false, sections); + expect(filteredSections).toStrictEqual([ + ...sections.groups.map((group) => ({ ...group, options: group.items })), + ]); + }); + + test('Should return the 2 last sections as it gets them if the search string is empty and the numOfGroupsToOmit is set to 1', () => { + const filteredSections = getFilteredGroups('', false, sections, 1); + expect(filteredSections).toStrictEqual([ + ...sections.groups.slice(1).map((group) => ({ ...group, options: group.items })), + ]); + }); + + test('Should return the section two as it gets it if the search string is asking for this', () => { + const filteredSections = getFilteredGroups('tWo', false, sections); + expect(filteredSections).toStrictEqual([ + { ...sections.groups[1], options: sections.groups[1].items }, + ]); + }); + + test('Should return the section two filtered on the search string if it is allowed to search in description', () => { + const filteredSections = getFilteredGroups('Section two item 1 blah blah', true, sections); + expect(filteredSections).toStrictEqual([ + { ...sections.groups[1], options: [sections.groups[1].items[0]] }, + ]); + }); +}); diff --git a/packages/kbn-language-documentation-popover/src/utils/get_filtered_groups.ts b/packages/kbn-language-documentation-popover/src/utils/get_filtered_groups.ts new file mode 100644 index 000000000000..2ac252bdad77 --- /dev/null +++ b/packages/kbn-language-documentation-popover/src/utils/get_filtered_groups.ts @@ -0,0 +1,41 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import type { LanguageDocumentationSections } from '../types'; +import { elementToString } from './element_to_string'; + +export const getFilteredGroups = ( + searchText: string, + searchInDescription?: boolean, + sections?: LanguageDocumentationSections, + numOfGroupsToOmit?: number +) => { + const normalizedSearchText = searchText.trim().toLocaleLowerCase(); + return sections?.groups + .slice(numOfGroupsToOmit ?? 0) + .map((group) => { + const options = group.items.filter((helpItem) => { + return ( + !normalizedSearchText || + helpItem.label.toLocaleLowerCase().includes(normalizedSearchText) || + // Converting the JSX element to a string first + (searchInDescription && + elementToString(helpItem.description) + ?.toLocaleLowerCase() + .includes(normalizedSearchText)) + ); + }); + return { ...group, options }; + }) + .filter((group) => { + if (group.options.length > 0 || !normalizedSearchText) { + return true; + } + return group.label.toLocaleLowerCase().includes(normalizedSearchText); + }); +}; diff --git a/packages/kbn-language-documentation-popover/tsconfig.json b/packages/kbn-language-documentation-popover/tsconfig.json index 48da6397a644..f613b6cb759a 100644 --- a/packages/kbn-language-documentation-popover/tsconfig.json +++ b/packages/kbn-language-documentation-popover/tsconfig.json @@ -5,6 +5,7 @@ "types": [ "jest", "node", + "@emotion/react/types/css-prop", ] }, "include": [ diff --git a/packages/kbn-text-based-editor/README.md b/packages/kbn-text-based-editor/README.md index 2bb9ae5887f2..ae7846a58d20 100644 --- a/packages/kbn-text-based-editor/README.md +++ b/packages/kbn-text-based-editor/README.md @@ -14,7 +14,6 @@ In order to enable text based languages on your unified search bar add `textBase ## Languages supported -- SQL: based on the Elasticsearch sql api - ESQL: based on the Elastisearch esql api diff --git a/packages/kbn-text-based-editor/package.json b/packages/kbn-text-based-editor/package.json index 3eeb282f953c..47d3d426b21f 100644 --- a/packages/kbn-text-based-editor/package.json +++ b/packages/kbn-text-based-editor/package.json @@ -5,10 +5,5 @@ "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0", "sideEffects": [ "*.scss" - ], - "scripts": { - "make:docs": "ts-node --transpileOnly scripts/generate_esql_docs.ts", - "postmake:docs": "yarn run lint:fix", - "lint:fix": "cd ../.. && node ./scripts/eslint --fix ./packages/kbn-text-based-editor/src/inline_documentation/generated" - } + ] } diff --git a/packages/kbn-text-based-editor/src/editor_footer/index.tsx b/packages/kbn-text-based-editor/src/editor_footer/index.tsx index fa60eb1a32f4..6468a2c08a8b 100644 --- a/packages/kbn-text-based-editor/src/editor_footer/index.tsx +++ b/packages/kbn-text-based-editor/src/editor_footer/index.tsx @@ -7,18 +7,25 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { memo, useState, useCallback, useEffect, useMemo } from 'react'; +import React, { memo, useState, useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiText, EuiFlexGroup, EuiFlexItem, EuiCode } from '@elastic/eui'; +import { + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiCode, + EuiButtonIcon, + EuiButtonEmpty, +} from '@elastic/eui'; import { Interpolation, Theme, css } from '@emotion/react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { - LanguageDocumentationPopover, - type LanguageDocumentationSections, + LanguageDocumentationInline, + LanguageDocumentationFlyout, } from '@kbn/language-documentation-popover'; import { getLimitFromESQLQuery } from '@kbn/esql-utils'; -import { type MonacoMessage, getDocumentationSections } from '../helpers'; +import { type MonacoMessage } from '../helpers'; import { ErrorsWarningsFooterPopover } from './errors_warnings_popover'; import { QueryHistoryAction, QueryHistory } from './query_history'; import { SubmitFeedbackComponent } from './feedback_component'; @@ -43,8 +50,6 @@ interface EditorFooterProps { updateQuery: (qs: string) => void; isHistoryOpen: boolean; setIsHistoryOpen: (status: boolean) => void; - isHelpMenuOpen: boolean; - setIsHelpMenuOpen: (status: boolean) => void; measuredContainerWidth: number; hideRunQueryText?: boolean; editorIsInline?: boolean; @@ -52,6 +57,7 @@ interface EditorFooterProps { hideTimeFilterInfo?: boolean; hideQueryHistory?: boolean; isInCompactMode?: boolean; + displayDocumentationAsFlyout?: boolean; } export const EditorFooter = memo(function EditorFooter({ @@ -71,18 +77,15 @@ export const EditorFooter = memo(function EditorFooter({ setIsHistoryOpen, hideQueryHistory, isInCompactMode, + displayDocumentationAsFlyout, measuredContainerWidth, code, - isHelpMenuOpen, - setIsHelpMenuOpen, }: EditorFooterProps) { const kibana = useKibana(); const { docLinks } = kibana.services; - const [isErrorPopoverOpen, setIsErrorPopoverOpen] = useState(false); + const [isLanguageComponentOpen, setIsLanguageComponentOpen] = useState(false); const [isWarningPopoverOpen, setIsWarningPopoverOpen] = useState(false); - const [documentationSections, setDocumentationSections] = - useState(); const onUpdateAndSubmit = useCallback( (qs: string) => { @@ -98,17 +101,17 @@ export const EditorFooter = memo(function EditorFooter({ [runQuery, updateQuery] ); - const limit = useMemo(() => getLimitFromESQLQuery(code), [code]); + const toggleHistoryComponent = useCallback(() => { + setIsHistoryOpen(!isHistoryOpen); + setIsLanguageComponentOpen(false); + }, [isHistoryOpen, setIsHistoryOpen]); - useEffect(() => { - async function getDocumentation() { - const sections = await getDocumentationSections('esql'); - setDocumentationSections(sections); - } - if (!documentationSections) { - getDocumentation(); - } - }, [documentationSections]); + const toggleLanguageComponent = useCallback(async () => { + setIsLanguageComponentOpen(!isLanguageComponentOpen); + setIsHistoryOpen(false); + }, [isLanguageComponentOpen, setIsHistoryOpen]); + + const limit = useMemo(() => getLimitFromESQLQuery(code), [code]); return ( )} - {documentationSections && !editorIsInline && ( - - + toggleLanguageComponent()} + css={css` + cursor: pointer; + `} + /> + - + )} @@ -318,34 +318,14 @@ export const EditorFooter = memo(function EditorFooter({ {!hideQueryHistory && ( setIsHistoryOpen(!isHistoryOpen)} + toggleHistory={toggleHistoryComponent} isHistoryOpen={isHistoryOpen} isSpaceReduced={true} /> )} - {documentationSections && ( - - - - )} + + + @@ -362,6 +342,11 @@ export const EditorFooter = memo(function EditorFooter({ /> )} + {isLanguageComponentOpen && editorIsInline && ( + + + + )} ); }); diff --git a/packages/kbn-text-based-editor/src/helpers.ts b/packages/kbn-text-based-editor/src/helpers.ts index 0ba6d5004606..b1bb3aaf826a 100644 --- a/packages/kbn-text-based-editor/src/helpers.ts +++ b/packages/kbn-text-based-editor/src/helpers.ts @@ -161,43 +161,6 @@ export const parseErrors = (errors: Error[], code: string): MonacoMessage[] => { }); }; -export const getDocumentationSections = async (language: string) => { - const groups: Array<{ - label: string; - description?: string; - items: Array<{ label: string; description?: JSX.Element }>; - }> = []; - if (language === 'esql') { - const { - sourceCommands, - processingCommands, - initialSection, - scalarFunctions, - aggregationFunctions, - groupingFunctions, - operators, - } = await import('./inline_documentation/esql_documentation_sections'); - groups.push({ - label: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.esql', { - defaultMessage: 'ES|QL', - }), - items: [], - }); - groups.push( - sourceCommands, - processingCommands, - scalarFunctions, - aggregationFunctions, - groupingFunctions, - operators - ); - return { - groups, - initialSection, - }; - } -}; - export const getIndicesList = async (dataViews: DataViewsPublicPluginStart) => { const indices = await dataViews.getIndices({ showAllIndices: false, diff --git a/packages/kbn-text-based-editor/src/text_based_languages_editor.test.tsx b/packages/kbn-text-based-editor/src/text_based_languages_editor.test.tsx index d8fe9512691a..0855d0326263 100644 --- a/packages/kbn-text-based-editor/src/text_based_languages_editor.test.tsx +++ b/packages/kbn-text-based-editor/src/text_based_languages_editor.test.tsx @@ -16,21 +16,6 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { TextBasedLanguagesEditor } from './text_based_languages_editor'; import type { TextBasedLanguagesEditorProps } from './types'; import { ReactWrapper } from 'enzyme'; - -jest.mock('./helpers', () => { - const module = jest.requireActual('./helpers'); - return { - ...module, - getDocumentationSections: () => ({ - groups: [ - { - label: 'How it works', - items: [], - }, - ], - }), - }; -}); import { of } from 'rxjs'; describe('TextBasedLanguagesEditor', () => { @@ -133,9 +118,6 @@ describe('TextBasedLanguagesEditor', () => { expect( component!.find('[data-test-subj="TextBasedLangEditor-toggleWordWrap"]').length ).not.toBe(0); - expect(component!.find('[data-test-subj="TextBasedLangEditor-documentation"]').length).not.toBe( - 0 - ); }); it('should render the resize for the expanded code editor mode', async () => { @@ -156,6 +138,18 @@ describe('TextBasedLanguagesEditor', () => { expect(component.find('[data-test-subj="TextBasedLangEditor-run-query"]').length).not.toBe(0); }); + it('should render the doc icon if the displayDocumentationAsFlyout is true', async () => { + const newProps = { + ...props, + displayDocumentationAsFlyout: true, + editorIsInline: false, + }; + const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps })); + expect(component.find('[data-test-subj="TextBasedLangEditor-documentation"]').length).not.toBe( + 0 + ); + }); + it('should not render the run query text if the hideRunQueryText prop is set to true', async () => { const newProps = { ...props, diff --git a/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx b/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx index d6936bde6fcb..2ebd5a42a966 100644 --- a/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx +++ b/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx @@ -78,6 +78,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ hideTimeFilterInfo, hideQueryHistory, hasOutline, + displayDocumentationAsFlyout, }: TextBasedLanguagesEditorProps) { const popoverRef = useRef(null); const datePickerOpenStatusRef = useRef(false); @@ -108,7 +109,6 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ const [isHistoryOpen, setIsHistoryOpen] = useState(false); const [isCodeEditorExpandedFocused, setIsCodeEditorExpandedFocused] = useState(false); - const [isLanguagePopoverOpen, setIsLanguagePopoverOpen] = useState(false); const [isQueryLoading, setIsQueryLoading] = useState(true); const [abortController, setAbortController] = useState(new AbortController()); @@ -738,8 +738,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ setIsHistoryOpen={toggleHistory} measuredContainerWidth={measuredEditorWidth} hideQueryHistory={hideHistoryComponent} - isHelpMenuOpen={isLanguagePopoverOpen} - setIsHelpMenuOpen={setIsLanguagePopoverOpen} + displayDocumentationAsFlyout={displayDocumentationAsFlyout} /> { diff --git a/src/plugins/controls/common/constants.ts b/src/plugins/controls/common/constants.ts index e100474177c7..e375a7b2315b 100644 --- a/src/plugins/controls/common/constants.ts +++ b/src/plugins/controls/common/constants.ts @@ -7,11 +7,11 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { ControlStyle, ControlWidth } from './types'; +import { ControlLabelPosition, ControlWidth } from './types'; export const DEFAULT_CONTROL_WIDTH: ControlWidth = 'medium'; export const DEFAULT_CONTROL_GROW: boolean = true; -export const DEFAULT_CONTROL_STYLE: ControlStyle = 'oneLine'; +export const DEFAULT_CONTROL_LABEL_POSITION: ControlLabelPosition = 'oneLine'; export const TIME_SLIDER_CONTROL = 'timeSlider'; export const RANGE_SLIDER_CONTROL = 'rangeSliderControl'; diff --git a/src/plugins/controls/common/control_group/types.ts b/src/plugins/controls/common/control_group/types.ts index cb51cf79e540..eb47d8b13eb7 100644 --- a/src/plugins/controls/common/control_group/types.ts +++ b/src/plugins/controls/common/control_group/types.ts @@ -8,7 +8,7 @@ */ import { DataViewField } from '@kbn/data-views-plugin/common'; -import { ControlStyle, DefaultControlState, ParentIgnoreSettings } from '../types'; +import { ControlLabelPosition, DefaultControlState, ParentIgnoreSettings } from '../types'; export const CONTROL_GROUP_TYPE = 'control_group'; @@ -31,7 +31,7 @@ export interface ControlGroupEditorConfig { export interface ControlGroupRuntimeState { chainingSystem: ControlGroupChainingSystem; - labelPosition: ControlStyle; // TODO: Rename this type to ControlLabelPosition + labelPosition: ControlLabelPosition; autoApplySelections: boolean; ignoreParentSettings?: ParentIgnoreSettings; @@ -50,7 +50,7 @@ export interface ControlGroupSerializedState ignoreParentSettingsJSON: string; // In runtime state, we refer to this property as `labelPosition`; // to avoid migrations, we will continue to refer to this property as `controlStyle` in the serialized state - controlStyle: ControlStyle; + controlStyle: ControlLabelPosition; // In runtime state, we refer to the inverse of this property as `autoApplySelections` // to avoid migrations, we will continue to refer to this property as `showApplySelections` in the serialized state showApplySelections?: boolean; diff --git a/src/plugins/controls/common/index.ts b/src/plugins/controls/common/index.ts index c59e4c04ac1b..dd9c56778bb6 100644 --- a/src/plugins/controls/common/index.ts +++ b/src/plugins/controls/common/index.ts @@ -8,7 +8,7 @@ */ export type { - ControlStyle, + ControlLabelPosition, ControlWidth, DefaultControlState, DefaultDataControlState, @@ -18,7 +18,7 @@ export type { export { DEFAULT_CONTROL_GROW, - DEFAULT_CONTROL_STYLE, + DEFAULT_CONTROL_LABEL_POSITION, DEFAULT_CONTROL_WIDTH, OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL, diff --git a/src/plugins/controls/common/options_list/types.ts b/src/plugins/controls/common/options_list/types.ts index e5eccbcab5cf..10d4a8855358 100644 --- a/src/plugins/controls/common/options_list/types.ts +++ b/src/plugins/controls/common/options_list/types.ts @@ -15,8 +15,6 @@ import { OptionsListSortingType } from './suggestions_sorting'; import { DefaultDataControlState } from '../types'; import { OptionsListSearchTechnique } from './suggestions_searching'; -export const OPTIONS_LIST_CONTROL = 'optionsListControl'; // TODO: Replace with OPTIONS_LIST_CONTROL_TYPE - /** * ---------------------------------------------------------------- * Options list state types diff --git a/src/plugins/controls/common/types.ts b/src/plugins/controls/common/types.ts index 34d4708b3e99..d3a6261aeb9d 100644 --- a/src/plugins/controls/common/types.ts +++ b/src/plugins/controls/common/types.ts @@ -8,7 +8,7 @@ */ export type ControlWidth = 'small' | 'medium' | 'large'; -export type ControlStyle = 'twoLine' | 'oneLine'; +export type ControlLabelPosition = 'twoLine' | 'oneLine'; export type TimeSlice = [number, number]; diff --git a/src/plugins/controls/jest_setup.ts b/src/plugins/controls/jest_setup.ts index 722f87562328..04a52e3b6653 100644 --- a/src/plugins/controls/jest_setup.ts +++ b/src/plugins/controls/jest_setup.ts @@ -8,8 +8,5 @@ */ // Start the services with stubs -import { pluginServices } from './public/services'; -import { registry } from './public/services/plugin_services.stub'; - -registry.start({}); -pluginServices.setRegistry(registry); +import { setStubKibanaServices } from './public/services/mocks'; +setStubKibanaServices(); diff --git a/src/plugins/controls/kibana.jsonc b/src/plugins/controls/kibana.jsonc index bd65ecc2d0b6..add8c14ee339 100644 --- a/src/plugins/controls/kibana.jsonc +++ b/src/plugins/controls/kibana.jsonc @@ -9,8 +9,6 @@ "browser": true, "requiredPlugins": [ "presentationUtil", - "kibanaReact", - "expressions", "embeddable", "dataViews", "data", @@ -18,6 +16,6 @@ "uiActions" ], "extraPublicDirs": ["common"], - "requiredBundles": ["kibanaUtils"] + "requiredBundles": [] } } diff --git a/src/plugins/controls/public/actions/clear_control_action.tsx b/src/plugins/controls/public/actions/clear_control_action.tsx index b7c2777473fd..02347ace2fd8 100644 --- a/src/plugins/controls/public/actions/clear_control_action.tsx +++ b/src/plugins/controls/public/actions/clear_control_action.tsx @@ -11,42 +11,10 @@ import React, { SyntheticEvent } from 'react'; import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { - apiIsPresentationContainer, - type PresentationContainer, -} from '@kbn/presentation-containers'; -import { - apiCanAccessViewMode, - apiHasParentApi, - apiHasType, - apiHasUniqueId, - apiIsOfType, - type EmbeddableApiContext, - type HasParentApi, - type HasType, - type HasUniqueId, -} from '@kbn/presentation-publishing'; -import { type Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; +import type { EmbeddableApiContext, HasUniqueId } from '@kbn/presentation-publishing'; +import { IncompatibleActionError, type Action } from '@kbn/ui-actions-plugin/public'; import { ACTION_CLEAR_CONTROL } from '.'; -import { CONTROL_GROUP_TYPE } from '..'; -import { isClearableControl, type CanClearSelections } from '../types'; - -export type ClearControlActionApi = HasType & - HasUniqueId & - CanClearSelections & - HasParentApi; - -const isApiCompatible = (api: unknown | null): api is ClearControlActionApi => - Boolean( - apiHasType(api) && - apiHasUniqueId(api) && - isClearableControl(api) && - apiHasParentApi(api) && - apiCanAccessViewMode(api.parentApi) && - apiIsOfType(api.parentApi, CONTROL_GROUP_TYPE) && - apiIsPresentationContainer(api.parentApi) - ); export class ClearControlAction implements Action { public readonly type = ACTION_CLEAR_CONTROL; @@ -56,12 +24,10 @@ export class ClearControlAction implements Action { constructor() {} public readonly MenuItem = ({ context }: { context: EmbeddableApiContext }) => { - if (!isApiCompatible(context.embeddable)) throw new IncompatibleActionError(); - return ( ) => { @@ -75,23 +41,24 @@ export class ClearControlAction implements Action { }; public getDisplayName({ embeddable }: EmbeddableApiContext) { - if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); return i18n.translate('controls.controlGroup.floatingActions.clearTitle', { defaultMessage: 'Clear', }); } public getIconType({ embeddable }: EmbeddableApiContext) { - if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); return 'eraser'; } public async isCompatible({ embeddable }: EmbeddableApiContext) { - return isApiCompatible(embeddable); + const { isCompatible } = await import('./clear_control_action_compatibility_check'); + return isCompatible(embeddable); } public async execute({ embeddable }: EmbeddableApiContext) { - if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); + const { compatibilityCheck } = await import('./clear_control_action_compatibility_check'); + if (!compatibilityCheck(embeddable)) throw new IncompatibleActionError(); + embeddable.clearSelections(); } } diff --git a/src/plugins/controls/public/actions/clear_control_action_compatibility_check.ts b/src/plugins/controls/public/actions/clear_control_action_compatibility_check.ts new file mode 100644 index 000000000000..f04cb91bc9a3 --- /dev/null +++ b/src/plugins/controls/public/actions/clear_control_action_compatibility_check.ts @@ -0,0 +1,42 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { PresentationContainer, apiIsPresentationContainer } from '@kbn/presentation-containers'; +import { + HasParentApi, + HasType, + HasUniqueId, + apiCanAccessViewMode, + apiHasParentApi, + apiHasType, + apiHasUniqueId, + apiIsOfType, +} from '@kbn/presentation-publishing'; +import { CONTROL_GROUP_TYPE } from '../../common'; +import { isClearableControl, type CanClearSelections } from '../types'; + +type ClearControlActionApi = HasType & + HasUniqueId & + CanClearSelections & + HasParentApi; + +export const compatibilityCheck = (api: unknown | null): api is ClearControlActionApi => + Boolean( + apiHasType(api) && + apiHasUniqueId(api) && + isClearableControl(api) && + apiHasParentApi(api) && + apiCanAccessViewMode(api.parentApi) && + apiIsOfType(api.parentApi, CONTROL_GROUP_TYPE) && + apiIsPresentationContainer(api.parentApi) + ); + +export function isCompatible(api: unknown) { + return compatibilityCheck(api); +} diff --git a/src/plugins/controls/public/actions/delete_control_action.test.tsx b/src/plugins/controls/public/actions/delete_control_action.test.tsx index 65be8a65ecd6..c158d743f69a 100644 --- a/src/plugins/controls/public/actions/delete_control_action.test.tsx +++ b/src/plugins/controls/public/actions/delete_control_action.test.tsx @@ -9,22 +9,15 @@ import { BehaviorSubject } from 'rxjs'; -import { coreMock } from '@kbn/core/public/mocks'; -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; -import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { ViewMode } from '@kbn/presentation-publishing'; - import { getOptionsListControlFactory } from '../react_controls/controls/data_controls/options_list_control/get_options_list_control_factory'; import { OptionsListControlApi } from '../react_controls/controls/data_controls/options_list_control/types'; import { getMockedBuildApi, getMockedControlGroupApi, } from '../react_controls/controls/mocks/control_mocks'; -import { pluginServices } from '../services'; import { DeleteControlAction } from './delete_control_action'; - -const mockDataViews = dataViewPluginMocks.createStartContract(); -const mockCore = coreMock.createStart(); +import { coreServices } from '../services/kibana_services'; const dashboardApi = { viewMode: new BehaviorSubject('view'), @@ -38,11 +31,7 @@ const controlGroupApi = getMockedControlGroupApi(dashboardApi, { let controlApi: OptionsListControlApi; beforeAll(async () => { - const controlFactory = getOptionsListControlFactory({ - core: mockCore, - data: dataPluginMock.createStartContract(), - dataViews: mockDataViews, - }); + const controlFactory = getOptionsListControlFactory(); const uuid = 'testControl'; const control = await controlFactory.buildControl( @@ -72,7 +61,7 @@ test('Execute throws an error when called with an embeddable not in a parent', a describe('Execute should open a confirm modal', () => { test('Canceling modal will keep control', async () => { const spyOn = jest.fn().mockResolvedValue(false); - pluginServices.getServices().overlays.openConfirm = spyOn; + coreServices.overlays.openConfirm = spyOn; const deleteControlAction = new DeleteControlAction(); await deleteControlAction.execute({ embeddable: controlApi }); @@ -83,7 +72,7 @@ describe('Execute should open a confirm modal', () => { test('Confirming modal will delete control', async () => { const spyOn = jest.fn().mockResolvedValue(true); - pluginServices.getServices().overlays.openConfirm = spyOn; + coreServices.overlays.openConfirm = spyOn; const deleteControlAction = new DeleteControlAction(); await deleteControlAction.execute({ embeddable: controlApi }); diff --git a/src/plugins/controls/public/actions/delete_control_action.tsx b/src/plugins/controls/public/actions/delete_control_action.tsx index 45a7a2038562..7ee55ddd3da6 100644 --- a/src/plugins/controls/public/actions/delete_control_action.tsx +++ b/src/plugins/controls/public/actions/delete_control_action.tsx @@ -10,65 +10,25 @@ import React from 'react'; import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { ViewMode } from '@kbn/embeddable-plugin/public'; import { i18n } from '@kbn/i18n'; -import { - apiIsPresentationContainer, - type PresentationContainer, -} from '@kbn/presentation-containers'; -import { - apiCanAccessViewMode, - apiHasParentApi, - apiHasType, - apiHasUniqueId, - apiIsOfType, - getInheritedViewMode, - type EmbeddableApiContext, - type HasParentApi, - type HasType, - type HasUniqueId, - type PublishesViewMode, -} from '@kbn/presentation-publishing'; +import type { HasUniqueId, EmbeddableApiContext } from '@kbn/presentation-publishing'; import { IncompatibleActionError, type Action } from '@kbn/ui-actions-plugin/public'; import { ACTION_DELETE_CONTROL } from '.'; -import { CONTROL_GROUP_TYPE } from '..'; -import { pluginServices } from '../services'; - -export type DeleteControlActionApi = HasType & - HasUniqueId & - HasParentApi; - -const isApiCompatible = (api: unknown | null): api is DeleteControlActionApi => - Boolean( - apiHasType(api) && - apiHasUniqueId(api) && - apiHasParentApi(api) && - apiCanAccessViewMode(api.parentApi) && - apiIsOfType(api.parentApi, CONTROL_GROUP_TYPE) && - apiIsPresentationContainer(api.parentApi) - ); +import { coreServices } from '../services/kibana_services'; export class DeleteControlAction implements Action { public readonly type = ACTION_DELETE_CONTROL; public readonly id = ACTION_DELETE_CONTROL; public order = 100; // should always be last - private openConfirm; - - constructor() { - ({ - overlays: { openConfirm: this.openConfirm }, - } = pluginServices.getServices()); - } + constructor() {} public readonly MenuItem = ({ context }: { context: EmbeddableApiContext }) => { - if (!isApiCompatible(context.embeddable)) throw new IncompatibleActionError(); - return ( this.execute(context)} @@ -79,46 +39,46 @@ export class DeleteControlAction implements Action { }; public getDisplayName({ embeddable }: EmbeddableApiContext) { - if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); return i18n.translate('controls.controlGroup.floatingActions.removeTitle', { defaultMessage: 'Delete', }); } public getIconType({ embeddable }: EmbeddableApiContext) { - if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); return 'trash'; } public async isCompatible({ embeddable }: EmbeddableApiContext) { - return ( - isApiCompatible(embeddable) && getInheritedViewMode(embeddable.parentApi) === ViewMode.EDIT - ); + const { isCompatible } = await import('./delete_control_action_compatibility_check'); + return isCompatible(embeddable); } public async execute({ embeddable }: EmbeddableApiContext) { - if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); + const { compatibilityCheck } = await import('./delete_control_action_compatibility_check'); + if (!compatibilityCheck(embeddable)) throw new IncompatibleActionError(); - this.openConfirm( - i18n.translate('controls.controlGroup.management.delete.sub', { - defaultMessage: 'Controls are not recoverable once removed.', - }), - { - confirmButtonText: i18n.translate('controls.controlGroup.management.delete.confirm', { - defaultMessage: 'Delete', - }), - cancelButtonText: i18n.translate('controls.controlGroup.management.delete.cancel', { - defaultMessage: 'Cancel', + coreServices.overlays + .openConfirm( + i18n.translate('controls.controlGroup.management.delete.sub', { + defaultMessage: 'Controls are not recoverable once removed.', }), - title: i18n.translate('controls.controlGroup.management.delete.deleteTitle', { - defaultMessage: 'Delete control?', - }), - buttonColor: 'danger', - } - ).then((confirmed) => { - if (confirmed) { - embeddable.parentApi.removePanel(embeddable.uuid); - } - }); + { + confirmButtonText: i18n.translate('controls.controlGroup.management.delete.confirm', { + defaultMessage: 'Delete', + }), + cancelButtonText: i18n.translate('controls.controlGroup.management.delete.cancel', { + defaultMessage: 'Cancel', + }), + title: i18n.translate('controls.controlGroup.management.delete.deleteTitle', { + defaultMessage: 'Delete control?', + }), + buttonColor: 'danger', + } + ) + .then((confirmed) => { + if (confirmed) { + embeddable.parentApi.removePanel(embeddable.uuid); + } + }); } } diff --git a/src/plugins/controls/public/actions/delete_control_action_compatibility_check.ts b/src/plugins/controls/public/actions/delete_control_action_compatibility_check.ts new file mode 100644 index 000000000000..a09b3448b2fc --- /dev/null +++ b/src/plugins/controls/public/actions/delete_control_action_compatibility_check.ts @@ -0,0 +1,42 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { ViewMode } from '@kbn/embeddable-plugin/public'; +import { PresentationContainer, apiIsPresentationContainer } from '@kbn/presentation-containers'; +import { + HasParentApi, + HasType, + HasUniqueId, + PublishesViewMode, + apiCanAccessViewMode, + apiHasParentApi, + apiHasType, + apiHasUniqueId, + apiIsOfType, + getInheritedViewMode, +} from '@kbn/presentation-publishing'; +import { CONTROL_GROUP_TYPE } from '../../common'; + +type DeleteControlActionApi = HasType & + HasUniqueId & + HasParentApi; + +export const compatibilityCheck = (api: unknown | null): api is DeleteControlActionApi => + Boolean( + apiHasType(api) && + apiHasUniqueId(api) && + apiHasParentApi(api) && + apiCanAccessViewMode(api.parentApi) && + apiIsOfType(api.parentApi, CONTROL_GROUP_TYPE) && + apiIsPresentationContainer(api.parentApi) + ); + +export function isCompatible(api: unknown) { + return compatibilityCheck(api) && getInheritedViewMode(api.parentApi) === ViewMode.EDIT; +} diff --git a/src/plugins/controls/public/actions/edit_control_action.test.tsx b/src/plugins/controls/public/actions/edit_control_action.test.tsx index 3c28feb90742..b1c24d779aaf 100644 --- a/src/plugins/controls/public/actions/edit_control_action.test.tsx +++ b/src/plugins/controls/public/actions/edit_control_action.test.tsx @@ -9,9 +9,6 @@ import { BehaviorSubject } from 'rxjs'; -import { coreMock } from '@kbn/core/public/mocks'; -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; -import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import dateMath from '@kbn/datemath'; import type { TimeRange } from '@kbn/es-query'; import type { ViewMode } from '@kbn/presentation-publishing'; @@ -23,12 +20,10 @@ import { getMockedControlGroupApi, } from '../react_controls/controls/mocks/control_mocks'; import { getTimesliderControlFactory } from '../react_controls/controls/timeslider_control/get_timeslider_control_factory'; +import { dataService } from '../services/kibana_services'; import { EditControlAction } from './edit_control_action'; -const mockDataViews = dataViewPluginMocks.createStartContract(); -const mockCore = coreMock.createStart(); -const dataStartServiceMock = dataPluginMock.createStartContract(); -dataStartServiceMock.query.timefilter.timefilter.calculateBounds = (timeRange: TimeRange) => { +dataService.query.timefilter.timefilter.calculateBounds = (timeRange: TimeRange) => { const now = new Date(); return { min: dateMath.parse(timeRange.from, { forceNow: now }), @@ -48,11 +43,7 @@ const controlGroupApi = getMockedControlGroupApi(dashboardApi, { let optionsListApi: OptionsListControlApi; beforeAll(async () => { - const controlFactory = getOptionsListControlFactory({ - core: mockCore, - data: dataStartServiceMock, - dataViews: mockDataViews, - }); + const controlFactory = getOptionsListControlFactory(); const optionsListUuid = 'optionsListControl'; const optionsListControl = await controlFactory.buildControl( @@ -73,10 +64,7 @@ beforeAll(async () => { describe('Incompatible embeddables', () => { test('Action is incompatible with embeddables that are not editable', async () => { - const timeSliderFactory = getTimesliderControlFactory({ - core: mockCore, - data: dataStartServiceMock, - }); + const timeSliderFactory = getTimesliderControlFactory(); const timeSliderUuid = 'timeSliderControl'; const timeSliderControl = await timeSliderFactory.buildControl( {}, diff --git a/src/plugins/controls/public/index.ts b/src/plugins/controls/public/index.ts index 6a490248b892..6c7a548cb091 100644 --- a/src/plugins/controls/public/index.ts +++ b/src/plugins/controls/public/index.ts @@ -21,7 +21,6 @@ export { ACTION_CLEAR_CONTROL, ACTION_DELETE_CONTROL, ACTION_EDIT_CONTROL } from export type { DataControlApi, DataControlFactory, - DataControlServices, } from './react_controls/controls/data_controls/types'; export { diff --git a/src/plugins/controls/public/plugin.ts b/src/plugins/controls/public/plugin.ts index 039b960bfc3c..c6e1a2873b16 100644 --- a/src/plugins/controls/public/plugin.ts +++ b/src/plugins/controls/public/plugin.ts @@ -10,63 +10,36 @@ import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; import { PANEL_HOVER_TRIGGER } from '@kbn/embeddable-plugin/public'; +import { ClearControlAction } from './actions/clear_control_action'; +import { DeleteControlAction } from './actions/delete_control_action'; +import { EditControlAction } from './actions/edit_control_action'; import { registerControlGroupEmbeddable } from './react_controls/control_group/register_control_group_embeddable'; import { registerOptionsListControl } from './react_controls/controls/data_controls/options_list_control/register_options_list_control'; import { registerRangeSliderControl } from './react_controls/controls/data_controls/range_slider/register_range_slider_control'; import { registerTimeSliderControl } from './react_controls/controls/timeslider_control/register_timeslider_control'; -import { controlsService } from './services/controls/controls_service'; -import type { - ControlsPluginSetup, - ControlsPluginSetupDeps, - ControlsPluginStart, - ControlsPluginStartDeps, -} from './types'; +import { setKibanaServices, untilPluginStartServicesReady } from './services/kibana_services'; +import type { ControlsPluginSetupDeps, ControlsPluginStartDeps } from './types'; export class ControlsPlugin - implements - Plugin< - ControlsPluginSetup, - ControlsPluginStart, - ControlsPluginSetupDeps, - ControlsPluginStartDeps - > + implements Plugin { - private async startControlsKibanaServices( - coreStart: CoreStart, - startPlugins: ControlsPluginStartDeps - ) { - const { registry, pluginServices } = await import('./services/plugin_services'); - pluginServices.setRegistry(registry.start({ coreStart, startPlugins })); - } - public setup( - _coreSetup: CoreSetup, + _coreSetup: CoreSetup, _setupPlugins: ControlsPluginSetupDeps - ): ControlsPluginSetup { - const { registerControlFactory } = controlsService; + ) { const { embeddable } = _setupPlugins; - registerControlGroupEmbeddable(_coreSetup, embeddable); - registerOptionsListControl(_coreSetup); - registerRangeSliderControl(_coreSetup); - registerTimeSliderControl(_coreSetup); - - return { - registerControlFactory, - }; + registerControlGroupEmbeddable(embeddable); + registerOptionsListControl(); + registerRangeSliderControl(); + registerTimeSliderControl(); } - public start(coreStart: CoreStart, startPlugins: ControlsPluginStartDeps): ControlsPluginStart { - this.startControlsKibanaServices(coreStart, startPlugins).then(async () => { - const { uiActions } = startPlugins; - - const [{ DeleteControlAction }, { EditControlAction }, { ClearControlAction }] = - await Promise.all([ - import('./actions/delete_control_action'), - import('./actions/edit_control_action'), - import('./actions/clear_control_action'), - ]); + public start(coreStart: CoreStart, startPlugins: ControlsPluginStartDeps) { + const { uiActions } = startPlugins; + setKibanaServices(coreStart, startPlugins); + untilPluginStartServicesReady().then(() => { const deleteControlAction = new DeleteControlAction(); uiActions.registerAction(deleteControlAction); uiActions.attachAction(PANEL_HOVER_TRIGGER, deleteControlAction.id); @@ -79,12 +52,6 @@ export class ControlsPlugin uiActions.registerAction(clearControlAction); uiActions.attachAction(PANEL_HOVER_TRIGGER, clearControlAction.id); }); - - const { getControlFactory, getAllControlTypes } = controlsService; - return { - getControlFactory, - getAllControlTypes, - }; } public stop() {} diff --git a/src/plugins/controls/public/react_controls/control_group/components/control_error.tsx b/src/plugins/controls/public/react_controls/control_group/components/control_error.tsx index 52b9e7d9b806..2ef6b06faeed 100644 --- a/src/plugins/controls/public/react_controls/control_group/components/control_error.tsx +++ b/src/plugins/controls/public/react_controls/control_group/components/control_error.tsx @@ -13,8 +13,6 @@ import { EuiButtonEmpty, EuiPopover } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { Markdown } from '@kbn/shared-ux-markdown'; -/** TODO: This file is duplicated from the controls plugin to avoid exporting it */ - interface ControlErrorProps { error: Error | string; } diff --git a/src/plugins/controls/public/react_controls/control_group/components/control_group.tsx b/src/plugins/controls/public/react_controls/control_group/components/control_group.tsx index 9c962113d1a7..54e778684806 100644 --- a/src/plugins/controls/public/react_controls/control_group/components/control_group.tsx +++ b/src/plugins/controls/public/react_controls/control_group/components/control_group.tsx @@ -30,7 +30,7 @@ import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiToolTip } from ' import { css } from '@emotion/react'; import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; -import type { ControlStyle } from '../../../../common'; +import type { ControlLabelPosition } from '../../../../common'; import type { DefaultControlApi } from '../../controls/types'; import { ControlGroupStrings } from '../control_group_strings'; import { ControlsInOrder } from '../init_controls_manager'; @@ -49,7 +49,7 @@ interface Props { setControlApi: (uuid: string, controlApi: DefaultControlApi) => void; }; hasUnappliedSelections: boolean; - labelPosition: ControlStyle; + labelPosition: ControlLabelPosition; } export function ControlGroup({ diff --git a/src/plugins/controls/public/react_controls/control_group/components/control_group_editor.test.tsx b/src/plugins/controls/public/react_controls/control_group/components/control_group_editor.test.tsx index 79d0312b2953..b3705106afe2 100644 --- a/src/plugins/controls/public/react_controls/control_group/components/control_group_editor.test.tsx +++ b/src/plugins/controls/public/react_controls/control_group/components/control_group_editor.test.tsx @@ -15,8 +15,8 @@ import { render } from '@testing-library/react'; import { ControlGroupApi } from '../../..'; import { ControlGroupChainingSystem, - ControlStyle, - DEFAULT_CONTROL_STYLE, + ControlLabelPosition, + DEFAULT_CONTROL_LABEL_POSITION, ParentIgnoreSettings, } from '../../../../common'; import { DefaultControlApi } from '../../controls/types'; @@ -33,7 +33,7 @@ describe('render', () => { onDeleteAll: () => {}, stateManager: { chainingSystem: new BehaviorSubject('HIERARCHICAL'), - labelPosition: new BehaviorSubject(DEFAULT_CONTROL_STYLE), + labelPosition: new BehaviorSubject(DEFAULT_CONTROL_LABEL_POSITION), autoApplySelections: new BehaviorSubject(true), ignoreParentSettings: new BehaviorSubject(undefined), }, diff --git a/src/plugins/controls/public/react_controls/control_group/components/control_group_editor.tsx b/src/plugins/controls/public/react_controls/control_group/components/control_group_editor.tsx index f908a557366f..c4e7dc61476b 100644 --- a/src/plugins/controls/public/react_controls/control_group/components/control_group_editor.tsx +++ b/src/plugins/controls/public/react_controls/control_group/components/control_group_editor.tsx @@ -27,7 +27,7 @@ import { } from '@elastic/eui'; import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; -import type { ControlStyle, ParentIgnoreSettings } from '../../../../common'; +import type { ControlLabelPosition, ParentIgnoreSettings } from '../../../../common'; import { CONTROL_LAYOUT_OPTIONS } from '../../controls/data_controls/editor_constants'; import type { ControlStateManager } from '../../controls/types'; import { ControlGroupStrings } from '../control_group_strings'; @@ -86,7 +86,7 @@ export const ControlGroupEditor = ({ onCancel, onSave, onDeleteAll, stateManager idSelected={selectedLabelPosition} legend={ControlGroupStrings.management.labelPosition.getLabelPositionLegend()} onChange={(newPosition: string) => { - stateManager.labelPosition.next(newPosition as ControlStyle); + stateManager.labelPosition.next(newPosition as ControlLabelPosition); }} /> diff --git a/src/plugins/controls/public/react_controls/control_group/components/control_panel.test.tsx b/src/plugins/controls/public/react_controls/control_group/components/control_panel.test.tsx index bbf8e8127813..365c896bb908 100644 --- a/src/plugins/controls/public/react_controls/control_group/components/control_panel.test.tsx +++ b/src/plugins/controls/public/react_controls/control_group/components/control_panel.test.tsx @@ -14,7 +14,7 @@ import { pluginServices as presentationUtilPluginServices } from '@kbn/presentat import { registry as presentationUtilServicesRegistry } from '@kbn/presentation-util-plugin/public/services/plugin_services.story'; import { render, waitFor } from '@testing-library/react'; -import type { ControlStyle, ControlWidth } from '../../../../common'; +import type { ControlLabelPosition, ControlWidth } from '../../../../common'; import { ControlPanel } from './control_panel'; describe('render', () => { @@ -74,7 +74,7 @@ describe('render', () => { mockApi = { uuid: 'control1', parentApi: { - labelPosition: new BehaviorSubject('oneLine'), + labelPosition: new BehaviorSubject('oneLine'), }, }; const controlPanel = render(); @@ -92,7 +92,7 @@ describe('render', () => { mockApi = { uuid: 'control1', parentApi: { - labelPosition: new BehaviorSubject('twoLine'), + labelPosition: new BehaviorSubject('twoLine'), }, }; const controlPanel = render(); diff --git a/src/plugins/controls/public/react_controls/control_group/get_control_group_factory.tsx b/src/plugins/controls/public/react_controls/control_group/get_control_group_factory.tsx index 6c3e8d10c3c6..77da1480eb49 100644 --- a/src/plugins/controls/public/react_controls/control_group/get_control_group_factory.tsx +++ b/src/plugins/controls/public/react_controls/control_group/get_control_group_factory.tsx @@ -11,9 +11,7 @@ import fastIsEqual from 'fast-deep-equal'; import React, { useEffect } from 'react'; import { BehaviorSubject } from 'rxjs'; -import { CoreStart } from '@kbn/core/public'; import { DataView } from '@kbn/data-views-plugin/common'; -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public'; import { i18n } from '@kbn/i18n'; import { @@ -31,11 +29,11 @@ import type { ControlGroupChainingSystem, ControlGroupRuntimeState, ControlGroupSerializedState, + ControlLabelPosition, ControlPanelsState, - ControlStyle, ParentIgnoreSettings, } from '../../../common'; -import { CONTROL_GROUP_TYPE, DEFAULT_CONTROL_STYLE } from '../../../common'; +import { CONTROL_GROUP_TYPE, DEFAULT_CONTROL_LABEL_POSITION } from '../../../common'; import { openDataControlEditor } from '../controls/data_controls/open_data_control_editor'; import { ControlGroup } from './components/control_group'; import { chaining$, controlFetch$, controlGroupFetch$ } from './control_fetch'; @@ -45,13 +43,11 @@ import { openEditControlGroupFlyout } from './open_edit_control_group_flyout'; import { initSelectionsManager } from './selections_manager'; import type { ControlGroupApi } from './types'; import { deserializeControlGroup } from './utils/serialization_utils'; +import { coreServices, dataViewsService } from '../../services/kibana_services'; const DEFAULT_CHAINING_SYSTEM = 'HIERARCHICAL'; -export const getControlGroupEmbeddableFactory = (services: { - core: CoreStart; - dataViews: DataViewsPublicPluginStart; -}) => { +export const getControlGroupEmbeddableFactory = () => { const controlGroupEmbeddableFactory: ReactEmbeddableFactory< ControlGroupSerializedState, ControlGroupRuntimeState, @@ -75,7 +71,7 @@ export const getControlGroupEmbeddableFactory = (services: { } = initialRuntimeState; const autoApplySelections$ = new BehaviorSubject(autoApplySelections); - const defaultDataViewId = await services.dataViews.getDefaultId(); + const defaultDataViewId = await dataViewsService.getDefaultId(); const lastSavedControlsState$ = new BehaviorSubject( lastSavedRuntimeState.initialChildControlState ); @@ -94,15 +90,12 @@ export const getControlGroupEmbeddableFactory = (services: { const ignoreParentSettings$ = new BehaviorSubject( ignoreParentSettings ); - const labelPosition$ = new BehaviorSubject( // TODO: Rename `ControlStyle` - initialLabelPosition ?? DEFAULT_CONTROL_STYLE // TODO: Rename `DEFAULT_CONTROL_STYLE` + const labelPosition$ = new BehaviorSubject( + initialLabelPosition ?? DEFAULT_CONTROL_LABEL_POSITION ); const allowExpensiveQueries$ = new BehaviorSubject(true); const disabledActionIds$ = new BehaviorSubject(undefined); - /** TODO: Handle loading; loading should be true if any child is loading */ - const dataLoading$ = new BehaviorSubject(false); - const unsavedChanges = initializeControlGroupUnsavedChanges( selectionsManager.applySelections, controlsManager.api.children$, @@ -122,7 +115,10 @@ export const getControlGroupEmbeddableFactory = (services: { (next: ParentIgnoreSettings | undefined) => ignoreParentSettings$.next(next), fastIsEqual, ], - labelPosition: [labelPosition$, (next: ControlStyle) => labelPosition$.next(next)], + labelPosition: [ + labelPosition$, + (next: ControlLabelPosition) => labelPosition$.next(next), + ], }, controlsManager.snapshotControlsRuntimeState, controlsManager.resetControlsUnsavedChanges, @@ -157,18 +153,13 @@ export const getControlGroupEmbeddableFactory = (services: { initialChildControlState: controlsManager.snapshotControlsRuntimeState(), }; }, - dataLoading: dataLoading$, onEdit: async () => { - openEditControlGroupFlyout( - api, - { - chainingSystem: chainingSystem$, - labelPosition: labelPosition$, - autoApplySelections: autoApplySelections$, - ignoreParentSettings: ignoreParentSettings$, - }, - { core: services.core } - ); + openEditControlGroupFlyout(api, { + chainingSystem: chainingSystem$, + labelPosition: labelPosition$, + autoApplySelections: autoApplySelections$, + ignoreParentSettings: ignoreParentSettings$, + }); }, isEditingEnabled: () => true, openAddDataControlFlyout: (settings) => { @@ -193,7 +184,6 @@ export const getControlGroupEmbeddableFactory = (services: { settings?.onSave?.(); }, controlGroupApi: api, - services, }); }, serializeState: () => { @@ -201,7 +191,7 @@ export const getControlGroupEmbeddableFactory = (services: { return { rawState: { chainingSystem: chainingSystem$.getValue(), - controlStyle: labelPosition$.getValue(), // Rename "labelPosition" to "controlStyle" + controlStyle: labelPosition$.getValue(), showApplySelections: !autoApplySelections$.getValue(), ignoreParentSettingsJSON: JSON.stringify(ignoreParentSettings$.getValue()), panelsJSON, @@ -265,10 +255,9 @@ export const getControlGroupEmbeddableFactory = (services: { /** Fetch the allowExpensiveQuries setting for the children to use if necessary */ const fetchAllowExpensiveQueries = async () => { try { - const { allowExpensiveQueries } = await services.core.http.get<{ + const { allowExpensiveQueries } = await coreServices.http.get<{ allowExpensiveQueries: boolean; - // TODO: Rename this route as part of https://github.com/elastic/kibana/issues/174961 - }>('/internal/controls/optionsList/getExpensiveQueriesSetting', { + }>('/internal/controls/getExpensiveQueriesSetting', { version: '1', }); if (!allowExpensiveQueries) { diff --git a/src/plugins/controls/public/react_controls/control_group/open_edit_control_group_flyout.tsx b/src/plugins/controls/public/react_controls/control_group/open_edit_control_group_flyout.tsx index 5e7026282123..5e7baf1f73e5 100644 --- a/src/plugins/controls/public/react_controls/control_group/open_edit_control_group_flyout.tsx +++ b/src/plugins/controls/public/react_controls/control_group/open_edit_control_group_flyout.tsx @@ -8,7 +8,6 @@ */ import { OverlayRef } from '@kbn/core-mount-utils-browser'; -import { CoreStart } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { tracksOverlays } from '@kbn/presentation-containers'; import { apiHasParentApi } from '@kbn/presentation-publishing'; @@ -19,13 +18,11 @@ import { BehaviorSubject } from 'rxjs'; import { ControlStateManager } from '../controls/types'; import { ControlGroupEditor } from './components/control_group_editor'; import { ControlGroupApi, ControlGroupEditorState } from './types'; +import { coreServices } from '../../services/kibana_services'; export const openEditControlGroupFlyout = ( controlGroupApi: ControlGroupApi, - stateManager: ControlStateManager, - services: { - core: CoreStart; - } + stateManager: ControlStateManager ) => { /** * Duplicate all state into a new manager because we do not want to actually apply the changes @@ -50,7 +47,7 @@ export const openEditControlGroupFlyout = ( }; const onDeleteAll = (ref: OverlayRef) => { - services.core.overlays + coreServices.overlays .openConfirm( i18n.translate('controls.controlGroup.management.delete.sub', { defaultMessage: 'Controls are not recoverable once removed.', @@ -77,7 +74,7 @@ export const openEditControlGroupFlyout = ( }); }; - const overlay = services.core.overlays.openFlyout( + const overlay = coreServices.overlays.openFlyout( toMountPoint( closeOverlay(overlay)} />, { - theme: services.core.theme, - i18n: services.core.i18n, + theme: coreServices.theme, + i18n: coreServices.i18n, } ), { diff --git a/src/plugins/controls/public/react_controls/control_group/register_control_group_embeddable.ts b/src/plugins/controls/public/react_controls/control_group/register_control_group_embeddable.ts index 513633e46a87..a64faa63e8ef 100644 --- a/src/plugins/controls/public/react_controls/control_group/register_control_group_embeddable.ts +++ b/src/plugins/controls/public/react_controls/control_group/register_control_group_embeddable.ts @@ -7,23 +7,16 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { CoreSetup } from '@kbn/core/public'; import type { EmbeddableSetup } from '@kbn/embeddable-plugin/public'; -import type { ControlsPluginStartDeps } from '../../types'; import { CONTROL_GROUP_TYPE } from '../../../common'; +import { untilPluginStartServicesReady } from '../../services/kibana_services'; -export function registerControlGroupEmbeddable( - coreSetup: CoreSetup, - embeddableSetup: EmbeddableSetup -) { +export function registerControlGroupEmbeddable(embeddableSetup: EmbeddableSetup) { embeddableSetup.registerReactEmbeddableFactory(CONTROL_GROUP_TYPE, async () => { - const [{ getControlGroupEmbeddableFactory }, [coreStart, depsStart]] = await Promise.all([ + const [{ getControlGroupEmbeddableFactory }] = await Promise.all([ import('./get_control_group_factory'), - coreSetup.getStartServices(), + untilPluginStartServicesReady(), ]); - return getControlGroupEmbeddableFactory({ - core: coreStart, - dataViews: depsStart.data.dataViews, - }); + return getControlGroupEmbeddableFactory(); }); } diff --git a/src/plugins/controls/public/react_controls/control_group/types.ts b/src/plugins/controls/public/react_controls/control_group/types.ts index cb09cac975e7..37f9f40c4079 100644 --- a/src/plugins/controls/public/react_controls/control_group/types.ts +++ b/src/plugins/controls/public/react_controls/control_group/types.ts @@ -19,7 +19,6 @@ import { import { HasEditCapabilities, HasParentApi, - PublishesDataLoading, PublishesDisabledActionIds, PublishesFilters, PublishesTimeslice, @@ -35,8 +34,8 @@ import { ControlGroupEditorConfig, ControlGroupRuntimeState, ControlGroupSerializedState, + ControlLabelPosition, ControlPanelState, - ControlStyle, DefaultControlState, ParentIgnoreSettings, } from '../../../common'; @@ -54,7 +53,6 @@ export type ControlGroupApi = PresentationContainer & PublishesDataViews & HasSerializedChildState & HasEditCapabilities & - PublishesDataLoading & Pick, 'unsavedChanges'> & PublishesTimeslice & PublishesDisabledActionIds & @@ -62,7 +60,7 @@ export type ControlGroupApi = PresentationContainer & allowExpensiveQueries$: PublishingSubject; autoApplySelections$: PublishingSubject; ignoreParentSettings$: PublishingSubject; - labelPosition: PublishingSubject; + labelPosition: PublishingSubject; asyncResetUnsavedChanges: () => Promise; controlFetch$: (controlUuid: string) => Observable; diff --git a/src/plugins/controls/public/react_controls/control_group/utils/control_group_state_builder.ts b/src/plugins/controls/public/react_controls/control_group/utils/control_group_state_builder.ts index 91e5379416c5..1c051e58af46 100644 --- a/src/plugins/controls/public/react_controls/control_group/utils/control_group_state_builder.ts +++ b/src/plugins/controls/public/react_controls/control_group/utils/control_group_state_builder.ts @@ -13,11 +13,12 @@ import { OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL, TIME_SLIDER_CONTROL, + type ControlGroupRuntimeState, + type ControlPanelsState, type DefaultDataControlState, } from '../../../../common'; -import { type ControlGroupRuntimeState, type ControlPanelsState } from '../../../../common'; import type { OptionsListControlState } from '../../../../common/options_list'; -import { pluginServices } from '../../../services'; +import { dataViewsService } from '../../../services/kibana_services'; import { getDataControlFieldRegistry } from '../../controls/data_controls/data_control_editor_utils'; import type { RangesliderControlState } from '../../controls/data_controls/range_slider/types'; @@ -82,7 +83,7 @@ export const controlGroupStateBuilder = { }; async function getCompatibleControlType(dataViewId: string, fieldName: string) { - const dataView = await pluginServices.getServices().dataViews.get(dataViewId); + const dataView = await dataViewsService.get(dataViewId); const fieldRegistry = await getDataControlFieldRegistry(dataView); const field = fieldRegistry[fieldName]; if (field.compatibleControlTypes.length === 0) { diff --git a/src/plugins/controls/public/react_controls/control_group/utils/initialization_utils.ts b/src/plugins/controls/public/react_controls/control_group/utils/initialization_utils.ts index 8bd19c3d6478..ef81b4e30b36 100644 --- a/src/plugins/controls/public/react_controls/control_group/utils/initialization_utils.ts +++ b/src/plugins/controls/public/react_controls/control_group/utils/initialization_utils.ts @@ -7,11 +7,11 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { type ControlGroupRuntimeState, DEFAULT_CONTROL_STYLE } from '../../../../common'; +import { DEFAULT_CONTROL_LABEL_POSITION, type ControlGroupRuntimeState } from '../../../../common'; export const getDefaultControlGroupRuntimeState = (): ControlGroupRuntimeState => ({ initialChildControlState: {}, - labelPosition: DEFAULT_CONTROL_STYLE, + labelPosition: DEFAULT_CONTROL_LABEL_POSITION, chainingSystem: 'HIERARCHICAL', autoApplySelections: true, ignoreParentSettings: { diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/data_control_editor.test.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/data_control_editor.test.tsx index 5f8f17c57bce..8d8385d603fb 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/data_control_editor.test.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/data_control_editor.test.tsx @@ -12,7 +12,6 @@ import { BehaviorSubject } from 'rxjs'; import { createStubDataView } from '@kbn/data-views-plugin/common/data_view.stub'; import { stubFieldSpecMap } from '@kbn/data-views-plugin/common/field.stub'; -import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { TimeRange } from '@kbn/es-query'; import { I18nProvider } from '@kbn/i18n-react'; import { act, fireEvent, render, RenderResult, waitFor } from '@testing-library/react'; @@ -22,6 +21,7 @@ import { DEFAULT_CONTROL_WIDTH, type DefaultDataControlState, } from '../../../../common'; +import { dataViewsService } from '../../../services/kibana_services'; import { getAllControlTypes, getControlFactory } from '../../control_factory_registry'; import type { ControlGroupApi } from '../../control_group/types'; import type { ControlFactory } from '../types'; @@ -39,7 +39,6 @@ jest.mock('../../control_factory_registry', () => ({ getControlFactory: jest.fn(), })); -const mockDataViews = dataViewPluginMocks.createStartContract(); const mockDataView = createStubDataView({ spec: { id: 'logstash-*', @@ -58,7 +57,6 @@ const mockDataView = createStubDataView({ timeFieldName: '@timestamp', }, }); -mockDataViews.get = jest.fn().mockResolvedValue(mockDataView); const dashboardApi = { timeRange$: new BehaviorSubject(undefined), @@ -82,7 +80,7 @@ describe('Data control editor', () => { controlType?: string; initialDefaultPanelTitle?: string; }) => { - mockDataViews.get = jest.fn().mockResolvedValue(mockDataView); + dataViewsService.get = jest.fn().mockResolvedValue(mockDataView); const controlEditor = render( @@ -97,13 +95,12 @@ describe('Data control editor', () => { controlId={controlId} controlType={controlType} initialDefaultPanelTitle={initialDefaultPanelTitle} - services={{ dataViews: mockDataViews }} /> ); await waitFor(() => { - expect(mockDataViews.get).toHaveBeenCalledTimes(1); + expect(dataViewsService.get).toHaveBeenCalledTimes(1); }); return controlEditor; diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/data_control_editor.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/data_control_editor.tsx index 5254fe200e97..35e21ca3b407 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/data_control_editor.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/data_control_editor.tsx @@ -33,7 +33,6 @@ import { EuiToolTip, } from '@elastic/eui'; import { DataViewField } from '@kbn/data-views-plugin/common'; -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { LazyDataViewPicker, LazyFieldPicker, @@ -46,6 +45,7 @@ import { type ControlWidth, type DefaultDataControlState, } from '../../../../common'; +import { dataViewsService } from '../../../services/kibana_services'; import { getAllControlTypes, getControlFactory } from '../../control_factory_registry'; import type { ControlGroupApi } from '../../control_group/types'; import { DataControlEditorStrings } from './data_control_constants'; @@ -67,9 +67,6 @@ export interface ControlEditorProps< controlGroupApi: ControlGroupApi; // controls must always have a parent API onCancel: (newState: Partial) => void; onSave: (newState: Partial, type: string) => void; - services: { - dataViews: DataViewsPublicPluginStart; - }; } const FieldPicker = withSuspense(LazyFieldPicker, null); @@ -151,8 +148,6 @@ export const DataControlEditor = ) => { const [editorState, setEditorState] = useState>(initialState); const [defaultPanelTitle, setDefaultPanelTitle] = useState( @@ -163,16 +158,14 @@ export const DataControlEditor = (true); const editorConfig = useMemo(() => controlGroupApi.getEditorConfig(), [controlGroupApi]); - // TODO: Maybe remove `useAsync` - see https://github.com/elastic/kibana/pull/182842#discussion_r1624909709 const { loading: dataViewListLoading, value: dataViewListItems = [], error: dataViewListError, } = useAsync(async () => { - return dataViewService.getIdsWithTitle(); + return dataViewsService.getIdsWithTitle(); }); - // TODO: Maybe remove `useAsync` - see https://github.com/elastic/kibana/pull/182842#discussion_r1624909709 const { loading: dataViewLoading, value: { selectedDataView, fieldRegistry } = { @@ -185,7 +178,7 @@ export const DataControlEditor = { return await loadFieldRegistryFromDataView(dataView); @@ -21,7 +20,6 @@ export const getDataControlFieldRegistry = memoize( (dataView: DataView) => [dataView.id, JSON.stringify(dataView.fields.getAll())].join('|') ); -/** TODO: This function is duplicated from the controls plugin to avoid exporting it */ const loadFieldRegistryFromDataView = async ( dataView: DataView ): Promise => { diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/initialize_data_control.test.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/initialize_data_control.test.tsx index 8ff50a2bc4ad..d189d0aaa1ae 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/initialize_data_control.test.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/initialize_data_control.test.tsx @@ -7,10 +7,9 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { coreMock } from '@kbn/core/public/mocks'; -import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import type { DataView } from '@kbn/data-views-plugin/public'; import { first, skip } from 'rxjs'; +import { dataViewsService } from '../../../services/kibana_services'; import { ControlGroupApi } from '../../control_group/types'; import { initializeDataControl } from './initialize_data_control'; @@ -21,9 +20,8 @@ describe('initializeDataControl', () => { }; const editorStateManager = {}; const controlGroupApi = {} as unknown as ControlGroupApi; - const mockDataViews = dataViewPluginMocks.createStartContract(); - // @ts-ignore - mockDataViews.get = async (id: string): Promise => { + + dataViewsService.get = async (id: string): Promise => { if (id !== 'myDataViewId') { throw new Error(`Simulated error: no data view found for id ${id}`); } @@ -40,10 +38,6 @@ describe('initializeDataControl', () => { }, } as unknown as DataView; }; - const services = { - core: coreMock.createStart(), - dataViews: mockDataViews, - }; describe('dataViewId subscription', () => { describe('no blocking errors', () => { @@ -55,8 +49,7 @@ describe('initializeDataControl', () => { 'referenceNameSuffix', dataControlState, editorStateManager, - controlGroupApi, - services + controlGroupApi ); dataControl.api.defaultPanelTitle!.pipe(skip(1), first()).subscribe(() => { @@ -90,8 +83,7 @@ describe('initializeDataControl', () => { dataViewId: 'notGonnaFindMeDataViewId', }, editorStateManager, - controlGroupApi, - services + controlGroupApi ); dataControl.api.dataViews.pipe(skip(1), first()).subscribe(() => { @@ -129,8 +121,7 @@ describe('initializeDataControl', () => { fieldName: 'notGonnaFindMeFieldName', }, editorStateManager, - controlGroupApi, - services + controlGroupApi ); dataControl.api.defaultPanelTitle!.pipe(skip(1), first()).subscribe(() => { diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/initialize_data_control.ts b/src/plugins/controls/public/react_controls/controls/data_controls/initialize_data_control.ts index 6b814efa1ec3..11fb453d5635 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/initialize_data_control.ts +++ b/src/plugins/controls/public/react_controls/controls/data_controls/initialize_data_control.ts @@ -10,19 +10,18 @@ import { isEqual } from 'lodash'; import { BehaviorSubject, combineLatest, debounceTime, first, skip, switchMap, tap } from 'rxjs'; -import { CoreStart } from '@kbn/core-lifecycle-browser'; import { DATA_VIEW_SAVED_OBJECT_TYPE, DataView, DataViewField, } from '@kbn/data-views-plugin/common'; -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { Filter } from '@kbn/es-query'; import { SerializedPanelState } from '@kbn/presentation-containers'; import { StateComparators } from '@kbn/presentation-publishing'; import { i18n } from '@kbn/i18n'; import type { DefaultControlState, DefaultDataControlState } from '../../../../common'; +import { dataViewsService } from '../../../services/kibana_services'; import type { ControlGroupApi } from '../../control_group/types'; import { initializeDefaultControlApi } from '../initialize_default_control_api'; import type { ControlApiInitialization, ControlStateManager } from '../types'; @@ -40,11 +39,7 @@ export const initializeDataControl = ( * responsible for managing */ editorStateManager: ControlStateManager, - controlGroupApi: ControlGroupApi, - services: { - core: CoreStart; - dataViews: DataViewsPublicPluginStart; - } + controlGroupApi: ControlGroupApi ): { api: ControlApiInitialization; cleanup: () => void; @@ -88,7 +83,7 @@ export const initializeDataControl = ( switchMap(async (currentDataViewId) => { let dataView: DataView | undefined; try { - dataView = await services.dataViews.get(currentDataViewId); + dataView = await dataViewsService.get(currentDataViewId); return { dataView }; } catch (error) { return { error }; @@ -156,7 +151,6 @@ export const initializeDataControl = ( // open the editor to get the new state openDataControlEditor({ - services, onSave: ({ type: newType, state: newState }) => { if (newType === controlType) { // apply the changes from the new state via the state manager diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/open_data_control_editor.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/open_data_control_editor.tsx index fe629555dea3..08118702a003 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/open_data_control_editor.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/open_data_control_editor.tsx @@ -10,14 +10,14 @@ import React from 'react'; import deepEqual from 'react-fast-compare'; -import { CoreStart, OverlayRef } from '@kbn/core/public'; -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import { OverlayRef } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { tracksOverlays } from '@kbn/presentation-containers'; import { apiHasParentApi } from '@kbn/presentation-publishing'; import { toMountPoint } from '@kbn/react-kibana-mount'; import type { DefaultDataControlState } from '../../../../common'; +import { coreServices } from '../../../services/kibana_services'; import type { ControlGroupApi } from '../../control_group/types'; import { DataControlEditor } from './data_control_editor'; @@ -30,7 +30,6 @@ export const openDataControlEditor = < initialDefaultPanelTitle, onSave, controlGroupApi, - services, }: { initialState: Partial; controlType?: string; @@ -38,10 +37,6 @@ export const openDataControlEditor = < initialDefaultPanelTitle?: string; onSave: ({ type, state }: { type: string; state: Partial }) => void; controlGroupApi: ControlGroupApi; - services: { - core: CoreStart; - dataViews: DataViewsPublicPluginStart; - }; }): void => { const closeOverlay = (overlayRef: OverlayRef) => { if (apiHasParentApi(controlGroupApi) && tracksOverlays(controlGroupApi.parentApi)) { @@ -55,7 +50,7 @@ export const openDataControlEditor = < closeOverlay(overlay); return; } - services.core.overlays + coreServices.overlays .openConfirm( i18n.translate('controls.controlGroup.management.discard.sub', { defaultMessage: `Changes that you've made to this control will be discarded, are you sure you want to continue?`, @@ -80,7 +75,7 @@ export const openDataControlEditor = < }); }; - const overlay = services.core.overlays.openFlyout( + const overlay = coreServices.overlays.openFlyout( toMountPoint( controlGroupApi={controlGroupApi} @@ -95,11 +90,10 @@ export const openDataControlEditor = < closeOverlay(overlay); onSave({ type: selectedControlType, state }); }} - services={{ dataViews: services.dataViews }} />, { - theme: services.core.theme, - i18n: services.core.i18n, + theme: coreServices.theme, + i18n: coreServices.i18n, } ), { diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/fetch_and_validate.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/fetch_and_validate.tsx index c2b1d7d84250..2e2cd341e870 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/fetch_and_validate.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/fetch_and_validate.tsx @@ -26,13 +26,11 @@ import { isValidSearch } from '../../../../../common/options_list/is_valid_searc import { OptionsListSelection } from '../../../../../common/options_list/options_list_selections'; import { ControlFetchContext } from '../../../control_group/control_fetch'; import { ControlStateManager } from '../../types'; -import { DataControlServices } from '../types'; import { OptionsListFetchCache } from './options_list_fetch_cache'; import { OptionsListComponentApi, OptionsListComponentState, OptionsListControlApi } from './types'; export function fetchAndValidate$({ api, - services, stateManager, }: { api: Pick & @@ -41,7 +39,6 @@ export function fetchAndValidate$({ loadingSuggestions$: BehaviorSubject; debouncedSearchString: Observable; }; - services: DataControlServices; stateManager: ControlStateManager< Pick > & { @@ -126,7 +123,7 @@ export function fetchAndValidate$({ const newAbortController = new AbortController(); abortController = newAbortController; try { - return await requestCache.runFetchRequest(request, newAbortController.signal, services); + return await requestCache.runFetchRequest(request, newAbortController.signal); } catch (error) { return { error }; } diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/get_options_list_control_factory.test.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/get_options_list_control_factory.test.tsx index 99e15a3d0f31..20911d1cdb87 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/get_options_list_control_factory.test.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/get_options_list_control_factory.test.tsx @@ -9,26 +9,22 @@ import React from 'react'; -import { coreMock } from '@kbn/core/public/mocks'; -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { DataView } from '@kbn/data-views-plugin/common'; import { createStubDataView } from '@kbn/data-views-plugin/common/data_view.stub'; -import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { act, render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { coreServices, dataViewsService } from '../../../../services/kibana_services'; import { getMockedBuildApi, getMockedControlGroupApi } from '../../mocks/control_mocks'; import { getOptionsListControlFactory } from './get_options_list_control_factory'; describe('Options List Control Api', () => { const uuid = 'myControl1'; const controlGroupApi = getMockedControlGroupApi(); - const mockDataViews = dataViewPluginMocks.createStartContract(); - const mockCore = coreMock.createStart(); const waitOneTick = () => act(() => new Promise((resolve) => setTimeout(resolve, 0))); - mockDataViews.get = jest.fn().mockImplementation(async (id: string): Promise => { + dataViewsService.get = jest.fn().mockImplementation(async (id: string): Promise => { if (id !== 'myDataViewId') { throw new Error(`Simulated error: no data view found for id ${id}`); } @@ -60,11 +56,7 @@ describe('Options List Control Api', () => { return stubDataView; }); - const factory = getOptionsListControlFactory({ - core: mockCore, - data: dataPluginMock.createStartContract(), - dataViews: mockDataViews, - }); + const factory = getOptionsListControlFactory(); describe('filters$', () => { test('should not set filters$ when selectedOptions is not provided', async () => { @@ -177,7 +169,7 @@ describe('Options List Control Api', () => { describe('make selection', () => { beforeAll(() => { - mockCore.http.fetch = jest.fn().mockResolvedValue({ + coreServices.http.fetch = jest.fn().mockResolvedValue({ suggestions: [ { value: 'woof', docCount: 10 }, { value: 'bark', docCount: 15 }, diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/get_options_list_control_factory.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/get_options_list_control_factory.tsx index d0c40736552c..2a23ac9341ab 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/get_options_list_control_factory.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/get_options_list_control_factory.tsx @@ -9,7 +9,7 @@ import fastIsEqual from 'fast-deep-equal'; import React, { useEffect } from 'react'; -import { BehaviorSubject, combineLatest, debounceTime, filter, skip } from 'rxjs'; +import { BehaviorSubject, combineLatest, debounceTime, filter, map, skip } from 'rxjs'; import { buildExistsFilter, buildPhraseFilter, buildPhrasesFilter, Filter } from '@kbn/es-query'; import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; @@ -25,7 +25,7 @@ import type { } from '../../../../../common/options_list'; import { getSelectionAsFieldType, isValidSearch } from '../../../../../common/options_list'; import { initializeDataControl } from '../initialize_data_control'; -import type { DataControlFactory, DataControlServices } from '../types'; +import type { DataControlFactory } from '../types'; import { OptionsListControl } from './components/options_list_control'; import { OptionsListEditorOptions } from './components/options_list_editor_options'; import { @@ -39,9 +39,10 @@ import { initializeOptionsListSelections } from './options_list_control_selectio import { OptionsListStrings } from './options_list_strings'; import type { OptionsListControlApi } from './types'; -export const getOptionsListControlFactory = ( - services: DataControlServices -): DataControlFactory => { +export const getOptionsListControlFactory = (): DataControlFactory< + OptionsListControlState, + OptionsListControlApi +> => { return { type: OPTIONS_LIST_CONTROL, order: 3, // should always be first, since this is the most popular control @@ -78,6 +79,7 @@ export const getOptionsListControlFactory = ( const searchStringValid$ = new BehaviorSubject(true); const requestSize$ = new BehaviorSubject(MIN_OPTIONS_LIST_REQUEST_SIZE); + const dataLoading$ = new BehaviorSubject(undefined); const availableOptions$ = new BehaviorSubject(undefined); const invalidSelections$ = new BehaviorSubject>(new Set()); const totalCardinality$ = new BehaviorSubject(0); @@ -90,8 +92,7 @@ export const getOptionsListControlFactory = ( 'optionsListDataView', initialState, { searchTechnique: searchTechnique$, singleSelect: singleSelect$ }, - controlGroupApi, - services + controlGroupApi ); const selections = initializeOptionsListSelections( @@ -115,12 +116,16 @@ export const getOptionsListControlFactory = ( /** Handle loading state; since suggestion fetching and validation are tied, only need one loading subject */ const loadingSuggestions$ = new BehaviorSubject(false); - const dataLoadingSubscription = loadingSuggestions$ + const dataLoadingSubscription = combineLatest([ + loadingSuggestions$, + dataControl.api.dataLoading, + ]) .pipe( - debounceTime(100) // debounce set loading so that it doesn't flash as the user types + debounceTime(100), // debounce set loading so that it doesn't flash as the user types + map((values) => values.some((value) => value)) ) .subscribe((isLoading) => { - dataControl.api.setDataLoading(isLoading); + dataLoading$.next(isLoading); }); /** Debounce the search string changes to reduce the number of fetch requests */ @@ -161,7 +166,6 @@ export const getOptionsListControlFactory = ( /** Fetch the suggestions and perform validation */ const loadMoreSubject = new BehaviorSubject(null); const fetchSubscription = fetchAndValidate$({ - services, api: { ...dataControl.api, loadMoreSubject, @@ -235,6 +239,7 @@ export const getOptionsListControlFactory = ( const api = buildApi( { ...dataControl.api, + dataLoading: dataLoading$, getTypeDisplayName: OptionsListStrings.control.getDisplayName, serializeState: () => { const { rawState: dataControlState, references } = dataControl.serialize(); diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/options_list_fetch_cache.ts b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/options_list_fetch_cache.ts index 548b1efebd02..60b146311873 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/options_list_fetch_cache.ts +++ b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/options_list_fetch_cache.ts @@ -20,7 +20,7 @@ import type { OptionsListResponse, OptionsListSuccessResponse, } from '../../../../../common/options_list/types'; -import type { DataControlServices } from '../types'; +import { coreServices, dataService } from '../../../../services/kibana_services'; const REQUEST_CACHE_SIZE = 50; // only store a max of 50 responses const REQUEST_CACHE_TTL = 1000 * 60; // time to live = 1 minute @@ -80,8 +80,7 @@ export class OptionsListFetchCache { public async runFetchRequest( request: OptionsListRequest, - abortSignal: AbortSignal, - services: DataControlServices + abortSignal: AbortSignal ): Promise { const requestHash = this.getRequestHash(request); @@ -90,11 +89,11 @@ export class OptionsListFetchCache { } else { const index = request.dataView.getIndexPattern(); - const timeService = services.data.query.timefilter.timefilter; + const timeService = dataService.query.timefilter.timefilter; const { query, filters, dataView, timeRange, field, ...passThroughProps } = request; const timeFilter = timeRange ? timeService.createFilter(dataView, timeRange) : undefined; const filtersToUse = [...(filters ?? []), ...(timeFilter ? [timeFilter] : [])]; - const config = getEsQueryConfig(services.core.uiSettings); + const config = getEsQueryConfig(coreServices.uiSettings); const esFilters = [buildEsQuery(dataView, query ?? [], filtersToUse ?? [], config)]; const requestBody = { @@ -105,7 +104,7 @@ export class OptionsListFetchCache { runtimeFieldMap: dataView.toSpec?.().runtimeFieldMap, }; - const result = await services.core.http.fetch( + const result = await coreServices.http.fetch( `/internal/controls/optionsList/${index}`, { version: '1', diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/register_options_list_control.ts b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/register_options_list_control.ts index 417eb42d4b1b..b58189a75dac 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/register_options_list_control.ts +++ b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/register_options_list_control.ts @@ -7,21 +7,16 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { CoreSetup } from '@kbn/core/public'; -import type { ControlsPluginStartDeps } from '../../../../types'; -import { registerControlFactory } from '../../../control_factory_registry'; import { OPTIONS_LIST_CONTROL } from '../../../../../common'; +import { untilPluginStartServicesReady } from '../../../../services/kibana_services'; +import { registerControlFactory } from '../../../control_factory_registry'; -export function registerOptionsListControl(coreSetup: CoreSetup) { +export function registerOptionsListControl() { registerControlFactory(OPTIONS_LIST_CONTROL, async () => { - const [{ getOptionsListControlFactory }, [coreStart, depsStart]] = await Promise.all([ + const [{ getOptionsListControlFactory }] = await Promise.all([ import('./get_options_list_control_factory'), - coreSetup.getStartServices(), + untilPluginStartServicesReady(), ]); - return getOptionsListControlFactory({ - core: coreStart, - data: depsStart.data, - dataViews: depsStart.data.dataViews, - }); + return getOptionsListControlFactory(); }); } diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/get_range_slider_control_factory.test.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/get_range_slider_control_factory.test.tsx index 76cb52981e8c..925ec3443849 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/get_range_slider_control_factory.test.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/get_range_slider_control_factory.test.tsx @@ -11,13 +11,11 @@ import React from 'react'; import { of } from 'rxjs'; import { estypes } from '@elastic/elasticsearch'; -import { coreMock } from '@kbn/core/public/mocks'; -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { DataViewField } from '@kbn/data-views-plugin/common'; -import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { SerializedPanelState } from '@kbn/presentation-containers'; import { fireEvent, render, waitFor } from '@testing-library/react'; +import { dataService, dataViewsService } from '../../../../services/kibana_services'; import { getMockedBuildApi, getMockedControlGroupApi } from '../../mocks/control_mocks'; import { getRangesliderControlFactory } from './get_range_slider_control_factory'; import { RangesliderControlState } from './types'; @@ -31,11 +29,10 @@ describe('RangesliderControlApi', () => { const controlGroupApi = getMockedControlGroupApi(); - const dataStartServiceMock = dataPluginMock.createStartContract(); let totalResults = DEFAULT_TOTAL_RESULTS; let min: estypes.AggregationsSingleMetricAggregateBase['value'] = DEFAULT_MIN; let max: estypes.AggregationsSingleMetricAggregateBase['value'] = DEFAULT_MAX; - dataStartServiceMock.search.searchSource.create = jest.fn().mockImplementation(() => { + dataService.search.searchSource.create = jest.fn().mockImplementation(() => { let isAggsRequest = false; return { setField: (key: string) => { @@ -54,9 +51,8 @@ describe('RangesliderControlApi', () => { }, }; }); - const mockDataViews = dataViewPluginMocks.createStartContract(); - mockDataViews.get = jest.fn().mockImplementation(async (id: string): Promise => { + dataViewsService.get = jest.fn().mockImplementation(async (id: string): Promise => { if (id !== 'myDataViewId') { throw new Error(`no data view found for id ${id}`); } @@ -82,11 +78,7 @@ describe('RangesliderControlApi', () => { } as unknown as DataView; }); - const factory = getRangesliderControlFactory({ - core: coreMock.createStart(), - data: dataStartServiceMock, - dataViews: mockDataViews, - }); + const factory = getRangesliderControlFactory(); beforeEach(() => { totalResults = DEFAULT_TOTAL_RESULTS; diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/get_range_slider_control_factory.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/get_range_slider_control_factory.tsx index 596206dc2f4f..3ad3b97af741 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/get_range_slider_control_factory.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/get_range_slider_control_factory.tsx @@ -16,7 +16,7 @@ import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; import { RANGE_SLIDER_CONTROL } from '../../../../../common'; import { initializeDataControl } from '../initialize_data_control'; -import type { DataControlFactory, DataControlServices } from '../types'; +import type { DataControlFactory } from '../types'; import { RangeSliderControl } from './components/range_slider_control'; import { hasNoResults$ } from './has_no_results'; import { minMax$ } from './min_max'; @@ -24,9 +24,10 @@ import { initializeRangeControlSelections } from './range_control_selections'; import { RangeSliderStrings } from './range_slider_strings'; import type { RangesliderControlApi, RangesliderControlState } from './types'; -export const getRangesliderControlFactory = ( - services: DataControlServices -): DataControlFactory => { +export const getRangesliderControlFactory = (): DataControlFactory< + RangesliderControlState, + RangesliderControlApi +> => { return { type: RANGE_SLIDER_CONTROL, getIconType: () => 'controlsHorizontal', @@ -71,8 +72,7 @@ export const getRangesliderControlFactory = ( { step: step$, }, - controlGroupApi, - services + controlGroupApi ); const selections = initializeRangeControlSelections( @@ -111,13 +111,14 @@ export const getRangesliderControlFactory = ( } ); - const dataLoadingSubscription = combineLatest([loadingMinMax$, loadingHasNoResults$]) + const dataLoadingSubscription = combineLatest([ + loadingMinMax$, + loadingHasNoResults$, + dataControl.api.dataLoading, + ]) .pipe( - map((values) => { - return values.some((value) => { - return value; - }); - }) + debounceTime(100), + map((values) => values.some((value) => value)) ) .subscribe((isLoading) => { dataLoading$.next(isLoading); @@ -138,7 +139,6 @@ export const getRangesliderControlFactory = ( const min$ = new BehaviorSubject(undefined); const minMaxSubscription = minMax$({ controlFetch$, - data: services.data, dataViews$: dataControl.api.dataViews, fieldName$: dataControl.stateManager.fieldName, setIsLoading: (isLoading: boolean) => { @@ -198,7 +198,6 @@ export const getRangesliderControlFactory = ( const selectionHasNoResults$ = new BehaviorSubject(false); const hasNotResultsSubscription = hasNoResults$({ controlFetch$, - data: services.data, dataViews$: dataControl.api.dataViews, rangeFilters$: dataControl.api.filters$, ignoreParentSettings$: controlGroupApi.ignoreParentSettings$, diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/has_no_results.ts b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/has_no_results.ts index 27676f5f7b64..24d4510b3fc2 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/has_no_results.ts +++ b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/has_no_results.ts @@ -8,25 +8,23 @@ */ import { estypes } from '@elastic/elasticsearch'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { DataView } from '@kbn/data-views-plugin/public'; import { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; import { PublishesDataViews } from '@kbn/presentation-publishing'; -import { combineLatest, lastValueFrom, Observable, switchMap, tap } from 'rxjs'; +import { Observable, combineLatest, lastValueFrom, switchMap, tap } from 'rxjs'; +import { dataService } from '../../../../services/kibana_services'; import { ControlFetchContext } from '../../../control_group/control_fetch'; import { ControlGroupApi } from '../../../control_group/types'; import { DataControlApi } from '../types'; export function hasNoResults$({ controlFetch$, - data, dataViews$, rangeFilters$, ignoreParentSettings$, setIsLoading, }: { controlFetch$: Observable; - data: DataPublicPluginStart; dataViews$?: PublishesDataViews['dataViews']; rangeFilters$: DataControlApi['filters$']; ignoreParentSettings$: ControlGroupApi['ignoreParentSettings$']; @@ -53,7 +51,6 @@ export function hasNoResults$({ prevRequestAbortController = abortController; return await hasNoResults({ abortSignal: abortController.signal, - data, dataView, rangeFilter, ...controlFetchContext, @@ -71,7 +68,6 @@ export function hasNoResults$({ async function hasNoResults({ abortSignal, - data, dataView, filters, query, @@ -79,14 +75,13 @@ async function hasNoResults({ timeRange, }: { abortSignal: AbortSignal; - data: DataPublicPluginStart; dataView: DataView; filters?: Filter[]; query?: Query | AggregateQuery; rangeFilter: Filter; timeRange?: TimeRange; }): Promise { - const searchSource = await data.search.searchSource.create(); + const searchSource = await dataService.search.searchSource.create(); searchSource.setField('size', 0); searchSource.setField('index', dataView); // Tracking total hits accurately has a performance cost @@ -97,7 +92,7 @@ async function hasNoResults({ const allFilters = filters ? [...filters] : []; allFilters.push(rangeFilter); if (timeRange) { - const timeFilter = data.query.timefilter.timefilter.createFilter(dataView, timeRange); + const timeFilter = dataService.query.timefilter.timefilter.createFilter(dataView, timeRange); if (timeFilter) allFilters.push(timeFilter); } if (allFilters.length) { diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/min_max.ts b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/min_max.ts index d3335e182f10..8e4d5e00374a 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/min_max.ts +++ b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/min_max.ts @@ -8,26 +8,24 @@ */ import { estypes } from '@elastic/elasticsearch'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { DataView, DataViewField } from '@kbn/data-views-plugin/public'; import { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; import { PublishesDataViews, PublishingSubject } from '@kbn/presentation-publishing'; -import { combineLatest, lastValueFrom, Observable, of, startWith, switchMap, tap } from 'rxjs'; import { apiPublishesReload } from '@kbn/presentation-publishing/interfaces/fetch/publishes_reload'; +import { Observable, combineLatest, lastValueFrom, of, startWith, switchMap, tap } from 'rxjs'; +import { dataService } from '../../../../services/kibana_services'; import { ControlFetchContext } from '../../../control_group/control_fetch'; import { ControlGroupApi } from '../../../control_group/types'; export function minMax$({ controlFetch$, controlGroupApi, - data, dataViews$, fieldName$, setIsLoading, }: { controlFetch$: Observable; controlGroupApi: ControlGroupApi; - data: DataPublicPluginStart; dataViews$: PublishesDataViews['dataViews']; fieldName$: PublishingSubject; setIsLoading: (isLoading: boolean) => void; @@ -60,7 +58,6 @@ export function minMax$({ prevRequestAbortController = abortController; return await getMinMax({ abortSignal: abortController.signal, - data, dataView, field: dataViewField, ...controlFetchContext, @@ -77,7 +74,6 @@ export function minMax$({ export async function getMinMax({ abortSignal, - data, dataView, field, filters, @@ -85,20 +81,19 @@ export async function getMinMax({ timeRange, }: { abortSignal: AbortSignal; - data: DataPublicPluginStart; dataView: DataView; field: DataViewField; filters?: Filter[]; query?: Query | AggregateQuery; timeRange?: TimeRange; }): Promise<{ min: number | undefined; max: number | undefined }> { - const searchSource = await data.search.searchSource.create(); + const searchSource = await dataService.search.searchSource.create(); searchSource.setField('size', 0); searchSource.setField('index', dataView); const allFilters = filters ? [...filters] : []; if (timeRange) { - const timeFilter = data.query.timefilter.timefilter.createFilter(dataView, timeRange); + const timeFilter = dataService.query.timefilter.timefilter.createFilter(dataView, timeRange); if (timeFilter) allFilters.push(timeFilter); } if (allFilters.length) { diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/register_range_slider_control.ts b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/register_range_slider_control.ts index 4f77fc3bac7e..0e1c0fd92579 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/register_range_slider_control.ts +++ b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/register_range_slider_control.ts @@ -7,22 +7,17 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { CoreSetup } from '@kbn/core/public'; -import type { ControlsPluginStartDeps } from '../../../../types'; -import { registerControlFactory } from '../../../control_factory_registry'; import { RANGE_SLIDER_CONTROL } from '../../../../../common'; +import { untilPluginStartServicesReady } from '../../../../services/kibana_services'; +import { registerControlFactory } from '../../../control_factory_registry'; -export function registerRangeSliderControl(coreSetup: CoreSetup) { +export function registerRangeSliderControl() { registerControlFactory(RANGE_SLIDER_CONTROL, async () => { - const [{ getRangesliderControlFactory }, [coreStart, depsStart]] = await Promise.all([ + const [{ getRangesliderControlFactory }] = await Promise.all([ import('./get_range_slider_control_factory'), - coreSetup.getStartServices(), + untilPluginStartServicesReady(), ]); - return getRangesliderControlFactory({ - core: coreStart, - data: depsStart.data, - dataViews: depsStart.data.dataViews, - }); + return getRangesliderControlFactory(); }); } diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/types.ts b/src/plugins/controls/public/react_controls/controls/data_controls/types.ts index 9eac14164240..89912e6eabb0 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/types.ts +++ b/src/plugins/controls/public/react_controls/controls/data_controls/types.ts @@ -7,10 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { CoreStart } from '@kbn/core/public'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { DataViewField } from '@kbn/data-views-plugin/common'; -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { FieldFormatConvertFunction } from '@kbn/field-formats-plugin/common'; import { HasEditCapabilities, @@ -62,12 +59,6 @@ export const isDataControlFactory = ( return typeof (factory as DataControlFactory).isFieldCompatible === 'function'; }; -export interface DataControlServices { - core: CoreStart; - data: DataPublicPluginStart; - dataViews: DataViewsPublicPluginStart; -} - interface DataControlField { field: DataViewField; compatibleControlTypes: string[]; diff --git a/src/plugins/controls/public/react_controls/controls/timeslider_control/get_time_range_meta.ts b/src/plugins/controls/public/react_controls/controls/timeslider_control/get_time_range_meta.ts index 1cccb264d19e..5c84cfbdef50 100644 --- a/src/plugins/controls/public/react_controls/controls/timeslider_control/get_time_range_meta.ts +++ b/src/plugins/controls/public/react_controls/controls/timeslider_control/get_time_range_meta.ts @@ -9,6 +9,7 @@ import { EuiRangeTick } from '@elastic/eui'; import { TimeRange } from '@kbn/es-query'; +import { coreServices, dataService } from '../../../services/kibana_services'; import { FROM_INDEX, getStepSize, @@ -17,7 +18,6 @@ import { roundUpToNextStepSizeFactor, TO_INDEX, } from './time_utils'; -import { Services } from './types'; export interface TimeRangeMeta { format: string; @@ -29,12 +29,9 @@ export interface TimeRangeMeta { timeRangeMin: number; } -export function getTimeRangeMeta( - timeRange: TimeRange | undefined, - services: Services -): TimeRangeMeta { - const nextBounds = timeRangeToBounds(timeRange ?? getDefaultTimeRange(services), services); - const ticks = getTicks(nextBounds[FROM_INDEX], nextBounds[TO_INDEX], getTimezone(services)); +export function getTimeRangeMeta(timeRange: TimeRange | undefined): TimeRangeMeta { + const nextBounds = timeRangeToBounds(timeRange ?? getDefaultTimeRange()); + const ticks = getTicks(nextBounds[FROM_INDEX], nextBounds[TO_INDEX], getTimezone()); const { format, stepSize } = getStepSize(ticks); return { format, @@ -47,17 +44,17 @@ export function getTimeRangeMeta( }; } -export function getTimezone(services: Services) { - return services.core.uiSettings.get('dateFormat:tz', 'Browser'); +export function getTimezone() { + return coreServices.uiSettings.get('dateFormat:tz', 'Browser'); } -function getDefaultTimeRange(services: Services) { - const defaultTimeRange = services.core.uiSettings.get('timepicker:timeDefaults'); +function getDefaultTimeRange() { + const defaultTimeRange = coreServices.uiSettings.get('timepicker:timeDefaults'); return defaultTimeRange ? defaultTimeRange : { from: 'now-15m', to: 'now' }; } -function timeRangeToBounds(timeRange: TimeRange, services: Services): [number, number] { - const timeRangeBounds = services.data.query.timefilter.timefilter.calculateBounds(timeRange); +function timeRangeToBounds(timeRange: TimeRange): [number, number] { + const timeRangeBounds = dataService.query.timefilter.timefilter.calculateBounds(timeRange); return timeRangeBounds.min === undefined || timeRangeBounds.max === undefined ? [Date.now() - 1000 * 60 * 15, Date.now()] : [timeRangeBounds.min.valueOf(), timeRangeBounds.max.valueOf()]; diff --git a/src/plugins/controls/public/react_controls/controls/timeslider_control/get_timeslider_control_factory.test.tsx b/src/plugins/controls/public/react_controls/controls/timeslider_control/get_timeslider_control_factory.test.tsx index 12381ad83c40..d4b8ff6c1346 100644 --- a/src/plugins/controls/public/react_controls/controls/timeslider_control/get_timeslider_control_factory.test.tsx +++ b/src/plugins/controls/public/react_controls/controls/timeslider_control/get_timeslider_control_factory.test.tsx @@ -7,14 +7,15 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { coreMock } from '@kbn/core/public/mocks'; -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import React from 'react'; +import { BehaviorSubject } from 'rxjs'; + import dateMath from '@kbn/datemath'; import { TimeRange } from '@kbn/es-query'; import { StateComparators } from '@kbn/presentation-publishing'; import { fireEvent, render } from '@testing-library/react'; -import React from 'react'; -import { BehaviorSubject } from 'rxjs'; + +import { dataService } from '../../../services/kibana_services'; import { getMockedControlGroupApi } from '../mocks/control_mocks'; import { ControlApiRegistration } from '../types'; import { getTimesliderControlFactory } from './get_timeslider_control_factory'; @@ -28,18 +29,14 @@ describe('TimesliderControlApi', () => { }; const controlGroupApi = getMockedControlGroupApi(dashboardApi); - const dataStartServiceMock = dataPluginMock.createStartContract(); - dataStartServiceMock.query.timefilter.timefilter.calculateBounds = (timeRange: TimeRange) => { + dataService.query.timefilter.timefilter.calculateBounds = (timeRange: TimeRange) => { const now = new Date(); return { min: dateMath.parse(timeRange.from, { forceNow: now }), max: dateMath.parse(timeRange.to, { roundUp: true, forceNow: now }), }; }; - const factory = getTimesliderControlFactory({ - core: coreMock.createStart(), - data: dataStartServiceMock, - }); + const factory = getTimesliderControlFactory(); let comparators: StateComparators | undefined; function buildApiMock( api: ControlApiRegistration, diff --git a/src/plugins/controls/public/react_controls/controls/timeslider_control/get_timeslider_control_factory.tsx b/src/plugins/controls/public/react_controls/controls/timeslider_control/get_timeslider_control_factory.tsx index 20baf8fb545e..cfc8e50bee1b 100644 --- a/src/plugins/controls/public/react_controls/controls/timeslider_control/get_timeslider_control_factory.tsx +++ b/src/plugins/controls/public/react_controls/controls/timeslider_control/get_timeslider_control_factory.tsx @@ -36,22 +36,23 @@ import { roundDownToNextStepSizeFactor, roundUpToNextStepSizeFactor, } from './time_utils'; -import { Services, Timeslice, TimesliderControlApi, TimesliderControlState } from './types'; +import { Timeslice, TimesliderControlApi, TimesliderControlState } from './types'; const displayName = i18n.translate('controls.timesliderControl.displayName', { defaultMessage: 'Time slider', }); -export const getTimesliderControlFactory = ( - services: Services -): ControlFactory => { +export const getTimesliderControlFactory = (): ControlFactory< + TimesliderControlState, + TimesliderControlApi +> => { return { type: TIME_SLIDER_CONTROL, getIconType: () => 'search', getDisplayName: () => displayName, buildControl: async (initialState, buildApi, uuid, controlGroupApi) => { const { timeRangeMeta$, formatDate, cleanupTimeRangeSubscription } = - initTimeRangeSubscription(controlGroupApi, services); + initTimeRangeSubscription(controlGroupApi); const timeslice$ = new BehaviorSubject<[number, number] | undefined>(undefined); const isAnchored$ = new BehaviorSubject(initialState.isAnchored); const isPopoverOpen$ = new BehaviorSubject(false); diff --git a/src/plugins/controls/public/react_controls/controls/timeslider_control/init_time_range_subscription.ts b/src/plugins/controls/public/react_controls/controls/timeslider_control/init_time_range_subscription.ts index 7b4a2deb9f0d..7934e9deaa9b 100644 --- a/src/plugins/controls/public/react_controls/controls/timeslider_control/init_time_range_subscription.ts +++ b/src/plugins/controls/public/react_controls/controls/timeslider_control/init_time_range_subscription.ts @@ -14,26 +14,23 @@ import moment from 'moment'; import { BehaviorSubject, skip } from 'rxjs'; import { getTimeRangeMeta, getTimezone, TimeRangeMeta } from './get_time_range_meta'; import { getMomentTimezone } from './time_utils'; -import { Services } from './types'; -export function initTimeRangeSubscription(controlGroupApi: unknown, services: Services) { +export function initTimeRangeSubscription(controlGroupApi: unknown) { const timeRange$ = apiHasParentApi(controlGroupApi) && apiPublishesTimeRange(controlGroupApi.parentApi) ? controlGroupApi.parentApi.timeRange$ : new BehaviorSubject(undefined); - const timeRangeMeta$ = new BehaviorSubject( - getTimeRangeMeta(timeRange$.value, services) - ); + const timeRangeMeta$ = new BehaviorSubject(getTimeRangeMeta(timeRange$.value)); const timeRangeSubscription = timeRange$.pipe(skip(1)).subscribe((timeRange) => { - timeRangeMeta$.next(getTimeRangeMeta(timeRange, services)); + timeRangeMeta$.next(getTimeRangeMeta(timeRange)); }); return { timeRangeMeta$, formatDate: (epoch: number) => { return moment - .tz(epoch, getMomentTimezone(getTimezone(services))) + .tz(epoch, getMomentTimezone(getTimezone())) .locale(i18n.getLocale()) .format(timeRangeMeta$.value.format); }, diff --git a/src/plugins/controls/public/react_controls/controls/timeslider_control/register_timeslider_control.ts b/src/plugins/controls/public/react_controls/controls/timeslider_control/register_timeslider_control.ts index 8fbf23305820..338a52631c93 100644 --- a/src/plugins/controls/public/react_controls/controls/timeslider_control/register_timeslider_control.ts +++ b/src/plugins/controls/public/react_controls/controls/timeslider_control/register_timeslider_control.ts @@ -7,20 +7,16 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { CoreSetup } from '@kbn/core/public'; -import type { ControlsPluginStartDeps } from '../../../types'; -import { registerControlFactory } from '../../control_factory_registry'; import { TIME_SLIDER_CONTROL } from '../../../../common'; +import { untilPluginStartServicesReady } from '../../../services/kibana_services'; +import { registerControlFactory } from '../../control_factory_registry'; -export function registerTimeSliderControl(coreSetup: CoreSetup) { +export function registerTimeSliderControl() { registerControlFactory(TIME_SLIDER_CONTROL, async () => { - const [{ getTimesliderControlFactory }, [coreStart, depsStart]] = await Promise.all([ + const [{ getTimesliderControlFactory }] = await Promise.all([ import('./get_timeslider_control_factory'), - coreSetup.getStartServices(), + untilPluginStartServicesReady(), ]); - return getTimesliderControlFactory({ - core: coreStart, - data: depsStart.data, - }); + return getTimesliderControlFactory(); }); } diff --git a/src/plugins/controls/public/react_controls/controls/timeslider_control/types.ts b/src/plugins/controls/public/react_controls/controls/timeslider_control/types.ts index 702d02ae9acc..634e0351e77e 100644 --- a/src/plugins/controls/public/react_controls/controls/timeslider_control/types.ts +++ b/src/plugins/controls/public/react_controls/controls/timeslider_control/types.ts @@ -7,8 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { CoreStart } from '@kbn/core/public'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { PublishesPanelTitle, PublishesTimeslice } from '@kbn/presentation-publishing'; import type { DefaultControlState } from '../../../../common'; import type { DefaultControlApi } from '../types'; @@ -25,8 +23,3 @@ export interface TimesliderControlState extends DefaultControlState { export type TimesliderControlApi = DefaultControlApi & Pick & PublishesTimeslice; - -export interface Services { - core: CoreStart; - data: DataPublicPluginStart; -} diff --git a/src/plugins/controls/public/react_controls/controls/types.ts b/src/plugins/controls/public/react_controls/controls/types.ts index 85045f8bd70b..ce4ad9f194fa 100644 --- a/src/plugins/controls/public/react_controls/controls/types.ts +++ b/src/plugins/controls/public/react_controls/controls/types.ts @@ -40,15 +40,15 @@ export type DefaultControlApi = PublishesDataLoading & HasType & HasUniqueId & HasParentApi & { - // Can not use HasSerializableState interface - // HasSerializableState types serializeState as function returning 'MaybePromise' - // Controls serializeState is sync - serializeState: () => SerializedPanelState; - /** TODO: Make these non-public as part of https://github.com/elastic/kibana/issues/174961 */ setDataLoading: (loading: boolean) => void; setBlockingError: (error: Error | undefined) => void; grow: PublishingSubject; width: PublishingSubject; + + // Can not use HasSerializableState interface + // HasSerializableState types serializeState as function returning 'MaybePromise' + // Controls serializeState is sync + serializeState: () => SerializedPanelState; }; export type ControlApiRegistration = Omit< @@ -62,7 +62,6 @@ export type ControlApiInitialization; -// TODO: Move this to the Control plugin's setup contract export interface ControlFactory< State extends DefaultControlState = DefaultControlState, ControlApi extends DefaultControlApi = DefaultControlApi diff --git a/src/plugins/controls/public/react_controls/external_api/control_group_renderer.test.tsx b/src/plugins/controls/public/react_controls/external_api/control_group_renderer.test.tsx index 58269308f184..e034ca817908 100644 --- a/src/plugins/controls/public/react_controls/external_api/control_group_renderer.test.tsx +++ b/src/plugins/controls/public/react_controls/external_api/control_group_renderer.test.tsx @@ -9,26 +9,22 @@ import React from 'react'; -import { coreMock } from '@kbn/core/public/mocks'; -import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; import { Filter } from '@kbn/es-query'; import { PublishesUnifiedSearch, PublishingSubject } from '@kbn/presentation-publishing'; import { act, render, waitFor } from '@testing-library/react'; import { ControlGroupRendererApi } from '.'; +import { CONTROL_GROUP_TYPE } from '../..'; import { getControlGroupEmbeddableFactory } from '../control_group/get_control_group_factory'; import { ControlGroupRenderer, ControlGroupRendererProps } from './control_group_renderer'; -import { CONTROL_GROUP_TYPE } from '../..'; type ParentApiType = PublishesUnifiedSearch & { unifiedSearchFilters$?: PublishingSubject; }; describe('control group renderer', () => { - const core = coreMock.createStart(); - const dataViews = dataViewPluginMocks.createStartContract(); - const factory = getControlGroupEmbeddableFactory({ core, dataViews }); + const factory = getControlGroupEmbeddableFactory(); const buildControlGroupSpy = jest.spyOn(factory, 'buildEmbeddable'); const mountControlGroupRenderer = async ( diff --git a/src/plugins/controls/public/services/controls/controls.stub.ts b/src/plugins/controls/public/services/controls/controls.stub.ts deleted file mode 100644 index 2e182998c907..000000000000 --- a/src/plugins/controls/public/services/controls/controls.stub.ts +++ /dev/null @@ -1,46 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlFactory, DefaultControlApi } from '../../react_controls/controls/types'; -import { ControlsServiceType } from './types'; - -export type ControlsServiceFactory = PluginServiceFactory; -export const controlsServiceFactory = () => getStubControlsService(); - -export const getStubControlsService = () => { - const controlsFactoriesMap: { [key: string]: ControlFactory } = {}; - - const mockRegisterControlFactory = async < - State extends object = object, - ApiType extends DefaultControlApi = DefaultControlApi - >( - controlType: string, - getFactory: () => Promise> - ) => { - controlsFactoriesMap[controlType] = (await getFactory()) as ControlFactory; - }; - - const mockGetControlFactory = < - State extends object = object, - ApiType extends DefaultControlApi = DefaultControlApi - >( - type: string - ) => { - return controlsFactoriesMap[type] as ControlFactory; - }; - - const getAllControlTypes = () => Object.keys(controlsFactoriesMap); - - return { - registerControlFactory: mockRegisterControlFactory, - getControlFactory: mockGetControlFactory, - getAllControlTypes, - }; -}; diff --git a/src/plugins/controls/public/services/controls/controls_service.ts b/src/plugins/controls/public/services/controls/controls_service.ts deleted file mode 100644 index c794c056a4f8..000000000000 --- a/src/plugins/controls/public/services/controls/controls_service.ts +++ /dev/null @@ -1,24 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { - getAllControlTypes, - getControlFactory, - registerControlFactory, -} from '../../react_controls/control_factory_registry'; -import { ControlsServiceType } from './types'; - -export const controlsServiceFactory = () => controlsService; - -// export controls service directly for use in plugin setup lifecycle -export const controlsService: ControlsServiceType = { - registerControlFactory, - getControlFactory, - getAllControlTypes, -}; diff --git a/src/plugins/controls/public/services/controls/types.ts b/src/plugins/controls/public/services/controls/types.ts deleted file mode 100644 index d9011819d815..000000000000 --- a/src/plugins/controls/public/services/controls/types.ts +++ /dev/null @@ -1,23 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { - getAllControlTypes, - getControlFactory, - registerControlFactory, -} from '../../react_controls/control_factory_registry'; - -export type ControlsServiceFactory = PluginServiceFactory; - -export interface ControlsServiceType { - registerControlFactory: typeof registerControlFactory; - getControlFactory: typeof getControlFactory; - getAllControlTypes: typeof getAllControlTypes; -} diff --git a/src/plugins/controls/public/services/core/core.stub.ts b/src/plugins/controls/public/services/core/core.stub.ts deleted file mode 100644 index 052ddf46129e..000000000000 --- a/src/plugins/controls/public/services/core/core.stub.ts +++ /dev/null @@ -1,24 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { analyticsServiceMock, coreMock, themeServiceMock } from '@kbn/core/public/mocks'; -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsCoreService } from './types'; - -export type CoreServiceFactory = PluginServiceFactory; - -export const coreServiceFactory: CoreServiceFactory = () => { - const corePluginMock = coreMock.createStart(); - return { - analytics: analyticsServiceMock.createAnalyticsServiceStart(), - theme: themeServiceMock.createSetupContract(), - i18n: corePluginMock.i18n, - notifications: corePluginMock.notifications, - }; -}; diff --git a/src/plugins/controls/public/services/core/core_service.ts b/src/plugins/controls/public/services/core/core_service.ts deleted file mode 100644 index 090784c32d80..000000000000 --- a/src/plugins/controls/public/services/core/core_service.ts +++ /dev/null @@ -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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsCoreService } from './types'; -import { ControlsPluginStartDeps } from '../../types'; - -export type CoreServiceFactory = KibanaPluginServiceFactory< - ControlsCoreService, - ControlsPluginStartDeps ->; - -export const coreServiceFactory: CoreServiceFactory = ({ coreStart }) => { - const { analytics, theme, i18n, notifications } = coreStart; - - return { - analytics, - theme, - i18n, - notifications, - }; -}; diff --git a/src/plugins/controls/public/services/core/types.ts b/src/plugins/controls/public/services/core/types.ts deleted file mode 100644 index 9424a490b3f3..000000000000 --- a/src/plugins/controls/public/services/core/types.ts +++ /dev/null @@ -1,17 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { CoreStart } from '@kbn/core/public'; - -export interface ControlsCoreService { - analytics: CoreStart['analytics']; - i18n: CoreStart['i18n']; - theme: CoreStart['theme']; - notifications: CoreStart['notifications']; -} diff --git a/src/plugins/controls/public/services/data/data.stub.ts b/src/plugins/controls/public/services/data/data.stub.ts deleted file mode 100644 index 5cd50a68768e..000000000000 --- a/src/plugins/controls/public/services/data/data.stub.ts +++ /dev/null @@ -1,31 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { of } from 'rxjs'; -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { ControlsDataService } from './types'; - -export type DataServiceFactory = PluginServiceFactory; -export const dataServiceFactory: DataServiceFactory = () => ({ - query: {} as unknown as DataPublicPluginStart['query'], - searchSource: { - create: () => ({ - setField: () => {}, - fetch$: () => - of({ - rawResponse: { aggregations: { minAgg: { value: 0 }, maxAgg: { value: 1000 } } }, - }), - }), - } as unknown as DataPublicPluginStart['search']['searchSource'], - timefilter: { - createFilter: () => {}, - } as unknown as DataPublicPluginStart['query']['timefilter']['timefilter'], - fetchFieldRange: () => Promise.resolve({ min: 0, max: 100 }), -}); diff --git a/src/plugins/controls/public/services/data/data_service.ts b/src/plugins/controls/public/services/data/data_service.ts deleted file mode 100644 index 84a36c2775dc..000000000000 --- a/src/plugins/controls/public/services/data/data_service.ts +++ /dev/null @@ -1,29 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsPluginStartDeps } from '../../types'; -import { ControlsDataService } from './types'; - -export type DataServiceFactory = KibanaPluginServiceFactory< - ControlsDataService, - ControlsPluginStartDeps ->; - -export const dataServiceFactory: DataServiceFactory = ({ startPlugins }) => { - const { - data: { query: queryPlugin, search }, - } = startPlugins; - - return { - query: queryPlugin, - searchSource: search.searchSource, - timefilter: queryPlugin.timefilter.timefilter, - }; -}; diff --git a/src/plugins/controls/public/services/data/types.ts b/src/plugins/controls/public/services/data/types.ts deleted file mode 100644 index 68a7553ee08d..000000000000 --- a/src/plugins/controls/public/services/data/types.ts +++ /dev/null @@ -1,15 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -export interface ControlsDataService { - query: DataPublicPluginStart['query']; - searchSource: DataPublicPluginStart['search']['searchSource']; - timefilter: DataPublicPluginStart['query']['timefilter']['timefilter']; -} diff --git a/src/plugins/controls/public/services/data_views/data_views.stub.ts b/src/plugins/controls/public/services/data_views/data_views.stub.ts deleted file mode 100644 index f2ffcf5f08a9..000000000000 --- a/src/plugins/controls/public/services/data_views/data_views.stub.ts +++ /dev/null @@ -1,52 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; -import { DataView } from '@kbn/data-views-plugin/common'; -import { ControlsDataViewsService } from './types'; - -export type DataViewsServiceFactory = PluginServiceFactory; - -let currentDataView: DataView | undefined; -export const injectStorybookDataView = (dataView?: DataView) => (currentDataView = dataView); - -export const dataViewsServiceFactory: DataViewsServiceFactory = () => ({ - get: ((dataViewId) => - new Promise((resolve, reject) => - setTimeout(() => { - if (!currentDataView) { - reject( - new Error( - 'mock DataViews service currentDataView is undefined, call injectStorybookDataView to set' - ) - ); - } else if (currentDataView.id === dataViewId) { - resolve(currentDataView); - } else { - reject( - new Error( - `mock DataViews service currentDataView.id: ${currentDataView.id} does not match requested dataViewId: ${dataViewId}` - ) - ); - } - }, 100) - ) as unknown) as DataViewsPublicPluginStart['get'], - getIdsWithTitle: (() => - new Promise((resolve) => - setTimeout(() => { - const idsWithTitle: Array<{ id: string | undefined; title: string }> = []; - if (currentDataView) { - idsWithTitle.push({ id: currentDataView.id, title: currentDataView.title }); - } - resolve(idsWithTitle); - }, 100) - ) as unknown) as DataViewsPublicPluginStart['getIdsWithTitle'], - getDefaultId: () => Promise.resolve(currentDataView?.id ?? null), -}); diff --git a/src/plugins/controls/public/services/data_views/data_views_service.ts b/src/plugins/controls/public/services/data_views/data_views_service.ts deleted file mode 100644 index 4ad4c0b8d224..000000000000 --- a/src/plugins/controls/public/services/data_views/data_views_service.ts +++ /dev/null @@ -1,29 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsPluginStartDeps } from '../../types'; -import { ControlsDataViewsService } from './types'; - -export type DataViewsServiceFactory = KibanaPluginServiceFactory< - ControlsDataViewsService, - ControlsPluginStartDeps ->; - -export const dataViewsServiceFactory: DataViewsServiceFactory = ({ startPlugins }) => { - const { - dataViews: { get, getIdsWithTitle, getDefaultId }, - } = startPlugins; - - return { - get, - getDefaultId, - getIdsWithTitle, - }; -}; diff --git a/src/plugins/controls/public/services/data_views/types.ts b/src/plugins/controls/public/services/data_views/types.ts deleted file mode 100644 index a204af439634..000000000000 --- a/src/plugins/controls/public/services/data_views/types.ts +++ /dev/null @@ -1,16 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; - -export interface ControlsDataViewsService { - get: DataViewsPublicPluginStart['get']; - getDefaultId: DataViewsPublicPluginStart['getDefaultId']; - getIdsWithTitle: DataViewsPublicPluginStart['getIdsWithTitle']; -} diff --git a/src/plugins/controls/public/services/embeddable/embeddable.stub.ts b/src/plugins/controls/public/services/embeddable/embeddable.stub.ts deleted file mode 100644 index 5f75b4e7b2d1..000000000000 --- a/src/plugins/controls/public/services/embeddable/embeddable.stub.ts +++ /dev/null @@ -1,19 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; -import { ControlsEmbeddableService } from './types'; - -export type EmbeddableServiceFactory = PluginServiceFactory; -export const embeddableServiceFactory: EmbeddableServiceFactory = () => { - const { doStart } = embeddablePluginMock.createInstance(); - const start = doStart(); - return { getEmbeddableFactory: start.getEmbeddableFactory }; -}; diff --git a/src/plugins/controls/public/services/embeddable/embeddable_service.ts b/src/plugins/controls/public/services/embeddable/embeddable_service.ts deleted file mode 100644 index 79c556f69c05..000000000000 --- a/src/plugins/controls/public/services/embeddable/embeddable_service.ts +++ /dev/null @@ -1,23 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsEmbeddableService } from './types'; -import { ControlsPluginStartDeps } from '../../types'; - -export type EmbeddableServiceFactory = KibanaPluginServiceFactory< - ControlsEmbeddableService, - ControlsPluginStartDeps ->; - -export const embeddableServiceFactory: EmbeddableServiceFactory = ({ startPlugins }) => { - return { - getEmbeddableFactory: startPlugins.embeddable.getEmbeddableFactory, - }; -}; diff --git a/src/plugins/controls/public/services/http/http.stub.ts b/src/plugins/controls/public/services/http/http.stub.ts deleted file mode 100644 index ead893559b48..000000000000 --- a/src/plugins/controls/public/services/http/http.stub.ts +++ /dev/null @@ -1,19 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { HttpResponse } from '@kbn/core/public'; -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsHTTPService } from './types'; - -type HttpServiceFactory = PluginServiceFactory; - -export const httpServiceFactory: HttpServiceFactory = () => ({ - get: async () => ({} as unknown as HttpResponse), - fetch: async () => ({} as unknown as HttpResponse), -}); diff --git a/src/plugins/controls/public/services/http/http_service.ts b/src/plugins/controls/public/services/http/http_service.ts deleted file mode 100644 index fac813ea9227..000000000000 --- a/src/plugins/controls/public/services/http/http_service.ts +++ /dev/null @@ -1,27 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsHTTPService } from './types'; -import { ControlsPluginStartDeps } from '../../types'; - -export type HttpServiceFactory = KibanaPluginServiceFactory< - ControlsHTTPService, - ControlsPluginStartDeps ->; -export const httpServiceFactory: HttpServiceFactory = ({ coreStart }) => { - const { - http: { get, fetch }, - } = coreStart; - - return { - get, - fetch, - }; -}; diff --git a/src/plugins/controls/public/services/http/types.ts b/src/plugins/controls/public/services/http/types.ts deleted file mode 100644 index 0072bc0dacff..000000000000 --- a/src/plugins/controls/public/services/http/types.ts +++ /dev/null @@ -1,15 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { CoreSetup } from '@kbn/core/public'; - -export interface ControlsHTTPService { - get: CoreSetup['http']['get']; - fetch: CoreSetup['http']['fetch']; -} diff --git a/src/plugins/controls/public/services/kibana_services.ts b/src/plugins/controls/public/services/kibana_services.ts new file mode 100644 index 000000000000..b9e5fadcecaa --- /dev/null +++ b/src/plugins/controls/public/services/kibana_services.ts @@ -0,0 +1,42 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { BehaviorSubject } from 'rxjs'; + +import { CoreStart } from '@kbn/core/public'; +import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; + +import { ControlsPluginStartDeps } from '../types'; + +export let coreServices: CoreStart; +export let dataService: DataPublicPluginStart; +export let dataViewsService: DataViewsPublicPluginStart; + +const servicesReady$ = new BehaviorSubject(false); + +export const setKibanaServices = (kibanaCore: CoreStart, deps: ControlsPluginStartDeps) => { + coreServices = kibanaCore; + dataService = deps.data; + dataViewsService = deps.dataViews; + + servicesReady$.next(true); +}; + +export const untilPluginStartServicesReady = () => { + if (servicesReady$.value) return Promise.resolve(); + return new Promise((resolve) => { + const subscription = servicesReady$.subscribe((isInitialized) => { + if (isInitialized) { + subscription.unsubscribe(); + resolve(); + } + }); + }); +}; diff --git a/src/plugins/controls/public/services/mocks.ts b/src/plugins/controls/public/services/mocks.ts new file mode 100644 index 000000000000..231323c37a12 --- /dev/null +++ b/src/plugins/controls/public/services/mocks.ts @@ -0,0 +1,23 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { coreMock } from '@kbn/core/public/mocks'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; + +import { setKibanaServices } from './kibana_services'; + +export const setStubKibanaServices = () => { + setKibanaServices(coreMock.createStart(), { + data: dataPluginMock.createStartContract(), + dataViews: dataViewPluginMocks.createStartContract(), + uiActions: uiActionsPluginMock.createStartContract(), + }); +}; diff --git a/src/plugins/controls/public/services/overlays/overlays.stub.ts b/src/plugins/controls/public/services/overlays/overlays.stub.ts deleted file mode 100644 index d7938f5a8a3c..000000000000 --- a/src/plugins/controls/public/services/overlays/overlays.stub.ts +++ /dev/null @@ -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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { - MountPoint, - OverlayFlyoutOpenOptions, - OverlayModalConfirmOptions, - OverlayRef, -} from '@kbn/core/public'; -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsOverlaysService } from './types'; - -type OverlaysServiceFactory = PluginServiceFactory; - -class StubRef implements OverlayRef { - public readonly onClose: Promise = Promise.resolve(); - - public close(): Promise { - return this.onClose; - } -} - -export const overlaysServiceFactory: OverlaysServiceFactory = () => ({ - openFlyout: (mount: MountPoint, options?: OverlayFlyoutOpenOptions) => new StubRef(), - openConfirm: (message: MountPoint | string, options?: OverlayModalConfirmOptions) => - Promise.resolve(true), -}); diff --git a/src/plugins/controls/public/services/overlays/overlays_service.ts b/src/plugins/controls/public/services/overlays/overlays_service.ts deleted file mode 100644 index b8e7309e4eb3..000000000000 --- a/src/plugins/controls/public/services/overlays/overlays_service.ts +++ /dev/null @@ -1,27 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsPluginStartDeps } from '../../types'; -import { ControlsOverlaysService } from './types'; - -export type OverlaysServiceFactory = KibanaPluginServiceFactory< - ControlsOverlaysService, - ControlsPluginStartDeps ->; -export const overlaysServiceFactory: OverlaysServiceFactory = ({ coreStart }) => { - const { - overlays: { openFlyout, openConfirm }, - } = coreStart; - - return { - openFlyout, - openConfirm, - }; -}; diff --git a/src/plugins/controls/public/services/overlays/types.ts b/src/plugins/controls/public/services/overlays/types.ts deleted file mode 100644 index 7cb5fd297554..000000000000 --- a/src/plugins/controls/public/services/overlays/types.ts +++ /dev/null @@ -1,20 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { - MountPoint, - OverlayFlyoutOpenOptions, - OverlayModalConfirmOptions, - OverlayRef, -} from '@kbn/core/public'; - -export interface ControlsOverlaysService { - openFlyout(mount: MountPoint, options?: OverlayFlyoutOpenOptions): OverlayRef; - openConfirm(message: MountPoint | string, options?: OverlayModalConfirmOptions): Promise; -} diff --git a/src/plugins/controls/public/services/plugin_services.stub.ts b/src/plugins/controls/public/services/plugin_services.stub.ts deleted file mode 100644 index 1e7b3982f88c..000000000000 --- a/src/plugins/controls/public/services/plugin_services.stub.ts +++ /dev/null @@ -1,54 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { - PluginServiceProvider, - PluginServiceProviders, - PluginServiceRegistry, - PluginServices, -} from '@kbn/presentation-util-plugin/public'; - -import { ControlsPluginStart } from '../types'; -import { ControlsServices } from './types'; - -import { controlsServiceFactory } from './controls/controls.stub'; -import { coreServiceFactory } from './core/core.stub'; -import { dataServiceFactory } from './data/data.stub'; -import { dataViewsServiceFactory } from './data_views/data_views.stub'; -import { embeddableServiceFactory } from './embeddable/embeddable.stub'; -import { httpServiceFactory } from './http/http.stub'; -import { overlaysServiceFactory } from './overlays/overlays.stub'; -import { settingsServiceFactory } from './settings/settings.stub'; -import { unifiedSearchServiceFactory } from './unified_search/unified_search.stub'; -import { storageServiceFactory } from './storage/storage_service.stub'; - -export const providers: PluginServiceProviders = { - embeddable: new PluginServiceProvider(embeddableServiceFactory), - controls: new PluginServiceProvider(controlsServiceFactory), - data: new PluginServiceProvider(dataServiceFactory), - dataViews: new PluginServiceProvider(dataViewsServiceFactory), - http: new PluginServiceProvider(httpServiceFactory), - overlays: new PluginServiceProvider(overlaysServiceFactory), - settings: new PluginServiceProvider(settingsServiceFactory), - core: new PluginServiceProvider(coreServiceFactory), - storage: new PluginServiceProvider(storageServiceFactory), - unifiedSearch: new PluginServiceProvider(unifiedSearchServiceFactory), -}; - -export const pluginServices = new PluginServices(); - -export const registry = new PluginServiceRegistry(providers); - -export const getStubPluginServices = (): ControlsPluginStart => { - pluginServices.setRegistry(registry.start({})); - return { - getControlFactory: pluginServices.getServices().controls.getControlFactory, - getAllControlTypes: pluginServices.getServices().controls.getAllControlTypes, - }; -}; diff --git a/src/plugins/controls/public/services/plugin_services.ts b/src/plugins/controls/public/services/plugin_services.ts deleted file mode 100644 index d0d255287117..000000000000 --- a/src/plugins/controls/public/services/plugin_services.ts +++ /dev/null @@ -1,52 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { - KibanaPluginServiceParams, - PluginServiceProvider, - PluginServiceProviders, - PluginServiceRegistry, - PluginServices, -} from '@kbn/presentation-util-plugin/public'; -import { ControlsPluginStartDeps } from '../types'; -import { ControlsServices } from './types'; - -import { controlsServiceFactory } from './controls/controls_service'; -import { coreServiceFactory } from './core/core_service'; -import { dataServiceFactory } from './data/data_service'; -import { dataViewsServiceFactory } from './data_views/data_views_service'; -import { embeddableServiceFactory } from './embeddable/embeddable_service'; -import { httpServiceFactory } from './http/http_service'; -import { overlaysServiceFactory } from './overlays/overlays_service'; -import { settingsServiceFactory } from './settings/settings_service'; -import { controlsStorageServiceFactory } from './storage/storage_service'; -import { unifiedSearchServiceFactory } from './unified_search/unified_search_service'; - -export const providers: PluginServiceProviders< - ControlsServices, - KibanaPluginServiceParams -> = { - controls: new PluginServiceProvider(controlsServiceFactory), - data: new PluginServiceProvider(dataServiceFactory), - dataViews: new PluginServiceProvider(dataViewsServiceFactory), - embeddable: new PluginServiceProvider(embeddableServiceFactory), - http: new PluginServiceProvider(httpServiceFactory), - overlays: new PluginServiceProvider(overlaysServiceFactory), - settings: new PluginServiceProvider(settingsServiceFactory), - storage: new PluginServiceProvider(controlsStorageServiceFactory), - core: new PluginServiceProvider(coreServiceFactory), - unifiedSearch: new PluginServiceProvider(unifiedSearchServiceFactory), -}; - -export const pluginServices = new PluginServices(); - -export const registry = new PluginServiceRegistry< - ControlsServices, - KibanaPluginServiceParams ->(providers); diff --git a/src/plugins/controls/public/services/settings/settings.stub.ts b/src/plugins/controls/public/services/settings/settings.stub.ts deleted file mode 100644 index 6ee5bdce4f2f..000000000000 --- a/src/plugins/controls/public/services/settings/settings.stub.ts +++ /dev/null @@ -1,18 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsSettingsService } from './types'; - -export type SettingsServiceFactory = PluginServiceFactory; -export const settingsServiceFactory: SettingsServiceFactory = () => ({ - getTimezone: () => 'Browser', - getDateFormat: () => 'MMM D, YYYY @ HH:mm:ss.SSS', - getDefaultTimeRange: () => ({ from: 'now-15m', to: 'now' }), -}); diff --git a/src/plugins/controls/public/services/settings/settings_service.ts b/src/plugins/controls/public/services/settings/settings_service.ts deleted file mode 100644 index e5f8751503a6..000000000000 --- a/src/plugins/controls/public/services/settings/settings_service.ts +++ /dev/null @@ -1,32 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsSettingsService } from './types'; -import { ControlsPluginStartDeps } from '../../types'; - -export type SettingsServiceFactory = KibanaPluginServiceFactory< - ControlsSettingsService, - ControlsPluginStartDeps ->; - -export const settingsServiceFactory: SettingsServiceFactory = ({ coreStart }) => { - return { - getDateFormat: () => { - return coreStart.uiSettings.get('dateFormat', 'MMM D, YYYY @ HH:mm:ss.SSS'); - }, - getTimezone: () => { - return coreStart.uiSettings.get('dateFormat:tz', 'Browser'); - }, - getDefaultTimeRange: () => { - const defaultTimeRange = coreStart.uiSettings.get('timepicker:timeDefaults'); - return defaultTimeRange ? defaultTimeRange : { from: 'now-15m', to: 'now' }; - }, - }; -}; diff --git a/src/plugins/controls/public/services/settings/types.ts b/src/plugins/controls/public/services/settings/types.ts deleted file mode 100644 index f8b29adeb85a..000000000000 --- a/src/plugins/controls/public/services/settings/types.ts +++ /dev/null @@ -1,16 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import type { TimeRange } from '@kbn/es-query'; - -export interface ControlsSettingsService { - getTimezone: () => string; - getDateFormat: () => string; - getDefaultTimeRange: () => TimeRange; -} diff --git a/src/plugins/controls/public/services/storage/storage_service.stub.ts b/src/plugins/controls/public/services/storage/storage_service.stub.ts deleted file mode 100644 index 389d804b51bb..000000000000 --- a/src/plugins/controls/public/services/storage/storage_service.stub.ts +++ /dev/null @@ -1,20 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsStorageService } from './types'; - -type StorageServiceFactory = PluginServiceFactory; - -export const storageServiceFactory: StorageServiceFactory = () => { - return { - getShowInvalidSelectionWarning: () => false, - setShowInvalidSelectionWarning: (value: boolean) => null, - }; -}; diff --git a/src/plugins/controls/public/services/storage/storage_service.ts b/src/plugins/controls/public/services/storage/storage_service.ts deleted file mode 100644 index 9f0403c505e7..000000000000 --- a/src/plugins/controls/public/services/storage/storage_service.ts +++ /dev/null @@ -1,31 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { Storage } from '@kbn/kibana-utils-plugin/public'; -import { ControlsStorageService } from './types'; - -const STORAGE_KEY = 'controls:showInvalidSelectionWarning'; - -class StorageService implements ControlsStorageService { - private storage: Storage; - - constructor() { - this.storage = new Storage(localStorage); - } - - getShowInvalidSelectionWarning = () => { - return this.storage.get(STORAGE_KEY); - }; - - setShowInvalidSelectionWarning = (value: boolean) => { - this.storage.set(STORAGE_KEY, value); - }; -} - -export const controlsStorageServiceFactory = () => new StorageService(); diff --git a/src/plugins/controls/public/services/types.ts b/src/plugins/controls/public/services/types.ts deleted file mode 100644 index c38ad6b64fac..000000000000 --- a/src/plugins/controls/public/services/types.ts +++ /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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { ControlsServiceType } from './controls/types'; -import { ControlsCoreService } from './core/types'; -import { ControlsDataService } from './data/types'; -import { ControlsDataViewsService } from './data_views/types'; -import { ControlsEmbeddableService } from './embeddable/types'; -import { ControlsHTTPService } from './http/types'; -import { ControlsOverlaysService } from './overlays/types'; -import { ControlsSettingsService } from './settings/types'; -import { ControlsStorageService } from './storage/types'; -import { ControlsUnifiedSearchService } from './unified_search/types'; - -export interface ControlsServices { - // dependency services - dataViews: ControlsDataViewsService; - overlays: ControlsOverlaysService; - embeddable: ControlsEmbeddableService; - data: ControlsDataService; - unifiedSearch: ControlsUnifiedSearchService; - http: ControlsHTTPService; - settings: ControlsSettingsService; - core: ControlsCoreService; - - // controls plugin's own services - controls: ControlsServiceType; - storage: ControlsStorageService; -} diff --git a/src/plugins/controls/public/services/unified_search/types.ts b/src/plugins/controls/public/services/unified_search/types.ts deleted file mode 100644 index 28aa8e05feb2..000000000000 --- a/src/plugins/controls/public/services/unified_search/types.ts +++ /dev/null @@ -1,14 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; - -export interface ControlsUnifiedSearchService { - autocomplete: UnifiedSearchPublicPluginStart['autocomplete']; -} diff --git a/src/plugins/controls/public/services/unified_search/unified_search.stub.ts b/src/plugins/controls/public/services/unified_search/unified_search.stub.ts deleted file mode 100644 index c7b1959c8bbc..000000000000 --- a/src/plugins/controls/public/services/unified_search/unified_search.stub.ts +++ /dev/null @@ -1,27 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { DataViewField } from '@kbn/data-views-plugin/common'; -import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; - -import { ControlsUnifiedSearchService } from './types'; - -let valueSuggestionMethod = ({ field, query }: { field: DataViewField; query: string }) => - Promise.resolve(['storybook', 'default', 'values']); -export const replaceValueSuggestionMethod = ( - newMethod: ({ field, query }: { field: DataViewField; query: string }) => Promise -) => (valueSuggestionMethod = newMethod); - -export type UnifiedSearchServiceFactory = PluginServiceFactory; -export const unifiedSearchServiceFactory: UnifiedSearchServiceFactory = () => ({ - autocomplete: { - getValueSuggestions: valueSuggestionMethod, - } as unknown as UnifiedSearchPublicPluginStart['autocomplete'], -}); diff --git a/src/plugins/controls/public/services/unified_search/unified_search_service.ts b/src/plugins/controls/public/services/unified_search/unified_search_service.ts deleted file mode 100644 index 45d10cfa9ac7..000000000000 --- a/src/plugins/controls/public/services/unified_search/unified_search_service.ts +++ /dev/null @@ -1,27 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsPluginStartDeps } from '../../types'; -import { ControlsUnifiedSearchService } from './types'; - -export type UnifiedSearchServiceFactory = KibanaPluginServiceFactory< - ControlsUnifiedSearchService, - ControlsPluginStartDeps ->; - -export const unifiedSearchServiceFactory: UnifiedSearchServiceFactory = ({ startPlugins }) => { - const { - unifiedSearch: { autocomplete }, - } = startPlugins; - - return { - autocomplete, - }; -}; diff --git a/src/plugins/controls/public/types.ts b/src/plugins/controls/public/types.ts index 2ecbd3876360..bed3260bb440 100644 --- a/src/plugins/controls/public/types.ts +++ b/src/plugins/controls/public/types.ts @@ -9,11 +9,8 @@ import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; -import { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public'; +import { EmbeddableSetup } from '@kbn/embeddable-plugin/public'; import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; -import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; - -import { ControlsServiceType } from './services/controls/types'; export interface CanClearSelections { clearSelections: () => void; @@ -26,22 +23,11 @@ export const isClearableControl = (control: unknown): control is CanClearSelecti /** * Plugin types */ -export interface ControlsPluginSetup { - registerControlFactory: ControlsServiceType['registerControlFactory']; -} - -export interface ControlsPluginStart { - getControlFactory: ControlsServiceType['getControlFactory']; - getAllControlTypes: ControlsServiceType['getAllControlTypes']; -} - export interface ControlsPluginSetupDeps { embeddable: EmbeddableSetup; } export interface ControlsPluginStartDeps { uiActions: UiActionsStart; - embeddable: EmbeddableStart; data: DataPublicPluginStart; dataViews: DataViewsPublicPluginStart; - unifiedSearch: UnifiedSearchPublicPluginStart; } diff --git a/src/plugins/controls/server/control_group/control_group_persistence.ts b/src/plugins/controls/server/control_group/control_group_persistence.ts index eb63b28ccecb..e90aa850c6d1 100644 --- a/src/plugins/controls/server/control_group/control_group_persistence.ts +++ b/src/plugins/controls/server/control_group/control_group_persistence.ts @@ -10,7 +10,7 @@ import { SerializableRecord } from '@kbn/utility-types'; import { - DEFAULT_CONTROL_STYLE, + DEFAULT_CONTROL_LABEL_POSITION, type ControlGroupRuntimeState, type ControlGroupSerializedState, type ControlPanelState, @@ -19,7 +19,7 @@ import { export const getDefaultControlGroupState = (): SerializableControlGroupState => ({ panels: {}, - labelPosition: DEFAULT_CONTROL_STYLE, + labelPosition: DEFAULT_CONTROL_LABEL_POSITION, chainingSystem: 'HIERARCHICAL', autoApplySelections: true, ignoreParentSettings: { diff --git a/src/plugins/controls/server/options_list/options_list_cluster_settings_route.ts b/src/plugins/controls/server/options_list/options_list_cluster_settings_route.ts index f51b9a5b5b62..04b0aaa3e6f7 100644 --- a/src/plugins/controls/server/options_list/options_list_cluster_settings_route.ts +++ b/src/plugins/controls/server/options_list/options_list_cluster_settings_route.ts @@ -15,7 +15,7 @@ export const setupOptionsListClusterSettingsRoute = ({ http }: CoreSetup) => { router.versioned .get({ access: 'internal', - path: '/internal/controls/optionsList/getExpensiveQueriesSetting', + path: '/internal/controls/getExpensiveQueriesSetting', }) .addVersion( { diff --git a/src/plugins/controls/tsconfig.json b/src/plugins/controls/tsconfig.json index fc9d6572ccf2..abcafa291358 100644 --- a/src/plugins/controls/tsconfig.json +++ b/src/plugins/controls/tsconfig.json @@ -35,7 +35,6 @@ "@kbn/presentation-containers", "@kbn/presentation-publishing", "@kbn/content-management-utils", - "@kbn/core-lifecycle-browser", "@kbn/field-formats-plugin", "@kbn/presentation-panel-plugin", "@kbn/shared-ux-utility" diff --git a/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx b/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx index b280e626467d..86940d52a81b 100644 --- a/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx +++ b/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx @@ -349,6 +349,9 @@ export class SavedObjectFinderUi extends React.Component< box: { incremental: true, 'data-test-subj': 'savedObjectFinderSearchInput', + schema: { + recognizedFields: ['type', 'tag'], + }, }, filters: this.props.showFilter ? [ diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap index 7e88de9674cc..bc0eda7e6d5a 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap @@ -6,6 +6,12 @@ exports[`Table prevents hidden saved objects from being deleted 1`] = ` box={ Object { "data-test-subj": "savedObjectSearchBar", + "schema": Object { + "recognizedFields": Array [ + "type", + "tag", + ], + }, } } filters={ @@ -234,6 +240,12 @@ exports[`Table should render normally 1`] = ` box={ Object { "data-test-subj": "savedObjectSearchBar", + "schema": Object { + "recognizedFields": Array [ + "type", + "tag", + ], + }, } } filters={ diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx index dbd9b2a55060..a32a1e9e958e 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx @@ -392,7 +392,12 @@ export class Table extends PureComponent { {activeActionContents} { renderESQLPopover(); expect(screen.getByTestId('esql-menu-button')).toBeInTheDocument(); await userEvent.click(screen.getByRole('button')); - + expect(screen.getByTestId('esql-quick-reference')).toBeInTheDocument(); expect(screen.getByTestId('esql-examples')).toBeInTheDocument(); expect(screen.getByTestId('esql-about')).toBeInTheDocument(); expect(screen.getByTestId('esql-feedback')).toBeInTheDocument(); diff --git a/src/plugins/unified_search/public/query_string_input/esql_menu_popover.tsx b/src/plugins/unified_search/public/query_string_input/esql_menu_popover.tsx index 003827344c30..d684448670c4 100644 --- a/src/plugins/unified_search/public/query_string_input/esql_menu_popover.tsx +++ b/src/plugins/unified_search/public/query_string_input/esql_menu_popover.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useState, useCallback } from 'react'; import { EuiPopover, EuiButton, @@ -19,6 +19,7 @@ import { import { useKibana } from '@kbn/kibana-react-plugin/public'; import { i18n } from '@kbn/i18n'; import { FEEDBACK_LINK } from '@kbn/esql-utils'; +import { LanguageDocumentationFlyout } from '@kbn/language-documentation-popover'; import type { IUnifiedSearchPluginServices } from '../types'; export const ESQLMenuPopover = () => { @@ -26,15 +27,33 @@ export const ESQLMenuPopover = () => { const { docLinks } = kibana.services; const [isESQLMenuPopoverOpen, setIsESQLMenuPopoverOpen] = useState(false); + const [isLanguageComponentOpen, setIsLanguageComponentOpen] = useState(false); + + const toggleLanguageComponent = useCallback(async () => { + setIsLanguageComponentOpen(!isLanguageComponentOpen); + setIsESQLMenuPopoverOpen(false); + }, [isLanguageComponentOpen]); + const esqlPanelItems = useMemo(() => { const panelItems: EuiContextMenuPanelProps['items'] = []; panelItems.push( + toggleLanguageComponent()} + > + {i18n.translate('unifiedSearch.query.queryBar.esqlMenu.quickReference', { + defaultMessage: 'Quick Reference', + })} + , setIsESQLMenuPopoverOpen(false)} > {i18n.translate('unifiedSearch.query.queryBar.esqlMenu.documentation', { defaultMessage: 'Documentation', @@ -46,6 +65,7 @@ export const ESQLMenuPopover = () => { data-test-subj="esql-examples" target="_blank" href={docLinks.links.query.queryESQLExamples} + onClick={() => setIsESQLMenuPopoverOpen(false)} > {i18n.translate('unifiedSearch.query.queryBar.esqlMenu.documentation', { defaultMessage: 'Example queries', @@ -58,6 +78,7 @@ export const ESQLMenuPopover = () => { data-test-subj="esql-feedback" target="_blank" href={FEEDBACK_LINK} + onClick={() => setIsESQLMenuPopoverOpen(false)} > {i18n.translate('unifiedSearch.query.queryBar.esqlMenu.documentation', { defaultMessage: 'Submit feedback', @@ -65,32 +86,44 @@ export const ESQLMenuPopover = () => { ); return panelItems; - }, [docLinks.links.query.queryESQL, docLinks.links.query.queryESQLExamples]); + }, [ + docLinks.links.query.queryESQL, + docLinks.links.query.queryESQLExamples, + toggleLanguageComponent, + ]); return ( - setIsESQLMenuPopoverOpen(!isESQLMenuPopoverOpen)} - data-test-subj="esql-menu-button" - size="s" - > - {i18n.translate('unifiedSearch.query.queryBar.esqlMenu.label', { - defaultMessage: 'ES|QL help', - })} - - } - panelProps={{ - ['data-test-subj']: 'esql-menu-popover', - css: { width: 240 }, - }} - isOpen={isESQLMenuPopoverOpen} - closePopover={() => setIsESQLMenuPopoverOpen(false)} - panelPaddingSize="s" - display="block" - > - - + <> + setIsESQLMenuPopoverOpen(!isESQLMenuPopoverOpen)} + data-test-subj="esql-menu-button" + size="s" + > + {i18n.translate('unifiedSearch.query.queryBar.esqlMenu.label', { + defaultMessage: 'ES|QL help', + })} + + } + panelProps={{ + ['data-test-subj']: 'esql-menu-popover', + css: { width: 240 }, + }} + isOpen={isESQLMenuPopoverOpen} + closePopover={() => setIsESQLMenuPopoverOpen(false)} + panelPaddingSize="s" + display="block" + > + + + + ); }; diff --git a/src/plugins/unified_search/tsconfig.json b/src/plugins/unified_search/tsconfig.json index 909c0031b5a3..e836f3c6daa6 100644 --- a/src/plugins/unified_search/tsconfig.json +++ b/src/plugins/unified_search/tsconfig.json @@ -46,7 +46,8 @@ "@kbn/data-view-utils", "@kbn/esql-utils", "@kbn/react-kibana-mount", - "@kbn/field-utils" + "@kbn/field-utils", + "@kbn/language-documentation-popover" ], "exclude": [ "target/**/*", diff --git a/x-pack/performance/journeys_e2e/web_logs_dashboard_esql.ts b/x-pack/performance/journeys_e2e/web_logs_dashboard_esql.ts new file mode 100644 index 000000000000..47422c2735bb --- /dev/null +++ b/x-pack/performance/journeys_e2e/web_logs_dashboard_esql.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Journey } from '@kbn/journeys'; +import { subj } from '@kbn/test-subj-selector'; + +export const journey = new Journey({ + esArchives: ['x-pack/performance/es_archives/sample_data_logs'], + kbnArchives: ['x-pack/performance/kbn_archives/logs_no_map_dashboard_esql'], +}) + + .step('Go to Dashboards Page', async ({ page, kbnUrl, kibanaPage }) => { + await page.goto(kbnUrl.get(`/app/dashboards`)); + await kibanaPage.waitForListViewTable(); + }) + + .step('Go to Web Logs Dashboard', async ({ page, kibanaPage }) => { + await page.click(subj('dashboardListingTitleLink-Logs-dashboard-with-ES|QL')); + await kibanaPage.waitForVisualizations({ count: 11 }); + }); diff --git a/x-pack/performance/kbn_archives/logs_no_map_dashboard_esql.json b/x-pack/performance/kbn_archives/logs_no_map_dashboard_esql.json new file mode 100644 index 000000000000..7d06e25ef09c --- /dev/null +++ b/x-pack/performance/kbn_archives/logs_no_map_dashboard_esql.json @@ -0,0 +1,330 @@ +{ + "attributes": { + "fieldFormatMap": "{\"hour_of_day\":{}}", + "name": "Kibana Sample Data Logs", + "runtimeFieldMap": "{\"hour_of_day\":{\"type\":\"long\",\"script\":{\"source\":\"emit(doc['timestamp'].value.getHour());\"}}}", + "timeFieldName": "timestamp", + "title": "kibana_sample_data_logs" + }, + "coreMigrationVersion": "8.6.0", + "created_at": "2022-10-26T13:11:17.374Z", + "id": "90943e30-9a47-11e8-b64d-95841ca0b247", + "migrationVersion": { + "index-pattern": "8.0.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2022-10-26T13:11:17.374Z", + "version": "WzEzNiwxXQ==" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "title": "[Logs] Machine OS and Destination Sankey Chart", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"[Logs] Machine OS and Destination Sankey Chart\",\"type\":\"vega\",\"params\":{\"spec\":\"{ \\n $schema: https://vega.github.io/schema/vega/v5.json\\n data: [\\n\\t{\\n \\t// query ES based on the currently selected time range and filter string\\n \\tname: rawData\\n \\turl: {\\n \\t%context%: true\\n \\t%timefield%: timestamp\\n \\tindex: kibana_sample_data_logs\\n \\tbody: {\\n \\tsize: 0\\n \\taggs: {\\n \\ttable: {\\n \\tcomposite: {\\n \\tsize: 10000\\n \\tsources: [\\n \\t{\\n \\tstk1: {\\n \\tterms: {field: \\\"machine.os.keyword\\\"}\\n \\t}\\n \\t}\\n \\t{\\n \\tstk2: {\\n \\tterms: {field: \\\"geo.dest\\\"}\\n \\t}\\n \\t}\\n \\t]\\n \\t}\\n \\t}\\n \\t}\\n \\t}\\n \\t}\\n \\t// From the result, take just the data we are interested in\\n \\tformat: {property: \\\"aggregations.table.buckets\\\"}\\n \\t// Convert key.stk1 -> stk1 for simpler access below\\n \\ttransform: [\\n \\t{type: \\\"formula\\\", expr: \\\"datum.key.stk1\\\", as: \\\"stk1\\\"}\\n \\t{type: \\\"formula\\\", expr: \\\"datum.key.stk2\\\", as: \\\"stk2\\\"}\\n \\t{type: \\\"formula\\\", expr: \\\"datum.doc_count\\\", as: \\\"size\\\"}\\n \\t]\\n\\t}\\n\\t{\\n \\tname: nodes\\n \\tsource: rawData\\n \\ttransform: [\\n \\t// when a country is selected, filter out unrelated data\\n \\t{\\n \\ttype: filter\\n \\texpr: !groupSelector || groupSelector.stk1 == datum.stk1 || groupSelector.stk2 == datum.stk2\\n \\t}\\n \\t// Set new key for later lookups - identifies each node\\n \\t{type: \\\"formula\\\", expr: \\\"datum.stk1+datum.stk2\\\", as: \\\"key\\\"}\\n \\t// instead of each table row, create two new rows,\\n \\t// one for the source (stack=stk1) and one for destination node (stack=stk2).\\n \\t// The country code stored in stk1 and stk2 fields is placed into grpId field.\\n \\t{\\n \\ttype: fold\\n \\tfields: [\\\"stk1\\\", \\\"stk2\\\"]\\n \\tas: [\\\"stack\\\", \\\"grpId\\\"]\\n \\t}\\n \\t// Create a sortkey, different for stk1 and stk2 stacks.\\n \\t{\\n \\ttype: formula\\n \\texpr: datum.stack == 'stk1' ? datum.stk1+datum.stk2 : datum.stk2+datum.stk1\\n \\tas: sortField\\n \\t}\\n \\t// Calculate y0 and y1 positions for stacking nodes one on top of the other,\\n \\t// independently for each stack, and ensuring they are in the proper order,\\n \\t// alphabetical from the top (reversed on the y axis)\\n \\t{\\n \\ttype: stack\\n \\tgroupby: [\\\"stack\\\"]\\n \\tsort: {field: \\\"sortField\\\", order: \\\"descending\\\"}\\n \\tfield: size\\n \\t}\\n \\t// calculate vertical center point for each node, used to draw edges\\n \\t{type: \\\"formula\\\", expr: \\\"(datum.y0+datum.y1)/2\\\", as: \\\"yc\\\"}\\n \\t]\\n\\t}\\n\\t{\\n \\tname: groups\\n \\tsource: nodes\\n \\ttransform: [\\n \\t// combine all nodes into country groups, summing up the doc counts\\n \\t{\\n \\ttype: aggregate\\n \\tgroupby: [\\\"stack\\\", \\\"grpId\\\"]\\n \\tfields: [\\\"size\\\"]\\n \\tops: [\\\"sum\\\"]\\n \\tas: [\\\"total\\\"]\\n \\t}\\n \\t// re-calculate the stacking y0,y1 values\\n \\t{\\n \\ttype: stack\\n \\tgroupby: [\\\"stack\\\"]\\n \\tsort: {field: \\\"grpId\\\", order: \\\"descending\\\"}\\n \\tfield: total\\n \\t}\\n \\t// project y0 and y1 values to screen coordinates\\n \\t// doing it once here instead of doing it several times in marks\\n \\t{type: \\\"formula\\\", expr: \\\"scale('y', datum.y0)\\\", as: \\\"scaledY0\\\"}\\n \\t{type: \\\"formula\\\", expr: \\\"scale('y', datum.y1)\\\", as: \\\"scaledY1\\\"}\\n \\t// boolean flag if the label should be on the right of the stack\\n \\t{type: \\\"formula\\\", expr: \\\"datum.stack == 'stk1'\\\", as: \\\"rightLabel\\\"}\\n \\t// Calculate traffic percentage for this country using \\\"y\\\" scale\\n \\t// domain upper bound, which represents the total traffic\\n \\t{\\n \\ttype: formula\\n \\texpr: datum.total/domain('y')[1]\\n \\tas: percentage\\n \\t}\\n \\t]\\n\\t}\\n\\t{\\n \\t// This is a temp lookup table with all the 'stk2' stack nodes\\n \\tname: destinationNodes\\n \\tsource: nodes\\n \\ttransform: [\\n \\t{type: \\\"filter\\\", expr: \\\"datum.stack == 'stk2'\\\"}\\n \\t]\\n\\t}\\n\\t{\\n \\tname: edges\\n \\tsource: nodes\\n \\ttransform: [\\n \\t// we only want nodes from the left stack\\n \\t{type: \\\"filter\\\", expr: \\\"datum.stack == 'stk1'\\\"}\\n \\t// find corresponding node from the right stack, keep it as \\\"target\\\"\\n \\t{\\n \\ttype: lookup\\n \\tfrom: destinationNodes\\n \\tkey: key\\n \\tfields: [\\\"key\\\"]\\n \\tas: [\\\"target\\\"]\\n \\t}\\n \\t// calculate SVG link path between stk1 and stk2 stacks for the node pair\\n \\t{\\n \\ttype: linkpath\\n \\torient: horizontal\\n \\tshape: diagonal\\n \\tsourceY: {expr: \\\"scale('y', datum.yc)\\\"}\\n \\tsourceX: {expr: \\\"scale('x', 'stk1') + bandwidth('x')\\\"}\\n \\ttargetY: {expr: \\\"scale('y', datum.target.yc)\\\"}\\n \\ttargetX: {expr: \\\"scale('x', 'stk2')\\\"}\\n \\t}\\n \\t// A little trick to calculate the thickness of the line.\\n \\t// The value needs to be the same as the hight of the node, but scaling\\n \\t// size to screen's height gives inversed value because screen's Y\\n \\t// coordinate goes from the top to the bottom, whereas the graph's Y=0\\n \\t// is at the bottom. So subtracting scaled doc count from screen height\\n \\t// (which is the \\\"lower\\\" bound of the \\\"y\\\" scale) gives us the right value\\n \\t{\\n \\ttype: formula\\n \\texpr: range('y')[0]-scale('y', datum.size)\\n \\tas: strokeWidth\\n \\t}\\n \\t// Tooltip needs individual link's percentage of all traffic\\n \\t{\\n \\ttype: formula\\n \\texpr: datum.size/domain('y')[1]\\n \\tas: percentage\\n \\t}\\n \\t]\\n\\t}\\n ]\\n scales: [\\n\\t{\\n \\t// calculates horizontal stack positioning\\n \\tname: x\\n \\ttype: band\\n \\trange: width\\n \\tdomain: [\\\"stk1\\\", \\\"stk2\\\"]\\n \\tpaddingOuter: 0.05\\n \\tpaddingInner: 0.95\\n\\t}\\n\\t{\\n \\t// this scale goes up as high as the highest y1 value of all nodes\\n \\tname: y\\n \\ttype: linear\\n \\trange: height\\n \\tdomain: {data: \\\"nodes\\\", field: \\\"y1\\\"}\\n\\t}\\n\\t{\\n \\t// use rawData to ensure the colors stay the same when clicking.\\n \\tname: color\\n \\ttype: ordinal\\n \\trange: category\\n \\tdomain: {data: \\\"rawData\\\", field: \\\"stk1\\\"}\\n\\t}\\n\\t{\\n \\t// this scale is used to map internal ids (stk1, stk2) to stack names\\n \\tname: stackNames\\n \\ttype: ordinal\\n \\trange: [\\\"Source\\\", \\\"Destination\\\"]\\n \\tdomain: [\\\"stk1\\\", \\\"stk2\\\"]\\n\\t}\\n ]\\n axes: [\\n\\t{\\n \\t// x axis should use custom label formatting to print proper stack names\\n \\torient: bottom\\n \\tscale: x\\n \\tencode: {\\n \\tlabels: {\\n \\tupdate: {\\n \\ttext: {scale: \\\"stackNames\\\", field: \\\"value\\\"}\\n \\t}\\n \\t}\\n \\t}\\n\\t}\\n\\t{orient: \\\"left\\\", scale: \\\"y\\\"}\\n ]\\n marks: [\\n\\t{\\n \\t// draw the connecting line between stacks\\n \\ttype: path\\n \\tname: edgeMark\\n \\tfrom: {data: \\\"edges\\\"}\\n \\t// this prevents some autosizing issues with large strokeWidth for paths\\n \\tclip: true\\n \\tencode: {\\n \\tupdate: {\\n \\t// By default use color of the left node, except when showing traffic\\n \\t// from just one country, in which case use destination color.\\n \\tstroke: [\\n \\t{\\n \\ttest: groupSelector && groupSelector.stack=='stk1'\\n \\tscale: color\\n \\tfield: stk2\\n \\t}\\n \\t{scale: \\\"color\\\", field: \\\"stk1\\\"}\\n \\t]\\n \\tstrokeWidth: {field: \\\"strokeWidth\\\"}\\n \\tpath: {field: \\\"path\\\"}\\n \\t// when showing all traffic, and hovering over a country,\\n \\t// highlight the traffic from that country.\\n \\tstrokeOpacity: {\\n \\tsignal: !groupSelector && (groupHover.stk1 == datum.stk1 || groupHover.stk2 == datum.stk2) ? 0.9 : 0.3\\n \\t}\\n \\t// Ensure that the hover-selected edges show on top\\n \\tzindex: {\\n \\tsignal: !groupSelector && (groupHover.stk1 == datum.stk1 || groupHover.stk2 == datum.stk2) ? 1 : 0\\n \\t}\\n \\t// format tooltip string\\n \\ttooltip: {\\n \\tsignal: datum.stk1 + ' → ' + datum.stk2 + '\\t' + format(datum.size, ',.0f') + ' (' + format(datum.percentage, '.1%') + ')'\\n \\t}\\n \\t}\\n \\t// Simple mouseover highlighting of a single line\\n \\thover: {\\n \\tstrokeOpacity: {value: 1}\\n \\t}\\n \\t}\\n\\t}\\n\\t{\\n \\t// draw stack groups (countries)\\n \\ttype: rect\\n \\tname: groupMark\\n \\tfrom: {data: \\\"groups\\\"}\\n \\tencode: {\\n \\tenter: {\\n \\tfill: {scale: \\\"color\\\", field: \\\"grpId\\\"}\\n \\twidth: {scale: \\\"x\\\", band: 1}\\n \\t}\\n \\tupdate: {\\n \\tx: {scale: \\\"x\\\", field: \\\"stack\\\"}\\n \\ty: {field: \\\"scaledY0\\\"}\\n \\ty2: {field: \\\"scaledY1\\\"}\\n \\tfillOpacity: {value: 0.6}\\n \\ttooltip: {\\n \\tsignal: datum.grpId + ' ' + format(datum.total, ',.0f') + ' (' + format(datum.percentage, '.1%') + ')'\\n \\t}\\n \\t}\\n \\thover: {\\n \\tfillOpacity: {value: 1}\\n \\t}\\n \\t}\\n\\t}\\n\\t{\\n \\t// draw country code labels on the inner side of the stack\\n \\ttype: text\\n \\tfrom: {data: \\\"groups\\\"}\\n \\t// don't process events for the labels - otherwise line mouseover is unclean\\n \\tinteractive: false\\n \\tencode: {\\n \\tupdate: {\\n \\t// depending on which stack it is, position x with some padding\\n \\tx: {\\n \\tsignal: scale('x', datum.stack) + (datum.rightLabel ? bandwidth('x') + 8 : -8)\\n \\t}\\n \\t// middle of the group\\n \\tyc: {signal: \\\"(datum.scaledY0 + datum.scaledY1)/2\\\"}\\n \\talign: {signal: \\\"datum.rightLabel ? 'left' : 'right'\\\"}\\n \\tbaseline: {value: \\\"middle\\\"}\\n \\tfontWeight: {value: \\\"bold\\\"}\\n \\t// only show text label if the group's height is large enough\\n \\ttext: {signal: \\\"abs(datum.scaledY0-datum.scaledY1) > 13 ? datum.grpId : ''\\\"}\\n \\t}\\n \\t}\\n\\t}\\n\\t{\\n \\t// Create a \\\"show all\\\" button. Shown only when a country is selected.\\n \\ttype: group\\n \\tdata: [\\n \\t// We need to make the button show only when groupSelector signal is true.\\n \\t// Each mark is drawn as many times as there are elements in the backing data.\\n \\t// Which means that if values list is empty, it will not be drawn.\\n \\t// Here I create a data source with one empty object, and filter that list\\n \\t// based on the signal value. This can only be done in a group.\\n \\t{\\n \\tname: dataForShowAll\\n \\tvalues: [{}]\\n \\ttransform: [{type: \\\"filter\\\", expr: \\\"groupSelector\\\"}]\\n \\t}\\n \\t]\\n \\t// Set button size and positioning\\n \\tencode: {\\n \\tenter: {\\n \\txc: {signal: \\\"width/2\\\"}\\n \\ty: {value: 30}\\n \\twidth: {value: 80}\\n \\theight: {value: 30}\\n \\t}\\n \\t}\\n \\tmarks: [\\n \\t{\\n \\t// This group is shown as a button with rounded corners.\\n \\ttype: group\\n \\t// mark name allows signal capturing\\n \\tname: groupReset\\n \\t// Only shows button if dataForShowAll has values.\\n \\tfrom: {data: \\\"dataForShowAll\\\"}\\n \\tencode: {\\n \\tenter: {\\n \\tcornerRadius: {value: 6}\\n \\tfill: {value: \\\"#F5F7FA\\\"}\\n \\tstroke: {value: \\\"#c1c1c1\\\"}\\n \\tstrokeWidth: {value: 2}\\n \\t// use parent group's size\\n \\theight: {\\n \\tfield: {group: \\\"height\\\"}\\n \\t}\\n \\twidth: {\\n \\tfield: {group: \\\"width\\\"}\\n \\t}\\n \\t}\\n \\tupdate: {\\n \\t// groups are transparent by default\\n \\topacity: {value: 1}\\n \\t}\\n \\thover: {\\n \\topacity: {value: 0.7}\\n \\t}\\n \\t}\\n \\tmarks: [\\n \\t{\\n \\ttype: text\\n \\t// if true, it will prevent clicking on the button when over text.\\n \\tinteractive: false\\n \\tencode: {\\n \\tenter: {\\n \\t// center text in the paren group\\n \\txc: {\\n \\tfield: {group: \\\"width\\\"}\\n \\tmult: 0.5\\n \\t}\\n \\tyc: {\\n \\tfield: {group: \\\"height\\\"}\\n \\tmult: 0.5\\n \\toffset: 2\\n \\t}\\n \\talign: {value: \\\"center\\\"}\\n \\tbaseline: {value: \\\"middle\\\"}\\n \\tfontWeight: {value: \\\"bold\\\"}\\n \\ttext: {value: \\\"Show All\\\"}\\n \\t}\\n \\t}\\n \\t}\\n \\t]\\n \\t}\\n \\t]\\n\\t}\\n ]\\n signals: [\\n\\t{\\n \\t// used to highlight traffic to/from the same country\\n \\tname: groupHover\\n \\tvalue: {}\\n \\ton: [\\n \\t{\\n \\tevents: @groupMark:mouseover\\n \\tupdate: \\\"{stk1:datum.stack=='stk1' && datum.grpId, stk2:datum.stack=='stk2' && datum.grpId}\\\"\\n \\t}\\n \\t{events: \\\"mouseout\\\", update: \\\"{}\\\"}\\n \\t]\\n\\t}\\n\\t// used to filter only the data related to the selected country\\n\\t{\\n \\tname: groupSelector\\n \\tvalue: false\\n \\ton: [\\n \\t{\\n \\t// Clicking groupMark sets this signal to the filter values\\n \\tevents: @groupMark:click!\\n \\tupdate: \\\"{stack:datum.stack, stk1:datum.stack=='stk1' && datum.grpId, stk2:datum.stack=='stk2' && datum.grpId}\\\"\\n \\t}\\n \\t{\\n \\t// Clicking \\\"show all\\\" button, or double-clicking anywhere resets it\\n \\tevents: [\\n \\t{type: \\\"click\\\", markname: \\\"groupReset\\\"}\\n \\t{type: \\\"dblclick\\\"}\\n \\t]\\n \\tupdate: \\\"false\\\"\\n \\t}\\n \\t]\\n\\t}\\n ]\\n}\\n\"},\"aggs\":[]}" + }, + "coreMigrationVersion": "8.6.0", + "created_at": "2022-10-26T13:11:17.374Z", + "id": "7cbd2350-2223-11e8-b802-5bcf64c2cfb4", + "migrationVersion": { + "visualization": "8.5.0" + }, + "references": [], + "type": "visualization", + "updated_at": "2022-10-26T13:11:17.374Z", + "version": "WzEzMywxXQ==" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "title": "[Logs] Unique Destination Heatmap", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"[Logs] Unique Destination Heatmap\",\"type\":\"vega\",\"aggs\":[],\"params\":{\"spec\":\"{\\n $schema: https://vega.github.io/schema/vega-lite/v5.json\\n data: {\\n url: {\\n %context%: true\\n %timefield%: @timestamp\\n index: kibana_sample_data_logs\\n body: {\\n aggs: {\\n countries: {\\n terms: {\\n field: geo.dest\\n size: 25\\n }\\n aggs: {\\n hours: {\\n histogram: {\\n field: hour_of_day\\n interval: 1\\n }\\n aggs: {\\n unique: {\\n cardinality: {\\n field: clientip\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n size: 0\\n }\\n }\\n format: {property: \\\"aggregations.countries.buckets\\\"}\\n }\\n \\n transform: [\\n {\\n flatten: [\\\"hours.buckets\\\"],\\n as: [\\\"buckets\\\"]\\n },\\n {\\n filter: \\\"datum.buckets.unique.value > 0\\\"\\n }\\n ]\\n\\n mark: {\\n type: rect\\n tooltip: {\\n expr: \\\"{\\\\\\\"Unique Visitors\\\\\\\": datum.buckets.unique.value,\\\\\\\"geo.src\\\\\\\": datum.key,\\\\\\\"Hour\\\\\\\": datum.buckets.key}\\\"\\n }\\n }\\n\\n encoding: {\\n x: {\\n field: buckets.key\\n type: nominal\\n scale: {\\n domain: {\\n expr: \\\"sequence(0, 24)\\\"\\n }\\n }\\n axis: {\\n title: false\\n labelAngle: 0\\n }\\n }\\n y: {\\n field: key\\n type: nominal\\n sort: {\\n field: -buckets.unique.value\\n }\\n axis: {title: false}\\n }\\n color: {\\n field: buckets.unique.value\\n type: quantitative\\n axis: {title: false}\\n scale: {\\n scheme: blues\\n }\\n }\\n }\\n}\\n\"}}" + }, + "coreMigrationVersion": "8.6.0", + "created_at": "2022-10-26T13:11:17.374Z", + "id": "cb099a20-ea66-11eb-9425-113343a037e3", + "migrationVersion": { + "visualization": "8.5.0" + }, + "references": [], + "type": "visualization", + "updated_at": "2022-10-26T13:11:17.374Z", + "version": "WzEzMCwxXQ==" +} + +{ + "attributes": { + "state": { + "datasourceStates": { + "formBased": { + "layers": { + "7d9a32b1-8cc2-410c-83a5-2eb66a3f0321": { + "columnOrder": [ + "a8511a62-2b78-4ba4-9425-a417df6e059f", + "b5f3dc78-dba8-4db8-87b6-24a0b9cca260", + "b5f3dc78-dba8-4db8-87b6-24a0b9cca260X0", + "b5f3dc78-dba8-4db8-87b6-24a0b9cca260X1", + "b5f3dc78-dba8-4db8-87b6-24a0b9cca260X2", + "b5f3dc78-dba8-4db8-87b6-24a0b9cca260X3" + ], + "columns": { + "a8511a62-2b78-4ba4-9425-a417df6e059f": { + "dataType": "number", + "isBucketed": true, + "label": "bytes", + "operationType": "range", + "params": { + "maxBars": "auto", + "ranges": [ + { + "from": 0, + "label": "", + "to": 1000 + } + ], + "type": "histogram" + }, + "scale": "interval", + "sourceField": "bytes" + }, + "b5f3dc78-dba8-4db8-87b6-24a0b9cca260": { + "customLabel": true, + "dataType": "number", + "isBucketed": false, + "label": "% of visits", + "operationType": "formula", + "params": { + "format": { + "id": "percent", + "params": { + "decimals": 1 + } + }, + "formula": "count() / overall_sum(count())", + "isFormulaBroken": false + }, + "references": [ + "b5f3dc78-dba8-4db8-87b6-24a0b9cca260X3" + ], + "scale": "ratio" + }, + "b5f3dc78-dba8-4db8-87b6-24a0b9cca260X0": { + "customLabel": true, + "dataType": "number", + "isBucketed": false, + "label": "Part of count() / overall_sum(count())", + "operationType": "count", + "scale": "ratio", + "sourceField": "___records___" + }, + "b5f3dc78-dba8-4db8-87b6-24a0b9cca260X1": { + "customLabel": true, + "dataType": "number", + "isBucketed": false, + "label": "Part of count() / overall_sum(count())", + "operationType": "count", + "scale": "ratio", + "sourceField": "___records___" + }, + "b5f3dc78-dba8-4db8-87b6-24a0b9cca260X2": { + "customLabel": true, + "dataType": "number", + "isBucketed": false, + "label": "Part of count() / overall_sum(count())", + "operationType": "overall_sum", + "references": [ + "b5f3dc78-dba8-4db8-87b6-24a0b9cca260X1" + ], + "scale": "ratio" + }, + "b5f3dc78-dba8-4db8-87b6-24a0b9cca260X3": { + "customLabel": true, + "dataType": "number", + "isBucketed": false, + "label": "Part of count() / overall_sum(count())", + "operationType": "math", + "params": { + "tinymathAst": { + "args": [ + "b5f3dc78-dba8-4db8-87b6-24a0b9cca260X0", + "b5f3dc78-dba8-4db8-87b6-24a0b9cca260X2" + ], + "location": { + "max": 30, + "min": 0 + }, + "name": "divide", + "text": "count() / overall_sum(count())", + "type": "function" + } + }, + "references": [ + "b5f3dc78-dba8-4db8-87b6-24a0b9cca260X0", + "b5f3dc78-dba8-4db8-87b6-24a0b9cca260X2" + ], + "scale": "ratio" + } + }, + "incompleteColumns": {} + } + } + } + }, + "filters": [], + "query": { + "language": "kuery", + "query": "" + }, + "visualization": { + "axisTitlesVisibilitySettings": { + "x": false, + "yLeft": false, + "yRight": true + }, + "fittingFunction": "None", + "gridlinesVisibilitySettings": { + "x": true, + "yLeft": true, + "yRight": true + }, + "layers": [ + { + "accessors": [ + "b5f3dc78-dba8-4db8-87b6-24a0b9cca260" + ], + "layerId": "7d9a32b1-8cc2-410c-83a5-2eb66a3f0321", + "layerType": "data", + "position": "top", + "seriesType": "bar_stacked", + "showGridlines": false, + "xAccessor": "a8511a62-2b78-4ba4-9425-a417df6e059f" + } + ], + "legend": { + "isVisible": true, + "legendSize": "auto", + "position": "right" + }, + "preferredSeriesType": "bar_stacked", + "tickLabelsVisibilitySettings": { + "x": true, + "yLeft": true, + "yRight": true + }, + "valueLabels": "hide", + "yLeftExtent": { + "mode": "full" + }, + "yRightExtent": { + "mode": "full" + } + } + }, + "title": "[Logs] Bytes distribution", + "visualizationType": "lnsXY" + }, + "coreMigrationVersion": "8.6.0", + "created_at": "2022-10-26T13:11:17.374Z", + "id": "16b1d7d0-ea71-11eb-8b4b-f7b600de0f7d", + "migrationVersion": { + "lens": "8.6.0" + }, + "references": [ + { + "id": "90943e30-9a47-11e8-b64d-95841ca0b247", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern" + }, + { + "id": "90943e30-9a47-11e8-b64d-95841ca0b247", + "name": "indexpattern-datasource-layer-7d9a32b1-8cc2-410c-83a5-2eb66a3f0321", + "type": "index-pattern" + } + ], + "type": "lens", + "updated_at": "2022-10-26T13:11:17.374Z", + "version": "WzEzNSwxXQ==" +} + +{ + "attributes": { + "version": 2, + "controlGroupInput": { + "chainingSystem": "HIERARCHICAL", + "controlStyle": "oneLine", + "ignoreParentSettingsJSON": "{\"ignoreFilters\":false,\"ignoreQuery\":false,\"ignoreTimerange\":false,\"ignoreValidations\":false}", + "panelsJSON": "{\"612f8db8-9ba9-41cf-a809-d133fe9b83a8\":{\"order\":0,\"width\":\"small\",\"grow\":true,\"type\":\"optionsListControl\",\"explicitInput\":{\"fieldName\":\"geo.src\",\"title\":\"Source Country\",\"id\":\"612f8db8-9ba9-41cf-a809-d133fe9b83a8\",\"enhancements\":{}}},\"9807212f-5078-4c42-879c-6f28b3033fc9\":{\"order\":1,\"width\":\"small\",\"grow\":true,\"type\":\"optionsListControl\",\"explicitInput\":{\"fieldName\":\"machine.os.keyword\",\"parentFieldName\":\"machine.os\",\"title\":\"OS\",\"id\":\"9807212f-5078-4c42-879c-6f28b3033fc9\",\"enhancements\":{}}},\"6bf7a1b4-282e-43ac-aa46-81b97fa3acae\":{\"order\":2,\"width\":\"small\",\"grow\":true,\"type\":\"rangeSliderControl\",\"explicitInput\":{\"fieldName\":\"bytes\",\"title\":\"Bytes\",\"id\":\"6bf7a1b4-282e-43ac-aa46-81b97fa3acae\",\"enhancements\":{}}}}" + }, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "description": "", + "refreshInterval": { + "pause": true, + "value": 60000 + }, + "timeRestore": true, + "optionsJSON": "{\"useMargins\":true,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"hidePanelTitles\":false}", + "panelsJSON": "[{\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":7,\"i\":\"4fe72258-efdd-4bdd-abdf-05262776290e\"},\"panelIndex\":\"4fe72258-efdd-4bdd-abdf-05262776290e\",\"embeddableConfig\":{\"hidePanelTitles\":true,\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"savedVis\":{\"title\":\"[Logs] Markdown Instructions\",\"description\":\"\",\"type\":\"markdown\",\"params\":{\"fontSize\":12,\"openLinksInNewTab\":true,\"markdown\":\"## Sample Logs Data\\nThis dashboard contains sample data for you to play with. You can view it, search it, and interact with the visualizations. For more information about Kibana, check our [docs](https://www.elastic.co/guide/en/kibana/current/index.html).\"},\"uiState\":{},\"data\":{\"aggs\":[],\"searchSource\":{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}}}},{\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":0,\"w\":11,\"h\":8,\"i\":\"a44f3813-2797-4593-bd35-6cede8306306\"},\"panelIndex\":\"a44f3813-2797-4593-bd35-6cede8306306\",\"embeddableConfig\":{\"attributes\":{\"title\":\"Metric\",\"references\":[],\"state\":{\"datasourceStates\":{\"textBased\":{\"layers\":{\"64a4cc71-c831-4644-9022-3fc833b8be77\":{\"index\":\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\",\"query\":{\"esql\":\"FROM kibana_sample_data_logs | STATS COUNT(*) | RENAME `COUNT(*)` as `Visits`\"},\"columns\":[{\"columnId\":\"Visits\",\"fieldName\":\"Visits\",\"meta\":{\"type\":\"number\",\"esType\":\"long\"},\"inMetricDimension\":true}],\"timeField\":\"@timestamp\"}},\"indexPatternRefs\":[{\"id\":\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\",\"title\":\"kibana_sample_data_logs\",\"timeField\":\"@timestamp\"}]}},\"filters\":[],\"query\":{\"esql\":\"FROM kibana_sample_data_logs | STATS COUNT(*) | RENAME `COUNT(*)` as `Visits`\"},\"visualization\":{\"layerId\":\"64a4cc71-c831-4644-9022-3fc833b8be77\",\"layerType\":\"data\",\"metricAccessor\":\"Visits\"},\"adHocDataViews\":{\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\":{\"id\":\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\",\"title\":\"kibana_sample_data_logs\",\"timeFieldName\":\"@timestamp\",\"sourceFilters\":[],\"type\":\"esql\",\"fieldFormats\":{},\"runtimeFieldMap\":{},\"allowNoIndex\":false,\"name\":\"kibana_sample_data_logs\",\"allowHidden\":false}}},\"visualizationType\":\"lnsMetric\",\"type\":\"lens\"},\"disabledActions\":[\"OPEN_FLYOUT_ADD_DRILLDOWN\"],\"enhancements\":{}},\"title\":\"\"},{\"type\":\"lens\",\"gridData\":{\"x\":35,\"y\":0,\"w\":12,\"h\":8,\"i\":\"5fe87c12-e623-4fbb-81af-d3cea2fb7861\"},\"panelIndex\":\"5fe87c12-e623-4fbb-81af-d3cea2fb7861\",\"embeddableConfig\":{\"attributes\":{\"title\":\"Metric\",\"references\":[],\"state\":{\"datasourceStates\":{\"textBased\":{\"layers\":{\"323d8ae0-0a4e-4540-84e2-0d36c8f15b46\":{\"index\":\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\",\"query\":{\"esql\":\"FROM kibana_sample_data_logs | STATS COUNT_DISTINCT(clientip) | RENAME `COUNT_DISTINCT(clientip)` as `Unique Visitors`\"},\"columns\":[{\"columnId\":\"Unique Visitors\",\"fieldName\":\"Unique Visitors\",\"meta\":{\"type\":\"number\",\"esType\":\"long\"},\"inMetricDimension\":true}],\"timeField\":\"@timestamp\"}},\"indexPatternRefs\":[{\"id\":\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\",\"title\":\"kibana_sample_data_logs\",\"timeField\":\"@timestamp\"}]}},\"filters\":[],\"query\":{\"esql\":\"FROM kibana_sample_data_logs | STATS COUNT_DISTINCT(clientip) | RENAME `COUNT_DISTINCT(clientip)` as `Unique Visitors`\"},\"visualization\":{\"layerId\":\"323d8ae0-0a4e-4540-84e2-0d36c8f15b46\",\"layerType\":\"data\",\"metricAccessor\":\"Unique Visitors\",\"palette\":{\"name\":\"custom\",\"type\":\"palette\",\"params\":{\"steps\":3,\"name\":\"custom\",\"reverse\":false,\"rangeType\":\"number\",\"rangeMin\":0,\"rangeMax\":null,\"progression\":\"fixed\",\"stops\":[{\"color\":\"#D23115\",\"stop\":500},{\"color\":\"#FCC400\",\"stop\":1000},{\"color\":\"#68BC00\",\"stop\":1640}],\"colorStops\":[{\"color\":\"#D23115\",\"stop\":0},{\"color\":\"#FCC400\",\"stop\":500},{\"color\":\"#68BC00\",\"stop\":1000}],\"continuity\":\"above\",\"maxSteps\":5}}},\"adHocDataViews\":{\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\":{\"id\":\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\",\"title\":\"kibana_sample_data_logs\",\"timeFieldName\":\"@timestamp\",\"sourceFilters\":[],\"type\":\"esql\",\"fieldFormats\":{},\"runtimeFieldMap\":{},\"allowNoIndex\":false,\"name\":\"kibana_sample_data_logs\",\"allowHidden\":false}}},\"visualizationType\":\"lnsMetric\",\"type\":\"lens\"},\"disabledActions\":[\"OPEN_FLYOUT_ADD_DRILLDOWN\"],\"enhancements\":{}},\"title\":\"\"},{\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":7,\"w\":24,\"h\":9,\"i\":\"8f731f6b-9bf3-44b8-bc3c-c4dfe6f1aef8\"},\"panelIndex\":\"8f731f6b-9bf3-44b8-bc3c-c4dfe6f1aef8\",\"embeddableConfig\":{\"attributes\":{\"title\":\"results over Over time\",\"references\":[],\"state\":{\"datasourceStates\":{\"textBased\":{\"layers\":{\"03e904c7-6fa2-4c18-953f-bd1c87c53826\":{\"index\":\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\",\"query\":{\"esql\":\"FROM kibana_sample_data_logs\\n| EVAL timestamp=DATE_TRUNC(3 hour, @timestamp), status = CASE( TO_INTEGER(response.keyword) >= 200 and TO_INTEGER(response.keyword) < 400, \\\"HTTP 2xx and 3xx\\\", TO_INTEGER(response.keyword) >= 400 and TO_INTEGER(response.keyword) < 500, \\\"HTTP 4xx\\\", \\\"HTTP 5xx\\\")\\n| STATS results = COUNT(*) by `Over time` = BUCKET(timestamp, 50, ?_tstart, ?_tend), status\"},\"columns\":[{\"columnId\":\"results\",\"fieldName\":\"results\",\"meta\":{\"type\":\"number\",\"esType\":\"long\"},\"inMetricDimension\":true},{\"columnId\":\"Over time\",\"fieldName\":\"Over time\",\"meta\":{\"type\":\"date\",\"esType\":\"date\"}},{\"columnId\":\"status\",\"fieldName\":\"status\",\"meta\":{\"type\":\"string\",\"esType\":\"keyword\"}}],\"timeField\":\"@timestamp\"}},\"indexPatternRefs\":[{\"id\":\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\",\"title\":\"kibana_sample_data_logs\",\"timeField\":\"@timestamp\"}]}},\"filters\":[],\"query\":{\"esql\":\"FROM kibana_sample_data_logs\\n| EVAL timestamp=DATE_TRUNC(3 hour, @timestamp), status = CASE( TO_INTEGER(response.keyword) >= 200 and TO_INTEGER(response.keyword) < 400, \\\"HTTP 2xx and 3xx\\\", TO_INTEGER(response.keyword) >= 400 and TO_INTEGER(response.keyword) < 500, \\\"HTTP 4xx\\\", \\\"HTTP 5xx\\\")\\n| STATS results = COUNT(*) by `Over time` = BUCKET(timestamp, 50, ?_tstart, ?_tend), status\"},\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"area_percentage_stacked\",\"layers\":[{\"layerId\":\"03e904c7-6fa2-4c18-953f-bd1c87c53826\",\"seriesType\":\"area_percentage_stacked\",\"xAccessor\":\"Over time\",\"splitAccessor\":\"status\",\"accessors\":[\"results\"],\"layerType\":\"data\",\"colorMapping\":{\"assignments\":[],\"specialAssignments\":[{\"rule\":{\"type\":\"other\"},\"color\":{\"type\":\"loop\"},\"touched\":false}],\"paletteId\":\"eui_amsterdam_color_blind\",\"colorMode\":{\"type\":\"categorical\"}}}]},\"adHocDataViews\":{\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\":{\"id\":\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\",\"title\":\"kibana_sample_data_logs\",\"timeFieldName\":\"@timestamp\",\"sourceFilters\":[],\"type\":\"esql\",\"fieldFormats\":{},\"runtimeFieldMap\":{},\"allowNoIndex\":false,\"name\":\"kibana_sample_data_logs\",\"allowHidden\":false}}},\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"description\":\"\"},\"disabledActions\":[\"OPEN_FLYOUT_ADD_DRILLDOWN\"],\"enhancements\":{},\"description\":\"\"},\"title\":\"[Logs] Response Codes Over Time\"},{\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":8,\"w\":11,\"h\":8,\"i\":\"5fe9ddf8-b36b-4a81-b7b4-57278d7f4e7a\"},\"panelIndex\":\"5fe9ddf8-b36b-4a81-b7b4-57278d7f4e7a\",\"embeddableConfig\":{\"attributes\":{\"title\":\"Metric\",\"references\":[],\"state\":{\"datasourceStates\":{\"textBased\":{\"layers\":{\"bc54f0c9-40ec-4082-92f5-3d6a6fdc4463\":{\"index\":\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\",\"query\":{\"esql\":\"FROM kibana_sample_data_logs\\n| EVAL type = CASE(TO_INTEGER(response.keyword) >= 400 and TO_INTEGER(response.keyword) < 500, \\\"4xx\\\", \\\"Other\\\")\\n| STATS count = COUNT(*) by type\\n| EVAL count_4xx = CASE(type == \\\"4xx\\\", count), count_rest = CASE(type == \\\"Other\\\", count)\\n| DROP count, type\\n| STATS count_4xx = MAX(count_4xx), count_rest = MAX(count_rest)\\n| EVAL percentage = ROUND(count_4xx / TO_DOUBLE(count_4xx + count_rest), 3) * 100\\n| RENAME percentage as `HTTP 4xx (%)`\\n| DROP count_4xx, count_rest\"},\"columns\":[{\"columnId\":\"HTTP 4xx (%)\",\"fieldName\":\"HTTP 4xx (%)\",\"meta\":{\"type\":\"number\",\"esType\":\"double\"},\"inMetricDimension\":true}],\"timeField\":\"@timestamp\"}},\"indexPatternRefs\":[{\"id\":\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\",\"title\":\"kibana_sample_data_logs\",\"timeField\":\"@timestamp\"}]}},\"filters\":[],\"query\":{\"esql\":\"FROM kibana_sample_data_logs\\n| EVAL type = CASE(TO_INTEGER(response.keyword) >= 400 and TO_INTEGER(response.keyword) < 500, \\\"4xx\\\", \\\"Other\\\")\\n| STATS count = COUNT(*) by type\\n| EVAL count_4xx = CASE(type == \\\"4xx\\\", count), count_rest = CASE(type == \\\"Other\\\", count)\\n| DROP count, type\\n| STATS count_4xx = MAX(count_4xx), count_rest = MAX(count_rest)\\n| EVAL percentage = ROUND(count_4xx / TO_DOUBLE(count_4xx + count_rest), 3) * 100\\n| RENAME percentage as `HTTP 4xx (%)`\\n| DROP count_4xx, count_rest\"},\"visualization\":{\"layerId\":\"bc54f0c9-40ec-4082-92f5-3d6a6fdc4463\",\"layerType\":\"data\",\"metricAccessor\":\"HTTP 4xx (%)\"},\"adHocDataViews\":{\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\":{\"id\":\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\",\"title\":\"kibana_sample_data_logs\",\"timeFieldName\":\"@timestamp\",\"sourceFilters\":[],\"type\":\"esql\",\"fieldFormats\":{},\"runtimeFieldMap\":{},\"allowNoIndex\":false,\"name\":\"kibana_sample_data_logs\",\"allowHidden\":false}}},\"visualizationType\":\"lnsMetric\",\"type\":\"lens\"},\"disabledActions\":[\"OPEN_FLYOUT_ADD_DRILLDOWN\"],\"hidePanelTitles\":true,\"enhancements\":{}},\"title\":\"count_4xx & count_rest & HTTP 4xx of (empty)\"},{\"type\":\"lens\",\"gridData\":{\"x\":35,\"y\":8,\"w\":12,\"h\":8,\"i\":\"08ff3801-880c-492d-8bd2-64987ebcd20e\"},\"panelIndex\":\"08ff3801-880c-492d-8bd2-64987ebcd20e\",\"embeddableConfig\":{\"attributes\":{\"title\":\"Metric\",\"references\":[],\"state\":{\"datasourceStates\":{\"textBased\":{\"layers\":{\"81ca7215-7fcf-4d72-98d7-911faade8c71\":{\"index\":\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\",\"query\":{\"esql\":\"FROM kibana_sample_data_logs\\n| EVAL type = CASE(TO_INTEGER(response.keyword) >= 500, \\\"5xx\\\", \\\"Other\\\")\\n| STATS count = COUNT(*) by type\\n| EVAL count_5xx = CASE(type == \\\"5xx\\\", count), count_rest = CASE(type == \\\"Other\\\", count)\\n| DROP count, type\\n| STATS count_5xx = MAX(count_5xx), count_rest = MAX(count_rest)\\n| EVAL percentage = ROUND(count_5xx / TO_DOUBLE(count_5xx + count_rest), 3) * 100\\n| RENAME percentage as `HTTP 5xx (%)`\\n| DROP count_5xx, count_rest\"},\"columns\":[{\"columnId\":\"HTTP 5xx (%)\",\"fieldName\":\"HTTP 5xx (%)\",\"meta\":{\"type\":\"number\",\"esType\":\"double\"},\"inMetricDimension\":true}],\"timeField\":\"@timestamp\"}},\"indexPatternRefs\":[{\"id\":\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\",\"title\":\"kibana_sample_data_logs\",\"timeField\":\"@timestamp\"}]}},\"filters\":[],\"query\":{\"esql\":\"FROM kibana_sample_data_logs\\n| EVAL type = CASE(TO_INTEGER(response.keyword) >= 500, \\\"5xx\\\", \\\"Other\\\")\\n| STATS count = COUNT(*) by type\\n| EVAL count_5xx = CASE(type == \\\"5xx\\\", count), count_rest = CASE(type == \\\"Other\\\", count)\\n| DROP count, type\\n| STATS count_5xx = MAX(count_5xx), count_rest = MAX(count_rest)\\n| EVAL percentage = ROUND(count_5xx / TO_DOUBLE(count_5xx + count_rest), 3) * 100\\n| RENAME percentage as `HTTP 5xx (%)`\\n| DROP count_5xx, count_rest\"},\"visualization\":{\"layerId\":\"81ca7215-7fcf-4d72-98d7-911faade8c71\",\"layerType\":\"data\",\"metricAccessor\":\"HTTP 5xx (%)\"},\"adHocDataViews\":{\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\":{\"id\":\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\",\"title\":\"kibana_sample_data_logs\",\"timeFieldName\":\"@timestamp\",\"sourceFilters\":[],\"type\":\"esql\",\"fieldFormats\":{},\"runtimeFieldMap\":{},\"allowNoIndex\":false,\"name\":\"kibana_sample_data_logs\",\"allowHidden\":false}}},\"visualizationType\":\"lnsMetric\",\"type\":\"lens\"},\"disabledActions\":[\"OPEN_FLYOUT_ADD_DRILLDOWN\"],\"hidePanelTitles\":true,\"enhancements\":{}},\"title\":\"count_4xx & count_rest & HTTP 4xx of (empty) (copy)\"},{\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":16,\"w\":24,\"h\":15,\"i\":\"b7c1b7cb-4e8b-44cd-b47b-624922ab4881\"},\"panelIndex\":\"b7c1b7cb-4e8b-44cd-b47b-624922ab4881\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}}},\"panelRefName\":\"panel_b7c1b7cb-4e8b-44cd-b47b-624922ab4881\"},{\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":16,\"w\":22,\"h\":38,\"i\":\"243222f6-1f49-4cfd-85eb-68c1028f06cf\"},\"panelIndex\":\"243222f6-1f49-4cfd-85eb-68c1028f06cf\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}}},\"panelRefName\":\"panel_243222f6-1f49-4cfd-85eb-68c1028f06cf\"},{\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":31,\"w\":24,\"h\":11,\"i\":\"3b5a152d-3747-4c1c-8e4f-00395008210f\"},\"panelIndex\":\"3b5a152d-3747-4c1c-8e4f-00395008210f\",\"embeddableConfig\":{\"attributes\":{\"title\":\"Unique Visits (Last hour) & Unique Visits (Total) & Bytes(Total - MB) & Bytes(Last hour - KB) of Type\",\"references\":[],\"state\":{\"datasourceStates\":{\"textBased\":{\"layers\":{\"60e6000b-e8d9-4143-b43c-f4c9b60ec127\":{\"index\":\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\",\"query\":{\"esql\":\"FROM kibana_sample_data_logs\\n| SORT @timestamp\\n| EVAL t = now()\\n| EVAL key = CASE(timestamp < t - 1 hour AND timestamp > t - 2 hour, \\\"Last hour\\\", \\\"Other\\\")\\n| STATS sum = SUM(bytes), count = COUNT_DISTINCT(clientip) by key, extension.keyword\\n| EVAL sum_last_hour = CASE(key == \\\"Last hour\\\", sum), sum_rest = CASE(key == \\\"Other\\\", sum), count_last_hour = CASE(key == \\\"Last hour\\\", count), count_rest = CASE(key == \\\"Other\\\", count)\\n| STATS sum_last_hour = MAX(sum_last_hour), sum_rest = MAX(sum_rest), count_last_hour = MAX(count_last_hour), count_rest = MAX(count_rest) by key, extension.keyword\\n| EVAL total_bytes = TO_DOUBLE(COALESCE(sum_last_hour, 0::LONG) + COALESCE(sum_rest, 0::LONG))\\n| EVAL total_visits = TO_DOUBLE(COALESCE(count_last_hour, 0::LONG) + COALESCE(count_rest, 0::LONG))\\n| EVAL bytes_transform = ROUND(total_bytes / 1000000.0, 1)\\n| EVAL bytes_transform_last_hour = ROUND(sum_last_hour / 1000.0, 2)\\n| KEEP count_last_hour, total_visits, bytes_transform, bytes_transform_last_hour, extension.keyword\\n| STATS count_last_hour = SUM(count_last_hour), total_visits = SUM(total_visits), bytes_transform = SUM(bytes_transform), bytes_transform_last_hour = SUM(bytes_transform_last_hour) BY extension.keyword\\n| RENAME total_visits as `Unique Visits (Total)`, count_last_hour as `Unique Visits (Last hour)`, bytes_transform as `Bytes(Total - MB)`, bytes_transform_last_hour as `Bytes(Last hour - KB)`, extension.keyword as `Type`\"},\"columns\":[{\"columnId\":\"Unique Visits (Last hour)\",\"fieldName\":\"Unique Visits (Last hour)\",\"meta\":{\"type\":\"number\",\"esType\":\"long\"},\"inMetricDimension\":true},{\"columnId\":\"Unique Visits (Total)\",\"fieldName\":\"Unique Visits (Total)\",\"meta\":{\"type\":\"number\",\"esType\":\"double\"},\"inMetricDimension\":true},{\"columnId\":\"Bytes(Total - MB)\",\"fieldName\":\"Bytes(Total - MB)\",\"meta\":{\"type\":\"number\",\"esType\":\"double\"},\"inMetricDimension\":true},{\"columnId\":\"Bytes(Last hour - KB)\",\"fieldName\":\"Bytes(Last hour - KB)\",\"meta\":{\"type\":\"number\",\"esType\":\"double\"},\"inMetricDimension\":true},{\"columnId\":\"Type\",\"fieldName\":\"Type\",\"meta\":{\"type\":\"string\",\"esType\":\"keyword\"},\"inMetricDimension\":true}],\"timeField\":\"@timestamp\"}},\"indexPatternRefs\":[{\"id\":\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\",\"title\":\"kibana_sample_data_logs\",\"timeField\":\"@timestamp\"}]}},\"filters\":[],\"query\":{\"esql\":\"FROM kibana_sample_data_logs\\n| SORT @timestamp\\n| EVAL t = now()\\n| EVAL key = CASE(timestamp < t - 1 hour AND timestamp > t - 2 hour, \\\"Last hour\\\", \\\"Other\\\")\\n| STATS sum = SUM(bytes), count = COUNT_DISTINCT(clientip) by key, extension.keyword\\n| EVAL sum_last_hour = CASE(key == \\\"Last hour\\\", sum), sum_rest = CASE(key == \\\"Other\\\", sum), count_last_hour = CASE(key == \\\"Last hour\\\", count), count_rest = CASE(key == \\\"Other\\\", count)\\n| STATS sum_last_hour = MAX(sum_last_hour), sum_rest = MAX(sum_rest), count_last_hour = MAX(count_last_hour), count_rest = MAX(count_rest) by key, extension.keyword\\n| EVAL total_bytes = TO_DOUBLE(COALESCE(sum_last_hour, 0::LONG) + COALESCE(sum_rest, 0::LONG))\\n| EVAL total_visits = TO_DOUBLE(COALESCE(count_last_hour, 0::LONG) + COALESCE(count_rest, 0::LONG))\\n| EVAL bytes_transform = ROUND(total_bytes / 1000000.0, 1)\\n| EVAL bytes_transform_last_hour = ROUND(sum_last_hour / 1000.0, 2)\\n| KEEP count_last_hour, total_visits, bytes_transform, bytes_transform_last_hour, extension.keyword\\n| STATS count_last_hour = SUM(count_last_hour), total_visits = SUM(total_visits), bytes_transform = SUM(bytes_transform), bytes_transform_last_hour = SUM(bytes_transform_last_hour) BY extension.keyword\\n| RENAME total_visits as `Unique Visits (Total)`, count_last_hour as `Unique Visits (Last hour)`, bytes_transform as `Bytes(Total - MB)`, bytes_transform_last_hour as `Bytes(Last hour - KB)`, extension.keyword as `Type`\"},\"visualization\":{\"layerId\":\"60e6000b-e8d9-4143-b43c-f4c9b60ec127\",\"layerType\":\"data\",\"columns\":[{\"columnId\":\"Unique Visits (Last hour)\",\"colorMode\":\"text\",\"palette\":{\"name\":\"custom\",\"type\":\"palette\",\"params\":{\"steps\":5,\"stops\":[{\"color\":\"#D23115\",\"stop\":10},{\"color\":\"#FCC400\",\"stop\":25},{\"color\":\"#68BC00\",\"stop\":26}],\"rangeType\":\"number\",\"rangeMin\":0,\"rangeMax\":null,\"continuity\":\"above\",\"colorStops\":[{\"color\":\"#D23115\",\"stop\":0},{\"color\":\"#FCC400\",\"stop\":10},{\"color\":\"#68BC00\",\"stop\":25}],\"name\":\"custom\"}}},{\"columnId\":\"Unique Visits (Total)\",\"colorMode\":\"text\",\"palette\":{\"name\":\"custom\",\"type\":\"palette\",\"params\":{\"steps\":5,\"stops\":[{\"color\":\"#D23115\",\"stop\":1000},{\"color\":\"#FCC400\",\"stop\":1500},{\"color\":\"#68BC00\",\"stop\":1501}],\"rangeType\":\"number\",\"rangeMin\":0,\"rangeMax\":null,\"continuity\":\"above\",\"colorStops\":[{\"color\":\"#D23115\",\"stop\":0},{\"color\":\"#FCC400\",\"stop\":1000},{\"color\":\"#68BC00\",\"stop\":1500}],\"name\":\"custom\"}}},{\"columnId\":\"Bytes(Total - MB)\"},{\"columnId\":\"Bytes(Last hour - KB)\"},{\"columnId\":\"Type\"}]},\"adHocDataViews\":{\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\":{\"id\":\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\",\"title\":\"kibana_sample_data_logs\",\"timeFieldName\":\"@timestamp\",\"sourceFilters\":[],\"type\":\"esql\",\"fieldFormats\":{},\"runtimeFieldMap\":{},\"allowNoIndex\":false,\"name\":\"kibana_sample_data_logs\",\"allowHidden\":false}}},\"visualizationType\":\"lnsDatatable\",\"type\":\"lens\"},\"disabledActions\":[\"OPEN_FLYOUT_ADD_DRILLDOWN\"],\"hidePanelTitles\":true,\"enhancements\":{}},\"title\":\"SUM(bytes) & Unique Visits (Total - MB) & Bytes(Total) of Type\"},{\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":42,\"w\":24,\"h\":12,\"i\":\"26ecb41a-f605-4bc9-8c84-7936ce98f348\"},\"panelIndex\":\"26ecb41a-f605-4bc9-8c84-7936ce98f348\",\"embeddableConfig\":{\"hidePanelTitles\":false,\"enhancements\":{},\"attributes\":{\"title\":\"[Logs] Bytes distribution\",\"visualizationType\":\"lnsXY\",\"state\":{\"datasourceStates\":{\"formBased\":{\"layers\":{\"7d9a32b1-8cc2-410c-83a5-2eb66a3f0321\":{\"columnOrder\":[\"a8511a62-2b78-4ba4-9425-a417df6e059f\",\"b5f3dc78-dba8-4db8-87b6-24a0b9cca260\",\"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X0\",\"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X1\",\"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X2\",\"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X3\"],\"columns\":{\"a8511a62-2b78-4ba4-9425-a417df6e059f\":{\"dataType\":\"number\",\"isBucketed\":true,\"label\":\"bytes\",\"operationType\":\"range\",\"params\":{\"maxBars\":\"auto\",\"ranges\":[{\"from\":0,\"label\":\"\",\"to\":1000}],\"type\":\"histogram\"},\"scale\":\"interval\",\"sourceField\":\"bytes\"},\"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X0\":{\"label\":\"Part of % of visits\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"params\":{\"emptyAsNull\":false},\"customLabel\":true},\"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X1\":{\"label\":\"Part of % of visits\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"params\":{\"emptyAsNull\":false},\"customLabel\":true},\"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X2\":{\"label\":\"Part of % of visits\",\"dataType\":\"number\",\"operationType\":\"overall_sum\",\"isBucketed\":false,\"scale\":\"ratio\",\"references\":[\"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X1\"],\"customLabel\":true},\"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X3\":{\"label\":\"Part of % of visits\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"divide\",\"args\":[\"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X0\",\"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X2\"],\"location\":{\"min\":0,\"max\":30},\"text\":\"count() / overall_sum(count())\"}},\"references\":[\"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X0\",\"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X2\"],\"customLabel\":true},\"b5f3dc78-dba8-4db8-87b6-24a0b9cca260\":{\"customLabel\":true,\"dataType\":\"number\",\"isBucketed\":false,\"label\":\"% of visits\",\"operationType\":\"formula\",\"params\":{\"format\":{\"id\":\"percent\",\"params\":{\"decimals\":1}},\"formula\":\"count() / overall_sum(count())\",\"isFormulaBroken\":false},\"references\":[\"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X3\"],\"scale\":\"ratio\"}},\"incompleteColumns\":{},\"indexPatternId\":\"90943e30-9a47-11e8-b64d-95841ca0b247\"}},\"currentIndexPatternId\":\"90943e30-9a47-11e8-b64d-95841ca0b247\"}},\"filters\":[],\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"visualization\":{\"axisTitlesVisibilitySettings\":{\"x\":false,\"yLeft\":false,\"yRight\":true},\"fittingFunction\":\"None\",\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"layers\":[{\"accessors\":[\"b5f3dc78-dba8-4db8-87b6-24a0b9cca260\"],\"layerId\":\"7d9a32b1-8cc2-410c-83a5-2eb66a3f0321\",\"position\":\"top\",\"seriesType\":\"bar_stacked\",\"showGridlines\":false,\"xAccessor\":\"a8511a62-2b78-4ba4-9425-a417df6e059f\",\"layerType\":\"data\"}],\"legend\":{\"isVisible\":true,\"position\":\"right\",\"legendSize\":\"auto\"},\"preferredSeriesType\":\"bar_stacked\",\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"valueLabels\":\"hide\",\"yLeftExtent\":{\"mode\":\"full\"},\"yRightExtent\":{\"mode\":\"full\"}}},\"references\":[{\"type\":\"index-pattern\",\"id\":\"90943e30-9a47-11e8-b64d-95841ca0b247\",\"name\":\"indexpattern-datasource-layer-7d9a32b1-8cc2-410c-83a5-2eb66a3f0321\"}],\"type\":\"lens\",\"savedObjectId\":\"16b1d7d0-ea71-11eb-8b4b-f7b600de0f7d\"}},\"title\":\"[Logs] Bytes distribution\"},{\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":54,\"w\":48,\"h\":13,\"i\":\"ef74e09a-4bf9-4a38-8251-e754404871fd\"},\"panelIndex\":\"ef74e09a-4bf9-4a38-8251-e754404871fd\",\"embeddableConfig\":{\"attributes\":{\"title\":\"Unique & 95th percentile of bytes & Median of bytes & count_5xx of url.keyword\",\"references\":[],\"state\":{\"datasourceStates\":{\"textBased\":{\"layers\":{\"8d5e3fae-adad-4bfc-8ec2-a1926a447315\":{\"index\":\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\",\"query\":{\"esql\":\"FROM kibana_sample_data_logs\\n| KEEP bytes, clientip, url.keyword, response.keyword\\n| EVAL type = CASE(TO_INTEGER(response.keyword) >= 400 and TO_INTEGER(response.keyword) < 500, \\\"4xx\\\", TO_INTEGER(response.keyword) >= 500, \\\"5xx\\\", \\\"Other\\\")\\n| STATS Visits = COUNT(), Unique = COUNT_DISTINCT(clientip), p95 = percentile(bytes, 95), median = median(bytes) by type, url.keyword\\n| EVAL count_4xx = CASE(type == \\\"4xx\\\", Visits), count_5xx = CASE(type == \\\"5xx\\\", Visits), count_rest = CASE(type == \\\"Other\\\", Visits)\\n| STATS count_4xx = SUM(count_4xx), count_5xx = SUM(count_5xx), count_rest = SUM(count_rest), Unique = SUM(Unique),`95th percentile of bytes` = MAX(p95), `Median of bytes` = MAX(median) BY url.keyword\\n| EVAL count_4xx = COALESCE(count_4xx, 0::LONG), count_5xx = COALESCE(count_5xx, 0::LONG), count_rest = COALESCE(count_rest, 0::LONG)\\n| EVAL total_records = TO_DOUBLE(count_4xx + count_5xx + count_rest)\\n| EVAL percentage_4xx = count_4xx / total_records, percentage_5xx = count_5xx / total_records\\n| EVAL percentage_4xx = ROUND(100 * percentage_4xx, 2)\\n| EVAL percentage_5xx = ROUND(100 * percentage_5xx, 2)\\n| DROP count_4xx, count_rest, total_records\\n| RENAME percentage_4xx as `HTTP 4xx`, percentage_5xx as `HTTP 5xx`\"},\"columns\":[{\"columnId\":\"Unique\",\"fieldName\":\"Unique\",\"meta\":{\"type\":\"number\",\"esType\":\"long\"},\"inMetricDimension\":true},{\"columnId\":\"95th percentile of bytes\",\"fieldName\":\"95th percentile of bytes\",\"meta\":{\"type\":\"number\",\"esType\":\"double\"},\"inMetricDimension\":true},{\"columnId\":\"Median of bytes\",\"fieldName\":\"Median of bytes\",\"meta\":{\"type\":\"number\",\"esType\":\"double\"},\"inMetricDimension\":true},{\"columnId\":\"url.keyword\",\"fieldName\":\"url.keyword\",\"meta\":{\"type\":\"string\",\"esType\":\"keyword\"},\"inMetricDimension\":true},{\"columnId\":\"count_5xx\",\"fieldName\":\"HTTP 5xx\",\"meta\":{\"type\":\"number\",\"esType\":\"double\"},\"inMetricDimension\":true},{\"columnId\":\"0089839c-ab91-420e-bdec-9583ce4d6015\",\"fieldName\":\"HTTP 4xx\",\"meta\":{\"type\":\"number\",\"esType\":\"double\"}}],\"timeField\":\"@timestamp\"}},\"indexPatternRefs\":[{\"id\":\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\",\"title\":\"kibana_sample_data_logs\",\"timeField\":\"@timestamp\"}]}},\"filters\":[],\"query\":{\"esql\":\"FROM kibana_sample_data_logs\\n| KEEP bytes, clientip, url.keyword, response.keyword\\n| EVAL type = CASE(TO_INTEGER(response.keyword) >= 400 and TO_INTEGER(response.keyword) < 500, \\\"4xx\\\", TO_INTEGER(response.keyword) >= 500, \\\"5xx\\\", \\\"Other\\\")\\n| STATS Visits = COUNT(), Unique = COUNT_DISTINCT(clientip), p95 = percentile(bytes, 95), median = median(bytes) by type, url.keyword\\n| EVAL count_4xx = CASE(type == \\\"4xx\\\", Visits), count_5xx = CASE(type == \\\"5xx\\\", Visits), count_rest = CASE(type == \\\"Other\\\", Visits)\\n| STATS count_4xx = SUM(count_4xx), count_5xx = SUM(count_5xx), count_rest = SUM(count_rest), Unique = SUM(Unique),`95th percentile of bytes` = MAX(p95), `Median of bytes` = MAX(median) BY url.keyword\\n| EVAL count_4xx = COALESCE(count_4xx, 0::LONG), count_5xx = COALESCE(count_5xx, 0::LONG), count_rest = COALESCE(count_rest, 0::LONG)\\n| EVAL total_records = TO_DOUBLE(count_4xx + count_5xx + count_rest)\\n| EVAL percentage_4xx = count_4xx / total_records, percentage_5xx = count_5xx / total_records\\n| EVAL percentage_4xx = ROUND(100 * percentage_4xx, 2)\\n| EVAL percentage_5xx = ROUND(100 * percentage_5xx, 2)\\n| DROP count_4xx, count_rest, total_records\\n| RENAME percentage_4xx as `HTTP 4xx`, percentage_5xx as `HTTP 5xx`\"},\"visualization\":{\"layerId\":\"8d5e3fae-adad-4bfc-8ec2-a1926a447315\",\"layerType\":\"data\",\"columns\":[{\"columnId\":\"Unique\"},{\"columnId\":\"95th percentile of bytes\"},{\"columnId\":\"Median of bytes\"},{\"columnId\":\"url.keyword\"},{\"columnId\":\"count_5xx\",\"colorMode\":\"cell\",\"palette\":{\"name\":\"custom\",\"type\":\"palette\",\"params\":{\"steps\":5,\"stops\":[{\"color\":\"#FBDDD6\",\"stop\":10},{\"color\":\"#CC5642\",\"stop\":100}],\"rangeType\":\"number\",\"rangeMin\":5,\"rangeMax\":null,\"continuity\":\"above\",\"colorStops\":[{\"color\":\"#FBDDD6\",\"stop\":5},{\"color\":\"#CC5642\",\"stop\":10}],\"name\":\"custom\"}}},{\"columnId\":\"0089839c-ab91-420e-bdec-9583ce4d6015\",\"isTransposed\":false,\"isMetric\":true,\"colorMode\":\"cell\",\"palette\":{\"name\":\"custom\",\"type\":\"palette\",\"params\":{\"steps\":5,\"stops\":[{\"color\":\"#FBDDD6\",\"stop\":10},{\"color\":\"#CC5642\",\"stop\":100}],\"rangeType\":\"number\",\"rangeMin\":0.05,\"rangeMax\":null,\"continuity\":\"above\",\"colorStops\":[{\"color\":\"#FBDDD6\",\"stop\":0.05},{\"color\":\"#CC5642\",\"stop\":10}],\"name\":\"custom\"}}}]},\"adHocDataViews\":{\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\":{\"id\":\"e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a\",\"title\":\"kibana_sample_data_logs\",\"timeFieldName\":\"@timestamp\",\"sourceFilters\":[],\"type\":\"esql\",\"fieldFormats\":{},\"runtimeFieldMap\":{},\"allowNoIndex\":false,\"name\":\"kibana_sample_data_logs\",\"allowHidden\":false}}},\"visualizationType\":\"lnsDatatable\",\"type\":\"lens\"},\"disabledActions\":[\"OPEN_FLYOUT_ADD_DRILLDOWN\"],\"enhancements\":{}}}]", + "refreshInterval": { + "pause": false, + "value": 900000 + }, + "timeTo": "2022-10-26T09:00:00.000Z", + "timeFrom": "2022-10-19T09:00:00.000Z", + "timeRestore": true, + "title": "Logs dashboard with ES|QL" + }, + "managed": false, + "coreMigrationVersion": "8.8.0", + "typeMigrationVersion": "10.2.0", + "updated_at": "2024-09-18T12:44:30.513Z", + "created_at": "2024-09-17T10:56:39.390Z", + "id": "edf84fe0-e1a0-11e7-b6d5-4dc382ef7f5b", + "references": [ + { + "name": "b7c1b7cb-4e8b-44cd-b47b-624922ab4881:panel_b7c1b7cb-4e8b-44cd-b47b-624922ab4881", + "type": "visualization", + "id": "cb099a20-ea66-11eb-9425-113343a037e3" + }, + { + "name": "243222f6-1f49-4cfd-85eb-68c1028f06cf:panel_243222f6-1f49-4cfd-85eb-68c1028f06cf", + "type": "visualization", + "id": "7cbd2350-2223-11e8-b802-5bcf64c2cfb4" + }, + { + "type": "index-pattern", + "id": "90943e30-9a47-11e8-b64d-95841ca0b247", + "name": "26ecb41a-f605-4bc9-8c84-7936ce98f348:indexpattern-datasource-layer-7d9a32b1-8cc2-410c-83a5-2eb66a3f0321" + }, + { + "id": "90943e30-9a47-11e8-b64d-95841ca0b247", + "name": "controlGroup_612f8db8-9ba9-41cf-a809-d133fe9b83a8:optionsListDataView", + "type": "index-pattern" + }, + { + "id": "90943e30-9a47-11e8-b64d-95841ca0b247", + "name": "controlGroup_9807212f-5078-4c42-879c-6f28b3033fc9:optionsListDataView", + "type": "index-pattern" + }, + { + "id": "90943e30-9a47-11e8-b64d-95841ca0b247", + "name": "controlGroup_6bf7a1b4-282e-43ac-aa46-81b97fa3acae:rangeSliderDataView", + "type": "index-pattern" + } + ], + "type": "dashboard", + "version": "WzIyNiwxXQ==" +} diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/bulk_disable/bulk_disable_rules_route.ts b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_disable/bulk_disable_rules_route.ts index 39b81ee74fe9..07ea4f54cf82 100644 --- a/x-pack/plugins/alerting/server/routes/rule/apis/bulk_disable/bulk_disable_rules_route.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_disable/bulk_disable_rules_route.ts @@ -6,9 +6,9 @@ */ import { IRouter } from '@kbn/core/server'; -import { verifyAccessAndContext, handleDisabledApiKeysError } from '../../../lib'; import { ILicenseState, RuleTypeDisabledError } from '../../../../lib'; import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../../../types'; +import { handleDisabledApiKeysError, verifyAccessAndContext } from '../../../lib'; import { bulkDisableRulesRequestBodySchemaV1, @@ -16,8 +16,8 @@ import { BulkDisableRulesResponseV1, } from '../../../../../common/routes/rule/apis/bulk_disable'; import type { RuleParamsV1 } from '../../../../../common/routes/rule/response'; -import { transformRuleToRuleResponseV1 } from '../../transforms'; import { Rule } from '../../../../application/rule/types'; +import { transformRuleToRuleResponseV1 } from '../../transforms'; export const bulkDisableRulesRoute = ({ router, diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/bulk_edit_rules_route.ts b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/bulk_edit_rules_route.ts index c863d2aba33c..b81675ee85d3 100644 --- a/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/bulk_edit_rules_route.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/bulk_edit_rules_route.ts @@ -8,20 +8,20 @@ import { IRouter } from '@kbn/core/server'; import { ILicenseState, RuleTypeDisabledError } from '../../../../lib'; -import { verifyAccessAndContext, handleDisabledApiKeysError } from '../../../lib'; import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../../../types'; +import { handleDisabledApiKeysError, verifyAccessAndContext } from '../../../lib'; import { bulkEditRulesRequestBodySchemaV1, BulkEditRulesRequestBodyV1, BulkEditRulesResponseV1, } from '../../../../../common/routes/rule/apis/bulk_edit'; -import { Rule } from '../../../../application/rule/types'; import type { RuleParamsV1 } from '../../../../../common/routes/rule/response'; +import { Rule } from '../../../../application/rule/types'; import { transformRuleToRuleResponseV1 } from '../../transforms'; -import { transformOperationsV1 } from './transforms'; import { validateRequiredGroupInDefaultActionsV1 } from '../../validation'; +import { transformOperationsV1 } from './transforms'; interface BuildBulkEditRulesRouteParams { licenseState: ILicenseState; diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/create/create_rule_route.ts b/x-pack/plugins/alerting/server/routes/rule/apis/create/create_rule_route.ts index 0edf062069e4..4e778cdf97a8 100644 --- a/x-pack/plugins/alerting/server/routes/rule/apis/create/create_rule_route.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/create/create_rule_route.ts @@ -5,13 +5,6 @@ * 2.0. */ -import { RuleTypeDisabledError } from '../../../../lib'; -import { - handleDisabledApiKeysError, - verifyAccessAndContext, - countUsageOfPredefinedIds, -} from '../../../lib'; -import { BASE_ALERTING_API_PATH } from '../../../../types'; import { RouteOptions } from '../../..'; import type { CreateRuleRequestBodyV1, @@ -24,9 +17,16 @@ import { } from '../../../../../common/routes/rule/apis/create'; import { RuleParamsV1, ruleResponseSchemaV1 } from '../../../../../common/routes/rule/response'; import { Rule } from '../../../../application/rule/types'; -import { transformCreateBodyV1 } from './transforms'; +import { RuleTypeDisabledError } from '../../../../lib'; +import { BASE_ALERTING_API_PATH } from '../../../../types'; +import { + countUsageOfPredefinedIds, + handleDisabledApiKeysError, + verifyAccessAndContext, +} from '../../../lib'; import { transformRuleToRuleResponseV1 } from '../../transforms'; import { validateRequiredGroupInDefaultActionsV1 } from '../../validation'; +import { transformCreateBodyV1 } from './transforms'; export const createRuleRoute = ({ router, licenseState, usageCounter }: RouteOptions) => { router.post( @@ -47,6 +47,15 @@ export const createRuleRoute = ({ router, licenseState, usageCounter }: RouteOpt body: () => ruleResponseSchemaV1, description: 'Indicates a successful call.', }, + 400: { + description: 'Indicates an invalid schema or parameters.', + }, + 403: { + description: 'Indicates that this call is forbidden.', + }, + 409: { + description: 'Indicates that the rule id is already in use.', + }, }, }, }, diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/delete/delete_rule_route.ts b/x-pack/plugins/alerting/server/routes/rule/apis/delete/delete_rule_route.ts index 5015cac45858..0452de7f7bec 100644 --- a/x-pack/plugins/alerting/server/routes/rule/apis/delete/delete_rule_route.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/delete/delete_rule_route.ts @@ -34,6 +34,15 @@ export const deleteRuleRoute = ( 204: { description: 'Indicates a successful call.', }, + 400: { + description: 'Indicates an invalid schema or parameters.', + }, + 403: { + description: 'Indicates that this call is forbidden.', + }, + 404: { + description: 'Indicates a rule with the given ID does not exist.', + }, }, }, }, diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/disable/disable_rule_route.ts b/x-pack/plugins/alerting/server/routes/rule/apis/disable/disable_rule_route.ts index 7b8695a58828..cae82e80be86 100644 --- a/x-pack/plugins/alerting/server/routes/rule/apis/disable/disable_rule_route.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/disable/disable_rule_route.ts @@ -7,14 +7,14 @@ import { IRouter } from '@kbn/core/server'; import { - disableRuleRequestParamsSchemaV1, - disableRuleRequestBodySchemaV1, - DisableRuleRequestParamsV1, DisableRuleRequestBodyV1, + DisableRuleRequestParamsV1, + disableRuleRequestBodySchemaV1, + disableRuleRequestParamsSchemaV1, } from '../../../../../common/routes/rule/apis/disable'; import { ILicenseState, RuleTypeDisabledError } from '../../../../lib'; -import { verifyAccessAndContext } from '../../../lib'; import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../../../../types'; +import { verifyAccessAndContext } from '../../../lib'; export const disableRuleRoute = ( router: IRouter, @@ -37,6 +37,15 @@ export const disableRuleRoute = ( 204: { description: 'Indicates a successful call.', }, + 400: { + description: 'Indicates an invalid schema.', + }, + 403: { + description: 'Indicates that this call is forbidden.', + }, + 404: { + description: 'Indicates a rule with the given ID does not exist.', + }, }, }, }, diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/enable/enable_rule_route.ts b/x-pack/plugins/alerting/server/routes/rule/apis/enable/enable_rule_route.ts index 2891b627e2de..4843ed932374 100644 --- a/x-pack/plugins/alerting/server/routes/rule/apis/enable/enable_rule_route.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/enable/enable_rule_route.ts @@ -7,14 +7,13 @@ import { IRouter } from '@kbn/core/server'; import { ILicenseState, RuleTypeDisabledError } from '../../../../lib'; -import { verifyAccessAndContext } from '../../../lib'; import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../../../../types'; +import { verifyAccessAndContext } from '../../../lib'; import { EnableRuleRequestParamsV1, enableRuleRequestParamsSchemaV1, } from '../../../../../common/routes/rule/apis/enable'; - export const enableRuleRoute = ( router: IRouter, licenseState: ILicenseState @@ -35,6 +34,15 @@ export const enableRuleRoute = ( 204: { description: 'Indicates a successful call.', }, + 400: { + description: 'Indicates an invalid schema or parameters.', + }, + 403: { + description: 'Indicates that this call is forbidden.', + }, + 404: { + description: 'Indicates a rule with the given ID does not exist.', + }, }, }, }, diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/find/find_rules_route.ts b/x-pack/plugins/alerting/server/routes/rule/apis/find/find_rules_route.ts index 3541e8e31475..fbfaf65a8024 100644 --- a/x-pack/plugins/alerting/server/routes/rule/apis/find/find_rules_route.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/find/find_rules_route.ts @@ -55,6 +55,12 @@ const buildFindRulesRoute = ({ body: () => ruleResponseSchemaV1, description: 'Indicates a successful call.', }, + 400: { + description: 'Indicates an invalid schema or parameters.', + }, + 403: { + description: 'Indicates that this call is forbidden.', + }, }, }, }, diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/get/get_rule_route.ts b/x-pack/plugins/alerting/server/routes/rule/apis/get/get_rule_route.ts index d6796250ea8a..fa68d00eb3c8 100644 --- a/x-pack/plugins/alerting/server/routes/rule/apis/get/get_rule_route.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/get/get_rule_route.ts @@ -50,6 +50,15 @@ const buildGetRuleRoute = ({ body: () => ruleResponseSchemaV1, description: 'Indicates a successful call.', }, + 400: { + description: 'Indicates an invalid schema or parameters.', + }, + 403: { + description: 'Indicates that this call is forbidden.', + }, + 404: { + description: 'Indicates a rule with the given ID does not exist.', + }, }, }, }, diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/mute_alert/mute_alert.ts b/x-pack/plugins/alerting/server/routes/rule/apis/mute_alert/mute_alert.ts index 41c7a9368273..5ce924b445ca 100644 --- a/x-pack/plugins/alerting/server/routes/rule/apis/mute_alert/mute_alert.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/mute_alert/mute_alert.ts @@ -34,6 +34,15 @@ export const muteAlertRoute = ( 204: { description: 'Indicates a successful call.', }, + 400: { + description: 'Indicates an invalid schema or parameters.', + }, + 403: { + description: 'Indicates that this call is forbidden.', + }, + 404: { + description: 'Indicates a rule or alert with the given ID does not exist.', + }, }, }, }, diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/unmute_alert/unmute_alert_route.ts b/x-pack/plugins/alerting/server/routes/rule/apis/unmute_alert/unmute_alert_route.ts index 3158778f72eb..63560e354dc1 100644 --- a/x-pack/plugins/alerting/server/routes/rule/apis/unmute_alert/unmute_alert_route.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/unmute_alert/unmute_alert_route.ts @@ -6,13 +6,13 @@ */ import { IRouter } from '@kbn/core/server'; -import { ILicenseState, RuleTypeDisabledError } from '../../../../lib'; -import { verifyAccessAndContext } from '../../../lib'; -import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../../../../types'; import { - unmuteAlertParamsSchemaV1, UnmuteAlertRequestParamsV1, + unmuteAlertParamsSchemaV1, } from '../../../../../common/routes/rule/apis/unmute_alert'; +import { ILicenseState, RuleTypeDisabledError } from '../../../../lib'; +import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../../../../types'; +import { verifyAccessAndContext } from '../../../lib'; import { transformRequestParamsToApplicationV1 } from './transforms'; export const unmuteAlertRoute = ( @@ -35,6 +35,15 @@ export const unmuteAlertRoute = ( 204: { description: 'Indicates a successful call.', }, + 400: { + description: 'Indicates an invalid schema or parameters.', + }, + 403: { + description: 'Indicates that this call is forbidden.', + }, + 404: { + description: 'Indicates a rule or alert with the given ID does not exist.', + }, }, }, }, diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/update/update_rule_route.ts b/x-pack/plugins/alerting/server/routes/rule/apis/update/update_rule_route.ts index 0fe2d9dae41c..507f748351ed 100644 --- a/x-pack/plugins/alerting/server/routes/rule/apis/update/update_rule_route.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/update/update_rule_route.ts @@ -6,8 +6,6 @@ */ import { IRouter } from '@kbn/core/server'; -import { ILicenseState, RuleTypeDisabledError } from '../../../../lib'; -import { verifyAccessAndContext, handleDisabledApiKeysError } from '../../../lib'; import type { UpdateRuleRequestBodyV1, UpdateRuleRequestParamsV1, @@ -18,11 +16,13 @@ import { updateParamsSchemaV1, } from '../../../../../common/routes/rule/apis/update'; import { RuleParamsV1, ruleResponseSchemaV1 } from '../../../../../common/routes/rule/response'; -import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../../../../types'; import { Rule } from '../../../../application/rule/types'; -import { transformUpdateBodyV1 } from './transforms'; +import { ILicenseState, RuleTypeDisabledError } from '../../../../lib'; +import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../../../../types'; +import { handleDisabledApiKeysError, verifyAccessAndContext } from '../../../lib'; import { transformRuleToRuleResponseV1 } from '../../transforms'; import { validateRequiredGroupInDefaultActionsV1 } from '../../validation'; +import { transformUpdateBodyV1 } from './transforms'; export const updateRuleRoute = ( router: IRouter, @@ -46,6 +46,18 @@ export const updateRuleRoute = ( body: () => ruleResponseSchemaV1, description: 'Indicates a successful call.', }, + 400: { + description: 'Indicates an invalid schema or parameters.', + }, + 403: { + description: 'Indicates that this call is forbidden.', + }, + 404: { + description: 'Indicates a rule with the given ID does not exist.', + }, + 409: { + description: 'Indicates that the rule has already been updated by another user.', + }, }, }, }, diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/update_api_key/update_rule_api_key_route.ts b/x-pack/plugins/alerting/server/routes/rule/apis/update_api_key/update_rule_api_key_route.ts index ab5c92332134..7ba858941235 100644 --- a/x-pack/plugins/alerting/server/routes/rule/apis/update_api_key/update_rule_api_key_route.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/update_api_key/update_rule_api_key_route.ts @@ -11,8 +11,8 @@ import { updateApiKeyParamsSchemaV1, } from '../../../../../common/routes/rule/apis/update_api_key'; import { ILicenseState, RuleTypeDisabledError } from '../../../../lib'; -import { verifyAccessAndContext } from '../../../lib'; import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../../../../types'; +import { verifyAccessAndContext } from '../../../lib'; export const updateRuleApiKeyRoute = ( router: IRouter, @@ -34,6 +34,18 @@ export const updateRuleApiKeyRoute = ( 204: { description: 'Indicates a successful call.', }, + 400: { + description: 'Indicates an invalid schema or parameters.', + }, + 403: { + description: 'Indicates that this call is forbidden.', + }, + 404: { + description: 'Indicates a rule with the given ID does not exist.', + }, + 409: { + description: 'Indicates that the rule has already been updated by another user.', + }, }, }, }, diff --git a/x-pack/plugins/cloud/common/constants.ts b/x-pack/plugins/cloud/common/constants.ts index f2009223f8ac..27d755ab0821 100644 --- a/x-pack/plugins/cloud/common/constants.ts +++ b/x-pack/plugins/cloud/common/constants.ts @@ -11,3 +11,5 @@ export const ELASTIC_SUPPORT_LINK = 'https://support.elastic.co/'; * This is the page for managing your snapshots on Cloud. */ export const CLOUD_SNAPSHOTS_PATH = 'elasticsearch/snapshots/'; + +export const ELASTICSEARCH_CONFIG_ROUTE = '/api/internal/cloud/elasticsearch_config'; diff --git a/x-pack/plugins/cloud/common/types.ts b/x-pack/plugins/cloud/common/types.ts index ac4593fd0e25..0f72caf51505 100644 --- a/x-pack/plugins/cloud/common/types.ts +++ b/x-pack/plugins/cloud/common/types.ts @@ -6,3 +6,7 @@ */ export type OnBoardingDefaultSolution = 'es' | 'oblt' | 'security'; + +export interface ElasticsearchConfigType { + elasticsearch_url?: string; +} diff --git a/x-pack/plugins/cloud/public/mocks.tsx b/x-pack/plugins/cloud/public/mocks.tsx index dd5c5eced618..b9f6d850b9ac 100644 --- a/x-pack/plugins/cloud/public/mocks.tsx +++ b/x-pack/plugins/cloud/public/mocks.tsx @@ -19,7 +19,9 @@ function createSetupMock(): jest.Mocked { deploymentUrl: 'deployment-url', profileUrl: 'profile-url', organizationUrl: 'organization-url', - elasticsearchUrl: 'elasticsearch-url', + fetchElasticsearchConfig: jest + .fn() + .mockResolvedValue({ elasticsearchUrl: 'elasticsearch-url' }), kibanaUrl: 'kibana-url', cloudHost: 'cloud-host', cloudDefaultPort: '443', @@ -53,6 +55,7 @@ const createStartMock = (): jest.Mocked => ({ serverless: { projectId: undefined, }, + fetchElasticsearchConfig: jest.fn().mockResolvedValue({ elasticsearchUrl: 'elasticsearch-url' }), }); export const cloudMock = { diff --git a/x-pack/plugins/cloud/public/plugin.test.ts b/x-pack/plugins/cloud/public/plugin.test.ts index 2c32ac8fe972..583d274db77d 100644 --- a/x-pack/plugins/cloud/public/plugin.test.ts +++ b/x-pack/plugins/cloud/public/plugin.test.ts @@ -37,6 +37,7 @@ describe('Cloud Plugin', () => { const plugin = new CloudPlugin(initContext); const coreSetup = coreMock.createSetup(); + coreSetup.http.get.mockResolvedValue({ elasticsearch_url: 'elasticsearch-url' }); const setup = plugin.setup(coreSetup); return { setup }; @@ -110,8 +111,8 @@ describe('Cloud Plugin', () => { it('exposes components decoded from the cloudId', () => { const decodedId: DecodedCloudId = { defaultPort: '9000', - host: 'host', elasticsearchUrl: 'elasticsearch-url', + host: 'host', kibanaUrl: 'kibana-url', }; decodeCloudIdMock.mockReturnValue(decodedId); @@ -120,7 +121,6 @@ describe('Cloud Plugin', () => { expect.objectContaining({ cloudDefaultPort: '9000', cloudHost: 'host', - elasticsearchUrl: 'elasticsearch-url', kibanaUrl: 'kibana-url', }) ); @@ -184,6 +184,11 @@ describe('Cloud Plugin', () => { }); expect(setup.serverless.projectType).toBe('security'); }); + it('exposes fetchElasticsearchConfig', async () => { + const { setup } = setupPlugin(); + const result = await setup.fetchElasticsearchConfig(); + expect(result).toEqual({ elasticsearchUrl: 'elasticsearch-url' }); + }); }); }); @@ -307,5 +312,13 @@ describe('Cloud Plugin', () => { const start = plugin.start(coreStart); expect(start.serverless.projectName).toBe('My Awesome Project'); }); + it('exposes fetchElasticsearchConfig', async () => { + const { plugin } = startPlugin(); + const coreStart = coreMock.createStart(); + coreStart.http.get.mockResolvedValue({ elasticsearch_url: 'elasticsearch-url' }); + const start = plugin.start(coreStart); + const result = await start.fetchElasticsearchConfig(); + expect(result).toEqual({ elasticsearchUrl: 'elasticsearch-url' }); + }); }); }); diff --git a/x-pack/plugins/cloud/public/plugin.tsx b/x-pack/plugins/cloud/public/plugin.tsx index a66193395506..e89e63dc1c15 100644 --- a/x-pack/plugins/cloud/public/plugin.tsx +++ b/x-pack/plugins/cloud/public/plugin.tsx @@ -12,12 +12,13 @@ import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kb import { registerCloudDeploymentMetadataAnalyticsContext } from '../common/register_cloud_deployment_id_analytics_context'; import { getIsCloudEnabled } from '../common/is_cloud_enabled'; import { parseDeploymentIdFromDeploymentUrl } from '../common/parse_deployment_id_from_deployment_url'; -import { CLOUD_SNAPSHOTS_PATH } from '../common/constants'; +import { CLOUD_SNAPSHOTS_PATH, ELASTICSEARCH_CONFIG_ROUTE } from '../common/constants'; import { decodeCloudId, type DecodedCloudId } from '../common/decode_cloud_id'; import { getFullCloudUrl } from '../common/utils'; import { parseOnboardingSolution } from '../common/parse_onboarding_default_solution'; -import type { CloudSetup, CloudStart } from './types'; +import type { CloudSetup, CloudStart, PublicElasticsearchConfigType } from './types'; import { getSupportUrl } from './utils'; +import { ElasticsearchConfigType } from '../common/types'; export interface CloudConfigType { id?: string; @@ -66,12 +67,14 @@ export class CloudPlugin implements Plugin { private readonly isServerlessEnabled: boolean; private readonly contextProviders: Array>> = []; private readonly logger: Logger; + private elasticsearchConfig?: PublicElasticsearchConfigType; constructor(private readonly initializerContext: PluginInitializerContext) { this.config = this.initializerContext.config.get(); this.isCloudEnabled = getIsCloudEnabled(this.config.id); this.isServerlessEnabled = !!this.config.serverless?.project_id; this.logger = initializerContext.logger.get(); + this.elasticsearchConfig = undefined; } public setup(core: CoreSetup): CloudSetup { @@ -99,7 +102,6 @@ export class CloudPlugin implements Plugin { csp, baseUrl, ...this.getCloudUrls(), - elasticsearchUrl: decodedId?.elasticsearchUrl, kibanaUrl: decodedId?.kibanaUrl, cloudHost: decodedId?.host, cloudDefaultPort: decodedId?.defaultPort, @@ -119,6 +121,7 @@ export class CloudPlugin implements Plugin { registerCloudService: (contextProvider) => { this.contextProviders.push(contextProvider); }, + fetchElasticsearchConfig: this.fetchElasticsearchConfig.bind(this, core.http), }; } @@ -166,7 +169,6 @@ export class CloudPlugin implements Plugin { profileUrl, organizationUrl, projectsUrl, - elasticsearchUrl: decodedId?.elasticsearchUrl, kibanaUrl: decodedId?.kibanaUrl, isServerlessEnabled: this.isServerlessEnabled, serverless: { @@ -176,6 +178,7 @@ export class CloudPlugin implements Plugin { }, performanceUrl, usersAndRolesUrl, + fetchElasticsearchConfig: this.fetchElasticsearchConfig.bind(this, coreStart.http), }; } @@ -216,4 +219,26 @@ export class CloudPlugin implements Plugin { projectsUrl: fullCloudProjectsUrl, }; } + + private async fetchElasticsearchConfig( + http: CoreStart['http'] + ): Promise { + if (this.elasticsearchConfig !== undefined) { + // This config should be fully populated on first fetch, so we should avoid refetching from server + return this.elasticsearchConfig; + } + try { + const result = await http.get(ELASTICSEARCH_CONFIG_ROUTE, { + version: '1', + }); + + this.elasticsearchConfig = { elasticsearchUrl: result.elasticsearch_url || undefined }; + return this.elasticsearchConfig; + } catch { + this.logger.error('Failed to fetch Elasticsearch config'); + return { + elasticsearchUrl: undefined, + }; + } + } } diff --git a/x-pack/plugins/cloud/public/types.ts b/x-pack/plugins/cloud/public/types.ts index 8df1ba645cb4..1428e887f1b9 100644 --- a/x-pack/plugins/cloud/public/types.ts +++ b/x-pack/plugins/cloud/public/types.ts @@ -58,9 +58,9 @@ export interface CloudStart { */ projectsUrl?: string; /** - * The full URL to the elasticsearch cluster. + * Fetches the full URL to the elasticsearch cluster. */ - elasticsearchUrl?: string; + fetchElasticsearchConfig: () => Promise; /** * The full URL to the Kibana deployment. */ @@ -150,9 +150,9 @@ export interface CloudSetup { */ snapshotsUrl?: string; /** - * The full URL to the elasticsearch cluster. + * Fetches the full URL to the elasticsearch cluster. */ - elasticsearchUrl?: string; + fetchElasticsearchConfig: () => Promise; /** * The full URL to the Kibana deployment. */ @@ -225,3 +225,12 @@ export interface CloudSetup { orchestratorTarget?: string; }; } + +export interface PublicElasticsearchConfigType { + /** + * The URL to the Elasticsearch cluster, derived from xpack.elasticsearch.publicBaseUrl if populated + * Otherwise this is based on the cloudId + * If neither is populated, this will be undefined + */ + elasticsearchUrl?: string; +} diff --git a/x-pack/plugins/cloud/server/plugin.ts b/x-pack/plugins/cloud/server/plugin.ts index a03878b760dd..9f45b5398ac2 100644 --- a/x-pack/plugins/cloud/server/plugin.ts +++ b/x-pack/plugins/cloud/server/plugin.ts @@ -18,6 +18,7 @@ import { decodeCloudId, DecodedCloudId } from '../common/decode_cloud_id'; import { parseOnboardingSolution } from '../common/parse_onboarding_default_solution'; import { getFullCloudUrl } from '../common/utils'; import { readInstanceSizeMb } from './env'; +import { defineRoutes } from './routes/elasticsearch_routes'; interface PluginsSetup { usageCollection?: UsageCollectionSetup; @@ -201,6 +202,9 @@ export class CloudPlugin implements Plugin { if (this.config.id) { decodedId = decodeCloudId(this.config.id, this.logger); } + const router = core.http.createRouter(); + const elasticsearchUrl = core.elasticsearch.publicBaseUrl || decodedId?.elasticsearchUrl; + defineRoutes({ logger: this.logger, router, elasticsearchUrl }); return { ...this.getCloudUrls(), @@ -209,7 +213,7 @@ export class CloudPlugin implements Plugin { organizationId, instanceSizeMb: readInstanceSizeMb(), deploymentId, - elasticsearchUrl: core.elasticsearch.publicBaseUrl || decodedId?.elasticsearchUrl, + elasticsearchUrl, kibanaUrl: decodedId?.kibanaUrl, cloudHost: decodedId?.host, cloudDefaultPort: decodedId?.defaultPort, diff --git a/x-pack/plugins/cloud/server/routes/elasticsearch_routes.ts b/x-pack/plugins/cloud/server/routes/elasticsearch_routes.ts new file mode 100644 index 000000000000..5cdc2f90559c --- /dev/null +++ b/x-pack/plugins/cloud/server/routes/elasticsearch_routes.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from '@kbn/core/server'; +import { Logger } from '@kbn/logging'; +import { ElasticsearchConfigType } from '../../common/types'; +import { ELASTICSEARCH_CONFIG_ROUTE } from '../../common/constants'; + +export function defineRoutes({ + elasticsearchUrl, + logger, + router, +}: { + elasticsearchUrl?: string; + logger: Logger; + router: IRouter; +}) { + router.versioned + .get({ + path: ELASTICSEARCH_CONFIG_ROUTE, + access: 'internal', + }) + .addVersion({ version: '1', validate: {} }, async (context, request, response) => { + const body: ElasticsearchConfigType = { + elasticsearch_url: elasticsearchUrl, + }; + return response.ok({ + body, + }); + }); +} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx index 6671e4c73c2e..8e3d48c47c78 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx @@ -266,6 +266,7 @@ export const IndexDataVisualizerESQL: FC = (dataVi detectedTimestamp={currentDataView?.timeFieldName} hideRunQueryText={false} isLoading={queryHistoryStatus ?? false} + displayDocumentationAsFlyout /> diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate.test.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate.test.tsx index 9bb23b677f74..f55034f72ccd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate.test.tsx @@ -22,6 +22,12 @@ jest.mock('../../../../shared/enterprise_search_url', () => ({ getEnterpriseSearchUrl: () => 'http://localhost:3002', })); +jest.mock('../../../../shared/cloud_details/cloud_details', () => ({ + useCloudDetails: () => ({ + elasticsearchUrl: 'your_deployment_url', + }), +})); + describe('AnalyticsCollectionIntegrate', () => { const analyticsCollections: AnalyticsCollection = { events_datastream: 'analytics-events-example', @@ -55,7 +61,7 @@ describe('AnalyticsCollectionIntegrate', () => { .toMatchInlineSnapshot(` "