From 7dca537382a830b8f383d7429be6e25edf69ab7d Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 14 Sep 2020 11:25:18 -0400 Subject: [PATCH 01/63] [Ingest pipelines] Forms for processors T-U (#76710) --- .../common_fields/properties_field.tsx | 51 +++++++++++++ .../processors/geoip.tsx | 36 +++------ .../manage_processor_form/processors/index.ts | 4 + .../manage_processor_form/processors/trim.tsx | 29 ++++++++ .../processors/uppercase.tsx | 29 ++++++++ .../processors/url_decode.tsx | 29 ++++++++ .../processors/user_agent.tsx | 73 +++++++++++++++++++ .../shared/map_processor_type_to_form.tsx | 12 ++- 8 files changed, 233 insertions(+), 30 deletions(-) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/properties_field.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/trim.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/uppercase.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/url_decode.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/user_agent.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/properties_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/properties_field.tsx new file mode 100644 index 0000000000000..404a80161068c --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/properties_field.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { EuiComboBoxOptionOption } from '@elastic/eui'; +import { ComboBoxField, FIELD_TYPES, UseField } from '../../../../../../../shared_imports'; + +import { FieldsConfig, to } from '../shared'; + +const fieldsConfig: FieldsConfig = { + properties: { + type: FIELD_TYPES.COMBO_BOX, + deserializer: to.arrayOfStrings, + serializer: (v: string[]) => (v.length ? v : undefined), + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.commonFields.propertiesFieldLabel', + { + defaultMessage: 'Properties (optional)', + } + ), + }, +}; + +interface Props { + helpText?: React.ReactNode; + propertyOptions?: EuiComboBoxOptionOption[]; +} + +export const PropertiesField: FunctionComponent = ({ helpText, propertyOptions }) => { + return ( + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx index c0624c988061c..937fa4d3c4d86 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx @@ -9,18 +9,13 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCode } from '@elastic/eui'; -import { - FIELD_TYPES, - UseField, - Field, - ComboBoxField, - ToggleField, -} from '../../../../../../shared_imports'; +import { FIELD_TYPES, UseField, Field, ToggleField } from '../../../../../../shared_imports'; import { FieldNameField } from './common_fields/field_name_field'; import { IgnoreMissingField } from './common_fields/ignore_missing_field'; import { FieldsConfig, from, to } from './shared'; import { TargetField } from './common_fields/target_field'; +import { PropertiesField } from './common_fields/properties_field'; const fieldsConfig: FieldsConfig = { /* Optional field config */ @@ -42,21 +37,6 @@ const fieldsConfig: FieldsConfig = { ), }, - properties: { - type: FIELD_TYPES.COMBO_BOX, - deserializer: to.arrayOfStrings, - label: i18n.translate('xpack.ingestPipelines.pipelineEditor.geoIPForm.propertiesFieldLabel', { - defaultMessage: 'Properties (optional)', - }), - helpText: i18n.translate( - 'xpack.ingestPipelines.pipelineEditor.geoIPForm.propertiesFieldHelpText', - { - defaultMessage: - 'Properties added to the target field. Valid properties depend on the database file used.', - } - ), - }, - first_only: { type: FIELD_TYPES.TOGGLE, defaultValue: true, @@ -95,10 +75,14 @@ export const GeoIP: FunctionComponent = () => { - diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/index.ts index e83560b4a44ce..e211d682ab0f0 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/index.ts @@ -34,5 +34,9 @@ export { SetProcessor } from './set'; export { SetSecurityUser } from './set_security_user'; export { Split } from './split'; export { Sort } from './sort'; +export { Trim } from './trim'; +export { Uppercase } from './uppercase'; +export { UrlDecode } from './url_decode'; +export { UserAgent } from './user_agent'; export { FormFieldsComponent } from './shared'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/trim.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/trim.tsx new file mode 100644 index 0000000000000..aca5a3b4121b5 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/trim.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; +import { FieldNameField } from './common_fields/field_name_field'; +import { TargetField } from './common_fields/target_field'; + +export const Trim: FunctionComponent = () => { + return ( + <> + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/uppercase.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/uppercase.tsx new file mode 100644 index 0000000000000..336b68f8c2b7b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/uppercase.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; +import { FieldNameField } from './common_fields/field_name_field'; +import { TargetField } from './common_fields/target_field'; + +export const Uppercase: FunctionComponent = () => { + return ( + <> + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/url_decode.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/url_decode.tsx new file mode 100644 index 0000000000000..196645a89f707 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/url_decode.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; +import { FieldNameField } from './common_fields/field_name_field'; +import { TargetField } from './common_fields/target_field'; + +export const UrlDecode: FunctionComponent = () => { + return ( + <> + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/user_agent.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/user_agent.tsx new file mode 100644 index 0000000000000..8395833c09f28 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/user_agent.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { EuiComboBoxOptionOption } from '@elastic/eui'; +import { FIELD_TYPES, UseField, Field } from '../../../../../../shared_imports'; + +import { FieldsConfig } from './shared'; +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; +import { FieldNameField } from './common_fields/field_name_field'; +import { TargetField } from './common_fields/target_field'; +import { PropertiesField } from './common_fields/properties_field'; + +const propertyOptions: EuiComboBoxOptionOption[] = [ + { label: 'name' }, + { label: 'os' }, + { label: 'device' }, + { label: 'original' }, + { label: 'version' }, +]; + +const fieldsConfig: FieldsConfig = { + /* Optional fields config */ + regex_file: { + type: FIELD_TYPES.TEXT, + deserializer: String, + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.userAgentForm.regexFileFieldLabel', + { + defaultMessage: 'Regex file (optional)', + } + ), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.userAgentForm.regexFileFieldHelpText', + { + defaultMessage: + 'A filename containing the regular expressions for parsing the user agent string.', + } + ), + }, +}; + +export const UserAgent: FunctionComponent = () => { + return ( + <> + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx index 9de371f8d0024..95a8d35c119a6 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx @@ -41,6 +41,10 @@ import { SetSecurityUser, Split, Sort, + Trim, + Uppercase, + UrlDecode, + UserAgent, FormFieldsComponent, } from '../manage_processor_form/processors'; @@ -404,28 +408,28 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { }), }, trim: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Trim, docLinkPath: '/trim-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.trim', { defaultMessage: 'Trim', }), }, uppercase: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Uppercase, docLinkPath: '/uppercase-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.uppercase', { defaultMessage: 'Uppercase', }), }, urldecode: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: UrlDecode, docLinkPath: '/urldecode-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.urldecode', { defaultMessage: 'URL decode', }), }, user_agent: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: UserAgent, docLinkPath: '/user-agent-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.userAgent', { defaultMessage: 'User agent', From 1a49c4e20376cc53ec33acb3d750cf0b943c5cd7 Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Mon, 14 Sep 2020 11:51:58 -0400 Subject: [PATCH 02/63] [Security Solution][Endpoint][Admin] Task/endpoint list actions (#76555) Endpoint list actions to security solution endpoint admin Co-authored-by: Paul Tavares --- x-pack/plugins/ingest_manager/public/index.ts | 2 + .../scripts/dev_agent/script.ts | 5 + .../use_navigate_to_app_event_handler.ts | 2 +- .../pages/endpoint_hosts/store/action.ts | 6 + .../pages/endpoint_hosts/store/index.test.ts | 1 + .../pages/endpoint_hosts/store/middleware.ts | 77 +++++++--- .../store/mock_endpoint_result_list.ts | 6 +- .../pages/endpoint_hosts/store/reducer.ts | 9 ++ .../pages/endpoint_hosts/store/selectors.ts | 7 + .../management/pages/endpoint_hosts/types.ts | 15 +- .../pages/endpoint_hosts/view/index.test.tsx | 95 +++++++++++- .../pages/endpoint_hosts/view/index.tsx | 142 +++++++++++++++++- .../apps/endpoint/endpoint_list.ts | 8 + 13 files changed, 342 insertions(+), 33 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/index.ts b/x-pack/plugins/ingest_manager/public/index.ts index 75ba0e584230f..730ab59c3eb19 100644 --- a/x-pack/plugins/ingest_manager/public/index.ts +++ b/x-pack/plugins/ingest_manager/public/index.ts @@ -20,3 +20,5 @@ export { export { NewPackagePolicy } from './applications/ingest_manager/types'; export * from './applications/ingest_manager/types/intra_app_route_state'; + +export { pagePathGetters } from './applications/ingest_manager/constants'; diff --git a/x-pack/plugins/ingest_manager/scripts/dev_agent/script.ts b/x-pack/plugins/ingest_manager/scripts/dev_agent/script.ts index 65375a076e9a4..47108508ec68a 100644 --- a/x-pack/plugins/ingest_manager/scripts/dev_agent/script.ts +++ b/x-pack/plugins/ingest_manager/scripts/dev_agent/script.ts @@ -14,7 +14,11 @@ import { PostAgentEnrollRequest, PostAgentEnrollResponse, } from '../../common/types'; +import * as kibanaPackage from '../../package.json'; +// @ts-ignore +// Using the ts-ignore because we are importing directly from a json to a script file +const version = kibanaPackage.version; const CHECKIN_INTERVAL = 3000; // 3 seconds type Agent = Pick<_Agent, 'id' | 'access_api_key'>; @@ -104,6 +108,7 @@ async function enroll(kibanaURL: string, apiKey: string, log: ToolingLog): Promi ip: '127.0.0.1', system: `${os.type()} ${os.release()}`, memory: os.totalmem(), + elastic: { agent: { version } }, }, user_provided: { dev_agent_version: '0.0.1', diff --git a/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_navigate_to_app_event_handler.ts b/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_navigate_to_app_event_handler.ts index 190009440529c..943b30925a54c 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_navigate_to_app_event_handler.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_navigate_to_app_event_handler.ts @@ -12,7 +12,7 @@ type NavigateToAppHandlerOptions = NavigateToAppOptions & { state?: S; onClick?: EventHandlerCallback; }; -type EventHandlerCallback = MouseEventHandler; +type EventHandlerCallback = MouseEventHandler; /** * Provides an event handlers that can be used with (for example) `onClick` to prevent the diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts index 84d09adfc295e..c2a838404b0bb 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts @@ -82,6 +82,11 @@ interface ServerReturnedEndpointNonExistingPolicies { payload: EndpointState['nonExistingPolicies']; } +interface ServerReturnedEndpointAgentPolicies { + type: 'serverReturnedEndpointAgentPolicies'; + payload: EndpointState['agentPolicies']; +} + interface ServerReturnedEndpointExistValue { type: 'serverReturnedEndpointExistValue'; payload: boolean; @@ -126,4 +131,5 @@ export type EndpointAction = | ServerFailedToReturnMetadataPatterns | AppRequestedEndpointList | ServerReturnedEndpointNonExistingPolicies + | ServerReturnedEndpointAgentPolicies | UserUpdatedEndpointListRefreshOptions; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts index f28ae9bf55ab2..4faef85afbdc8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts @@ -52,6 +52,7 @@ describe('EndpointList store concerns', () => { policyItemsLoading: false, endpointPackageInfo: undefined, nonExistingPolicies: {}, + agentPolicies: {}, endpointsExist: true, patterns: [], patternsError: undefined, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index 5bf085023c65d..7673702f54370 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -18,7 +18,7 @@ import { patterns, searchBarQuery, } from './selectors'; -import { EndpointState } from '../types'; +import { EndpointState, PolicyIds } from '../types'; import { sendGetEndpointSpecificPackagePolicies, sendGetEndpointSecurityPackage, @@ -105,15 +105,21 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory => { +): Promise => { if (hosts.length === 0) { return; } @@ -318,29 +336,38 @@ const getNonExistingPoliciesForEndpointsList = async ( )})`, }, }) - ).items.reduce((list, agentPolicy) => { - (agentPolicy.package_policies as string[]).forEach((packagePolicy) => { - list[packagePolicy as string] = true; - }); - return list; - }, {}); + ).items.reduce( + (list, agentPolicy) => { + (agentPolicy.package_policies as string[]).forEach((packagePolicy) => { + list.packagePolicy[packagePolicy as string] = true; + list.agentPolicy[packagePolicy as string] = agentPolicy.id; + }); + return list; + }, + { packagePolicy: {}, agentPolicy: {} } + ); - const nonExisting = policyIdsToCheck.reduce( - (list, policyId) => { - if (policiesFound[policyId]) { + // packagePolicy contains non-existing packagePolicy ids whereas agentPolicy contains existing agentPolicy ids + const nonExistingPackagePoliciesAndExistingAgentPolicies = policyIdsToCheck.reduce( + (list, policyId: string) => { + if (policiesFound.packagePolicy[policyId as string]) { + list.agentPolicy[policyId as string] = policiesFound.agentPolicy[policyId]; return list; } - list[policyId] = true; + list.packagePolicy[policyId as string] = true; return list; }, - {} + { packagePolicy: {}, agentPolicy: {} } ); - if (Object.keys(nonExisting).length === 0) { + if ( + Object.keys(nonExistingPackagePoliciesAndExistingAgentPolicies.packagePolicy).length === 0 && + Object.keys(nonExistingPackagePoliciesAndExistingAgentPolicies.agentPolicy).length === 0 + ) { return; } - return nonExisting; + return nonExistingPackagePoliciesAndExistingAgentPolicies; }; const doEndpointsExist = async (http: HttpStart): Promise => { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts index cfde474c6290d..c5363a5ae9522 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts @@ -20,6 +20,7 @@ import { } from '../../policy/store/policy_list/services/ingest'; import { GetAgentPoliciesResponse, + GetAgentPoliciesResponseItem, GetPackagesResponse, } from '../../../../../../ingest_manager/common/types/rest_spec'; import { GetPolicyListResponse } from '../../policy/types'; @@ -43,7 +44,7 @@ export const mockEndpointResultList: (options?: { // total - numberToSkip is the count of non-skipped ones, but return no more than a pageSize, and no less than 0 const actualCountToReturn = Math.max(Math.min(total - numberToSkip, requestPageSize), 0); - const hosts = []; + const hosts: HostInfo[] = []; for (let index = 0; index < actualCountToReturn; index++) { hosts.push({ metadata: generator.generateHostMetadata(), @@ -78,12 +79,14 @@ const endpointListApiPathHandlerMocks = ({ epmPackages = [generator.generateEpmPackage()], endpointPackagePolicies = [], policyResponse = generator.generatePolicyResponse(), + agentPolicy = generator.generateAgentPolicy(), }: { /** route handlers will be setup for each individual host in this array */ endpointsResults?: HostResultList['hosts']; epmPackages?: GetPackagesResponse['response']; endpointPackagePolicies?: GetPolicyListResponse['items']; policyResponse?: HostPolicyResponse; + agentPolicy?: GetAgentPoliciesResponseItem; } = {}) => { const apiHandlers = { // endpoint package info @@ -106,7 +109,6 @@ const endpointListApiPathHandlerMocks = ({ // Do policies referenced in endpoint list exist // just returns 1 single agent policy that includes all of the packagePolicy IDs provided [INGEST_API_AGENT_POLICIES]: (): GetAgentPoliciesResponse => { - const agentPolicy = generator.generateAgentPolicy(); (agentPolicy.package_policies as string[]).push( ...endpointPackagePolicies.map((packagePolicy) => packagePolicy.id) ); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts index d688fa3b76b5a..99a1df7eb4002 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts @@ -30,6 +30,7 @@ export const initialEndpointListState: Immutable = { policyItemsLoading: false, endpointPackageInfo: undefined, nonExistingPolicies: {}, + agentPolicies: {}, endpointsExist: true, patterns: [], patternsError: undefined, @@ -72,6 +73,14 @@ export const endpointListReducer: ImmutableReducer = ( ...action.payload, }, }; + } else if (action.type === 'serverReturnedEndpointAgentPolicies') { + return { + ...state, + agentPolicies: { + ...state.agentPolicies, + ...action.payload, + }, + }; } else if (action.type === 'serverReturnedMetadataPatterns') { // handle error case return { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts index 8eefcc271794a..852bc9791fc90 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts @@ -217,6 +217,13 @@ export const nonExistingPolicies: ( state: Immutable ) => Immutable = (state) => state.nonExistingPolicies; +/** + * returns the list of known existing agent policies + */ +export const agentPolicies: ( + state: Immutable +) => Immutable = (state) => state.agentPolicies; + /** * Return boolean that indicates whether endpoints exist * @param state diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts index b73e60718d12e..77f21243ea120 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts @@ -51,8 +51,10 @@ export interface EndpointState { selectedPolicyId?: string; /** Endpoint package info */ endpointPackageInfo?: GetPackagesResponse['response'][0]; - /** tracks the list of policies IDs used in Host metadata that may no longer exist */ - nonExistingPolicies: Record; + /** Tracks the list of policies IDs used in Host metadata that may no longer exist */ + nonExistingPolicies: PolicyIds['packagePolicy']; + /** List of Package Policy Ids mapped to an associated Fleet Parent Agent Policy Id*/ + agentPolicies: PolicyIds['agentPolicy']; /** Tracks whether hosts exist and helps control if onboarding should be visible */ endpointsExist: boolean; /** index patterns for query bar */ @@ -65,6 +67,15 @@ export interface EndpointState { autoRefreshInterval: number; } +/** + * packagePolicy contains a list of Package Policy IDs (received via Endpoint metadata policy response) mapped to a boolean whether they exist or not. + * agentPolicy contains a list of existing Package Policy Ids mapped to an associated Fleet parent Agent Config. + */ +export interface PolicyIds { + packagePolicy: Record; + agentPolicy: Record; +} + /** * Query params on the host page parsed from the URL */ diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 6e37367930466..14167f25d5b90 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -6,7 +6,6 @@ import React from 'react'; import * as reactTestingLibrary from '@testing-library/react'; - import { EndpointList } from './index'; import '../../../../common/mock/match_media.ts'; import { @@ -669,4 +668,98 @@ describe('when on the list page', () => { }); }); }); + + describe('when the more actions column is opened', () => { + let hostInfo: HostInfo; + let agentId: string; + let agentPolicyId: string; + const generator = new EndpointDocGenerator('seed'); + let renderAndWaitForData: () => Promise>; + + const mockEndpointListApi = () => { + const { hosts } = mockEndpointResultList(); + hostInfo = { + host_status: hosts[0].host_status, + metadata: hosts[0].metadata, + }; + const packagePolicy = docGenerator.generatePolicyPackagePolicy(); + packagePolicy.id = hosts[0].metadata.Endpoint.policy.applied.id; + const agentPolicy = generator.generateAgentPolicy(); + agentPolicyId = agentPolicy.id; + agentId = hosts[0].metadata.elastic.agent.id; + + setEndpointListApiMockImplementation(coreStart.http, { + endpointsResults: [hostInfo], + endpointPackagePolicies: [packagePolicy], + agentPolicy, + }); + }; + + beforeEach(() => { + mockEndpointListApi(); + + reactTestingLibrary.act(() => { + history.push('/endpoints'); + }); + + renderAndWaitForData = async () => { + const renderResult = render(); + await middlewareSpy.waitForAction('serverReturnedEndpointList'); + await middlewareSpy.waitForAction('serverReturnedEndpointAgentPolicies'); + return renderResult; + }; + + coreStart.application.getUrlForApp.mockImplementation((appName) => { + switch (appName) { + case 'securitySolution': + return '/app/security'; + case 'ingestManager': + return '/app/ingestManager'; + } + return appName; + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('navigates to the Security Solution Host Details page', async () => { + const renderResult = await renderAndWaitForData(); + // open the endpoint actions menu + const endpointActionsButton = await renderResult.findByTestId('endpointTableRowActions'); + reactTestingLibrary.act(() => { + reactTestingLibrary.fireEvent.click(endpointActionsButton); + }); + + const hostLink = await renderResult.findByTestId('hostLink'); + expect(hostLink.getAttribute('href')).toEqual( + `/app/security/hosts/${hostInfo.metadata.host.hostname}` + ); + }); + it('navigates to the Ingest Agent Policy page', async () => { + const renderResult = await renderAndWaitForData(); + const endpointActionsButton = await renderResult.findByTestId('endpointTableRowActions'); + reactTestingLibrary.act(() => { + reactTestingLibrary.fireEvent.click(endpointActionsButton); + }); + + const agentPolicyLink = await renderResult.findByTestId('agentPolicyLink'); + expect(agentPolicyLink.getAttribute('href')).toEqual( + `/app/ingestManager#/policies/${agentPolicyId}` + ); + }); + it('navigates to the Ingest Agent Details page', async () => { + const renderResult = await renderAndWaitForData(); + const endpointActionsButton = await renderResult.findByTestId('endpointTableRowActions'); + reactTestingLibrary.act(() => { + reactTestingLibrary.fireEvent.click(endpointActionsButton); + }); + + const agentDetailsLink = await renderResult.findByTestId('agentDetailsLink'); + expect(agentDetailsLink.getAttribute('href')).toEqual( + `/app/ingestManager#/fleet/agents/${agentId}` + ); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 378f3cc4cb316..166f1660bf3d6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo, useCallback, memo } from 'react'; +import React, { useMemo, useCallback, memo, useState } from 'react'; import { EuiHorizontalRule, EuiBasicTable, @@ -16,6 +16,11 @@ import { EuiSelectableProps, EuiSuperDatePicker, EuiSpacer, + EuiPopover, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiContextMenuPanelProps, + EuiButtonIcon, EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; @@ -24,6 +29,8 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { createStructuredSelector } from 'reselect'; import { useDispatch } from 'react-redux'; +import { EuiContextMenuItemProps } from '@elastic/eui/src/components/context_menu/context_menu_item'; +import { NavigateToAppOptions } from 'kibana/public'; import { EndpointDetailsFlyout } from './details'; import * as selectors from '../store/selectors'; import { useEndpointSelector } from './hooks'; @@ -42,6 +49,7 @@ import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/ import { CreatePackagePolicyRouteState, AgentPolicyDetailsDeployAgentAction, + pagePathGetters, } from '../../../../../../ingest_manager/public'; import { SecurityPageName } from '../../../../app/types'; import { getEndpointListPath, getEndpointDetailsPath } from '../../../common/routing'; @@ -50,6 +58,8 @@ import { EndpointAction } from '../store/action'; import { EndpointPolicyLink } from './components/endpoint_policy_link'; import { AdminSearchBar } from './components/search_bar'; import { AdministrationListPage } from '../../../components/administration_list_page'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { APP_ID } from '../../../../../common/constants'; const EndpointListNavLink = memo<{ name: string; @@ -73,9 +83,40 @@ const EndpointListNavLink = memo<{ }); EndpointListNavLink.displayName = 'EndpointListNavLink'; +const TableRowActions = memo<{ + items: EuiContextMenuPanelProps['items']; +}>(({ items }) => { + const [isOpen, setIsOpen] = useState(false); + const handleCloseMenu = useCallback(() => setIsOpen(false), [setIsOpen]); + const handleToggleMenu = useCallback(() => setIsOpen(!isOpen), [isOpen]); + + return ( + + } + isOpen={isOpen} + closePopover={handleCloseMenu} + > + + + ); +}); +TableRowActions.displayName = 'EndpointTableRowActions'; + const selector = (createStructuredSelector as CreateStructuredSelector)(selectors); export const EndpointList = () => { const history = useHistory(); + const { services } = useKibana(); const { listData, pageIndex, @@ -90,6 +131,7 @@ export const EndpointList = () => { policyItemsLoading, endpointPackageVersion, endpointsExist, + agentPolicies, autoRefreshInterval, isAutoRefreshEnabled, patternsError, @@ -350,8 +392,87 @@ export const EndpointList = () => { ); }, }, + { + field: '', + name: i18n.translate('xpack.securitySolution.endpoint.list.actions', { + defaultMessage: 'Actions', + }), + actions: [ + { + // eslint-disable-next-line react/display-name + render: (item: HostInfo) => { + return ( + + + , + + + , + + + , + ]} + /> + ); + }, + }, + ], + }, ]; - }, [formatUrl, queryParams, search]); + }, [formatUrl, queryParams, search, agentPolicies, services?.application?.getUrlForApp]); const renderTableOrEmptyState = useMemo(() => { if (endpointsExist) { @@ -467,3 +588,20 @@ export const EndpointList = () => { ); }; + +const EuiContextMenuItemNavByRouter = memo< + Omit & { + navigateAppId: string; + navigateOptions: NavigateToAppOptions; + children: React.ReactNode; + } +>(({ navigateAppId, navigateOptions, children, ...otherMenuItemProps }) => { + const handleOnClick = useNavigateToAppEventHandler(navigateAppId, navigateOptions); + + return ( + + {children} + + ); +}); +EuiContextMenuItemNavByRouter.displayName = 'EuiContextMenuItemNavByRouter'; diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts index 00b4b82f9d602..b0b8d14108004 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts @@ -28,6 +28,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'IP Address', 'Version', 'Last Active', + 'Actions', ], [ 'rezzani-7.example.com', @@ -38,6 +39,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { '10.101.149.26, 2606:a000:ffc0:39:11ef:37b9:3371:578c', '6.8.0', 'Jan 24, 2020 @ 16:06:09.541', + '', ], [ 'cadmann-4.example.com', @@ -48,6 +50,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { '10.192.213.130, 10.70.28.129', '6.6.1', 'Jan 24, 2020 @ 16:06:09.541', + '', ], [ 'thurlow-9.example.com', @@ -58,6 +61,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { '10.46.229.234', '6.0.0', 'Jan 24, 2020 @ 16:06:09.541', + '', ], ]; @@ -238,6 +242,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'IP Address', 'Version', 'Last Active', + 'Actions', ], ['No items found'], ]; @@ -268,6 +273,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'IP Address', 'Version', 'Last Active', + 'Actions', ], [ 'cadmann-4.example.com', @@ -278,6 +284,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { '10.192.213.130, 10.70.28.129', '6.6.1', 'Jan 24, 2020 @ 16:06:09.541', + '', ], [ 'thurlow-9.example.com', @@ -288,6 +295,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { '10.46.229.234', '6.0.0', 'Jan 24, 2020 @ 16:06:09.541', + '', ], ]; From dcd119ce5f2935bcc5a027c45a51dfd29d1643a2 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 14 Sep 2020 18:22:28 +0200 Subject: [PATCH 03/63] [RUM Dashboard] Visitors by region map (#77135) Co-authored-by: Elastic Machine --- x-pack/plugins/apm/kibana.json | 15 +- .../plugins/apm/public/application/csmApp.tsx | 17 +- .../RumDashboard/CoreVitals/CoreVitalItem.tsx | 1 - .../app/RumDashboard/RumDashboard.tsx | 4 + .../VisitorBreakdownMap/EmbeddedMap.tsx | 183 ++++++++++++++++++ .../VisitorBreakdownMap/LayerList.ts | 174 +++++++++++++++++ .../VisitorBreakdownMap/MapToolTip.tsx | 109 +++++++++++ .../__stories__/MapTooltip.stories.tsx | 57 ++++++ .../__tests__/EmbeddedMap.test.tsx | 44 +++++ .../__tests__/LayerList.test.ts | 17 ++ .../__tests__/MapToolTip.test.tsx | 24 +++ .../__tests__/__mocks__/regions_layer.mock.ts | 151 +++++++++++++++ .../__snapshots__/EmbeddedMap.test.tsx.snap | 45 +++++ .../__snapshots__/MapToolTip.test.tsx.snap | 55 ++++++ .../VisitorBreakdownMap/index.tsx | 24 +++ .../VisitorBreakdownMap/useMapFilters.ts | 102 ++++++++++ .../components/app/RumDashboard/index.tsx | 2 +- .../app/RumDashboard/translations.ts | 12 ++ x-pack/plugins/apm/public/plugin.ts | 12 +- x-pack/plugins/maps/public/index.ts | 2 + 20 files changed, 1033 insertions(+), 17 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/LayerList.ts create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/EmbeddedMap.test.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/LayerList.test.ts create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/MapToolTip.test.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/EmbeddedMap.test.tsx.snap create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/MapToolTip.test.tsx.snap create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/index.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index 6cc3bb2a2c7e1..8aa4417580337 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -7,7 +7,8 @@ "apmOss", "data", "licensing", - "triggers_actions_ui" + "triggers_actions_ui", + "embeddable" ], "optionalPlugins": [ "cloud", @@ -22,17 +23,13 @@ ], "server": true, "ui": true, - "configPath": [ - "xpack", - "apm" - ], - "extraPublicDirs": [ - "public/style/variables" - ], + "configPath": ["xpack", "apm"], + "extraPublicDirs": ["public/style/variables"], "requiredBundles": [ "kibanaReact", "kibanaUtils", "observability", - "home" + "home", + "maps" ] } diff --git a/x-pack/plugins/apm/public/application/csmApp.tsx b/x-pack/plugins/apm/public/application/csmApp.tsx index cdfe42bd628cc..c63ec3700c877 100644 --- a/x-pack/plugins/apm/public/application/csmApp.tsx +++ b/x-pack/plugins/apm/public/application/csmApp.tsx @@ -26,7 +26,7 @@ import { LoadingIndicatorProvider } from '../context/LoadingIndicatorContext'; import { UrlParamsProvider } from '../context/UrlParamsContext'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; import { ConfigSchema } from '../index'; -import { ApmPluginSetupDeps } from '../plugin'; +import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin'; import { createCallApmApi } from '../services/rest/createCallApmApi'; import { px, units } from '../style/variables'; @@ -70,11 +70,13 @@ export function CsmAppRoot({ deps, history, config, + corePlugins: { embeddable }, }: { core: CoreStart; deps: ApmPluginSetupDeps; history: AppMountParameters['history']; config: ConfigSchema; + corePlugins: ApmPluginStartDeps; }) { const i18nCore = core.i18n; const plugins = deps; @@ -86,7 +88,7 @@ export function CsmAppRoot({ return ( - + @@ -110,12 +112,19 @@ export const renderApp = ( core: CoreStart, deps: ApmPluginSetupDeps, { element, history }: AppMountParameters, - config: ConfigSchema + config: ConfigSchema, + corePlugins: ApmPluginStartDeps ) => { createCallApmApi(core.http); ReactDOM.render( - , + , element ); return () => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/CoreVitalItem.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/CoreVitalItem.tsx index a4cbebf20b54c..22d50ca0d5c41 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/CoreVitalItem.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/CoreVitalItem.tsx @@ -118,7 +118,6 @@ export function CoreVitalItem({ setInFocusInd(ind); }} /> - ); } diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx index f05c07e8512ac..48c0f6cc60d84 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx @@ -18,6 +18,7 @@ import { PageLoadDistribution } from './PageLoadDistribution'; import { I18LABELS } from './translations'; import { VisitorBreakdown } from './VisitorBreakdown'; import { CoreVitals } from './CoreVitals'; +import { VisitorBreakdownMap } from './VisitorBreakdownMap'; export function RumDashboard() { return ( @@ -67,6 +68,9 @@ export function RumDashboard() { + + + diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx new file mode 100644 index 0000000000000..93608a0ccd826 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx @@ -0,0 +1,183 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState, useRef } from 'react'; +import uuid from 'uuid'; +import styled from 'styled-components'; + +import { + MapEmbeddable, + MapEmbeddableInput, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../maps/public/embeddable'; +import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../maps/common/constants'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { + ErrorEmbeddable, + ViewMode, + isErrorEmbeddable, +} from '../../../../../../../../src/plugins/embeddable/public'; +import { getLayerList } from './LayerList'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { RenderTooltipContentParams } from '../../../../../../maps/public'; +import { MapToolTip } from './MapToolTip'; +import { useMapFilters } from './useMapFilters'; +import { EmbeddableStart } from '../../../../../../../../src/plugins/embeddable/public'; + +const EmbeddedPanel = styled.div` + z-index: auto; + flex: 1; + display: flex; + flex-direction: column; + height: 100%; + position: relative; + .embPanel__content { + display: flex; + flex: 1 1 100%; + z-index: 1; + min-height: 0; // Absolute must for Firefox to scroll contents + } + &&& .mapboxgl-canvas { + animation: none !important; + } +`; + +interface KibanaDeps { + embeddable: EmbeddableStart; +} +export function EmbeddedMapComponent() { + const { urlParams } = useUrlParams(); + + const { start, end, serviceName } = urlParams; + + const mapFilters = useMapFilters(); + + const [embeddable, setEmbeddable] = useState< + MapEmbeddable | ErrorEmbeddable | undefined + >(); + + const embeddableRoot: React.RefObject = useRef< + HTMLDivElement + >(null); + + const { + services: { embeddable: embeddablePlugin }, + } = useKibana(); + + if (!embeddablePlugin) { + throw new Error('Embeddable start plugin not found'); + } + const factory: any = embeddablePlugin.getEmbeddableFactory( + MAP_SAVED_OBJECT_TYPE + ); + + const input: MapEmbeddableInput = { + id: uuid.v4(), + filters: mapFilters, + refreshConfig: { + value: 0, + pause: false, + }, + viewMode: ViewMode.VIEW, + isLayerTOCOpen: false, + query: { + query: 'transaction.type : "page-load"', + language: 'kuery', + }, + ...(start && { + timeRange: { + from: new Date(start!).toISOString(), + to: new Date(end!).toISOString(), + }, + }), + hideFilterActions: true, + }; + + function renderTooltipContent({ + addFilters, + closeTooltip, + features, + isLocked, + getLayerName, + loadFeatureProperties, + loadFeatureGeometry, + }: RenderTooltipContentParams) { + const props = { + addFilters, + closeTooltip, + isLocked, + getLayerName, + loadFeatureProperties, + loadFeatureGeometry, + }; + + return ; + } + + useEffect(() => { + if (embeddable != null && serviceName) { + embeddable.updateInput({ filters: mapFilters }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mapFilters]); + + // DateRange updated useEffect + useEffect(() => { + if (embeddable != null && start != null && end != null) { + const timeRange = { + from: new Date(start).toISOString(), + to: new Date(end).toISOString(), + }; + embeddable.updateInput({ timeRange }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [start, end]); + + useEffect(() => { + async function setupEmbeddable() { + if (!factory) { + throw new Error('Map embeddable not found.'); + } + const embeddableObject: any = await factory.create({ + ...input, + title: 'Visitors by region', + }); + + if (embeddableObject && !isErrorEmbeddable(embeddableObject)) { + embeddableObject.setRenderTooltipContent(renderTooltipContent); + await embeddableObject.setLayerList(getLayerList()); + } + + setEmbeddable(embeddableObject); + } + + setupEmbeddable(); + + // we want this effect to execute exactly once after the component mounts + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // We can only render after embeddable has already initialized + useEffect(() => { + if (embeddableRoot.current && embeddable) { + embeddable.render(embeddableRoot.current); + } + }, [embeddable, embeddableRoot]); + + return ( + +
+ + ); +} + +EmbeddedMapComponent.displayName = 'EmbeddedMap'; + +export const EmbeddedMap = React.memo(EmbeddedMapComponent); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/LayerList.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/LayerList.ts new file mode 100644 index 0000000000000..138a3f4018c65 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/LayerList.ts @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EMSFileSourceDescriptor, + EMSTMSSourceDescriptor, + ESTermSourceDescriptor, + LayerDescriptor as BaseLayerDescriptor, + VectorLayerDescriptor as BaseVectorLayerDescriptor, + VectorStyleDescriptor, +} from '../../../../../../maps/common/descriptor_types'; +import { + AGG_TYPE, + COLOR_MAP_TYPE, + FIELD_ORIGIN, + LABEL_BORDER_SIZES, + STYLE_TYPE, + SYMBOLIZE_AS_TYPES, +} from '../../../../../../maps/common/constants'; + +import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../../../../src/plugins/apm_oss/public'; + +const ES_TERM_SOURCE: ESTermSourceDescriptor = { + type: 'ES_TERM_SOURCE', + id: '3657625d-17b0-41ef-99ba-3a2b2938655c', + indexPatternTitle: 'apm-*', + term: 'client.geo.country_iso_code', + metrics: [ + { + type: AGG_TYPE.AVG, + field: 'transaction.duration.us', + label: 'Page load duration', + }, + ], + indexPatternId: APM_STATIC_INDEX_PATTERN_ID, + applyGlobalQuery: true, +}; + +export const REGION_NAME = 'region_name'; +export const COUNTRY_NAME = 'name'; + +export const TRANSACTION_DURATION_REGION = + '__kbnjoin__avg_of_transaction.duration.us__e62a1b9c-d7ff-4fd4-a0f6-0fdc44bb9e41'; + +export const TRANSACTION_DURATION_COUNTRY = + '__kbnjoin__avg_of_transaction.duration.us__3657625d-17b0-41ef-99ba-3a2b2938655c'; + +interface LayerDescriptor extends BaseLayerDescriptor { + sourceDescriptor: EMSTMSSourceDescriptor; +} + +interface VectorLayerDescriptor extends BaseVectorLayerDescriptor { + sourceDescriptor: EMSFileSourceDescriptor; +} + +export function getLayerList() { + const baseLayer: LayerDescriptor = { + sourceDescriptor: { type: 'EMS_TMS', isAutoSelect: true }, + id: 'b7af286d-2580-4f47-be93-9653d594ce7e', + label: null, + minZoom: 0, + maxZoom: 24, + alpha: 1, + visible: true, + style: { type: 'TILE' }, + type: 'VECTOR_TILE', + }; + + const getLayerStyle = (fieldName: string): VectorStyleDescriptor => { + return { + type: 'VECTOR', + properties: { + icon: { type: STYLE_TYPE.STATIC, options: { value: 'marker' } }, + fillColor: { + type: STYLE_TYPE.DYNAMIC, + options: { + color: 'Blue to Red', + colorCategory: 'palette_0', + fieldMetaOptions: { isEnabled: true, sigma: 3 }, + type: COLOR_MAP_TYPE.ORDINAL, + field: { + name: fieldName, + origin: FIELD_ORIGIN.JOIN, + }, + useCustomColorRamp: false, + }, + }, + lineColor: { + type: STYLE_TYPE.DYNAMIC, + options: { color: '#3d3d3d', fieldMetaOptions: { isEnabled: true } }, + }, + lineWidth: { type: STYLE_TYPE.STATIC, options: { size: 1 } }, + iconSize: { type: STYLE_TYPE.STATIC, options: { size: 6 } }, + iconOrientation: { + type: STYLE_TYPE.STATIC, + options: { orientation: 0 }, + }, + labelText: { type: STYLE_TYPE.STATIC, options: { value: '' } }, + labelColor: { + type: STYLE_TYPE.STATIC, + options: { color: '#000000' }, + }, + labelSize: { type: STYLE_TYPE.STATIC, options: { size: 14 } }, + labelBorderColor: { + type: STYLE_TYPE.STATIC, + options: { color: '#FFFFFF' }, + }, + symbolizeAs: { options: { value: SYMBOLIZE_AS_TYPES.CIRCLE } }, + labelBorderSize: { options: { size: LABEL_BORDER_SIZES.SMALL } }, + }, + isTimeAware: true, + }; + }; + + const pageLoadDurationByCountryLayer: VectorLayerDescriptor = { + joins: [ + { + leftField: 'iso2', + right: ES_TERM_SOURCE, + }, + ], + sourceDescriptor: { + type: 'EMS_FILE', + id: 'world_countries', + tooltipProperties: [COUNTRY_NAME], + applyGlobalQuery: true, + }, + style: getLayerStyle(TRANSACTION_DURATION_COUNTRY), + id: 'e8d1d974-eed8-462f-be2c-f0004b7619b2', + label: null, + minZoom: 0, + maxZoom: 24, + alpha: 0.75, + visible: true, + type: 'VECTOR', + }; + + const pageLoadDurationByAdminRegionLayer: VectorLayerDescriptor = { + joins: [ + { + leftField: 'region_iso_code', + right: { + type: 'ES_TERM_SOURCE', + id: 'e62a1b9c-d7ff-4fd4-a0f6-0fdc44bb9e41', + indexPatternTitle: 'apm-*', + term: 'client.geo.region_iso_code', + metrics: [{ type: AGG_TYPE.AVG, field: 'transaction.duration.us' }], + indexPatternId: APM_STATIC_INDEX_PATTERN_ID, + }, + }, + ], + sourceDescriptor: { + type: 'EMS_FILE', + id: 'administrative_regions_lvl2', + tooltipProperties: ['region_iso_code', REGION_NAME], + }, + style: getLayerStyle(TRANSACTION_DURATION_REGION), + id: '0e936d41-8765-41c9-97f0-05e166391366', + label: null, + minZoom: 3, + maxZoom: 24, + alpha: 0.75, + visible: true, + type: 'VECTOR', + }; + return [ + baseLayer, + pageLoadDurationByCountryLayer, + pageLoadDurationByAdminRegionLayer, + ]; +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx new file mode 100644 index 0000000000000..07b40addedec3 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState } from 'react'; +import { + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiOutsideClickDetector, + EuiPopoverTitle, +} from '@elastic/eui'; +import styled from 'styled-components'; +import { + COUNTRY_NAME, + REGION_NAME, + TRANSACTION_DURATION_COUNTRY, + TRANSACTION_DURATION_REGION, +} from './LayerList'; +import { RenderTooltipContentParams } from '../../../../../../maps/public'; +import { I18LABELS } from '../translations'; + +type MapToolTipProps = Partial; + +const DescriptionItem = styled(EuiDescriptionListDescription)` + &&& { + width: 25%; + } +`; + +const TitleItem = styled(EuiDescriptionListTitle)` + &&& { + width: 75%; + } +`; + +function MapToolTipComponent({ + closeTooltip, + features = [], + loadFeatureProperties, +}: MapToolTipProps) { + const { id: featureId, layerId } = features[0] ?? {}; + + const [regionName, setRegionName] = useState(featureId as string); + const [pageLoadDuration, setPageLoadDuration] = useState(''); + + const formatPageLoadValue = (val: number) => { + const valInMs = val / 1000; + if (valInMs > 1000) { + return (valInMs / 1000).toFixed(2) + ' sec'; + } + + return (valInMs / 1000).toFixed(0) + ' ms'; + }; + + useEffect(() => { + const loadRegionInfo = async () => { + if (loadFeatureProperties) { + const items = await loadFeatureProperties({ layerId, featureId }); + items.forEach((item) => { + if ( + item.getPropertyKey() === COUNTRY_NAME || + item.getPropertyKey() === REGION_NAME + ) { + setRegionName(item.getRawValue() as string); + } + if ( + item.getPropertyKey() === TRANSACTION_DURATION_REGION || + item.getPropertyKey() === TRANSACTION_DURATION_COUNTRY + ) { + setPageLoadDuration( + formatPageLoadValue(+(item.getRawValue() as string)) + ); + } + }); + } + }; + loadRegionInfo(); + }); + + return ( + { + if (closeTooltip != null) { + closeTooltip(); + } + }} + > + <> + {regionName} + + + {I18LABELS.avgPageLoadDuration} + + {pageLoadDuration} + + + + ); +} + +export const MapToolTip = React.memo(MapToolTipComponent); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx new file mode 100644 index 0000000000000..023f5d61a964e --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { storiesOf } from '@storybook/react'; +import React from 'react'; +import { EuiThemeProvider } from '../../../../../../../observability/public'; +import { MapToolTip } from '../MapToolTip'; +import { COUNTRY_NAME, TRANSACTION_DURATION_COUNTRY } from '../LayerList'; + +storiesOf('app/RumDashboard/VisitorsRegionMap', module) + .addDecorator((storyFn) => {storyFn()}) + .add( + 'Tooltip', + () => { + const loadFeatureProps = async () => { + return [ + { + getPropertyKey: () => COUNTRY_NAME, + getRawValue: () => 'United States', + }, + { + getPropertyKey: () => TRANSACTION_DURATION_COUNTRY, + getRawValue: () => 2434353, + }, + ]; + }; + return ( + + ); + }, + { + info: { + propTables: false, + source: false, + }, + } + ); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/EmbeddedMap.test.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/EmbeddedMap.test.tsx new file mode 100644 index 0000000000000..790be81bb65c0 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/EmbeddedMap.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { render } from 'enzyme'; +import React from 'react'; + +import { EmbeddedMap } from '../EmbeddedMap'; +import { KibanaContextProvider } from '../../../../../../../security_solution/public/common/lib/kibana'; +import { embeddablePluginMock } from '../../../../../../../../../src/plugins/embeddable/public/mocks'; + +describe('Embedded Map', () => { + test('it renders', () => { + const [core] = mockCore(); + + const wrapper = render( + + + + ); + + expect(wrapper).toMatchSnapshot(); + }); +}); + +const mockEmbeddable = embeddablePluginMock.createStartContract(); + +mockEmbeddable.getEmbeddableFactory = jest.fn().mockImplementation(() => ({ + create: () => ({ + reload: jest.fn(), + setRenderTooltipContent: jest.fn(), + setLayerList: jest.fn(), + }), +})); + +const mockCore: () => [any] = () => { + const core = { + embeddable: mockEmbeddable, + }; + + return [core]; +}; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/LayerList.test.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/LayerList.test.ts new file mode 100644 index 0000000000000..eb149ee2a132d --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/LayerList.test.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mockLayerList } from './__mocks__/regions_layer.mock'; +import { getLayerList } from '../LayerList'; + +describe('LayerList', () => { + describe('getLayerList', () => { + test('it returns the region layer', () => { + const layerList = getLayerList(); + expect(layerList).toStrictEqual(mockLayerList); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/MapToolTip.test.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/MapToolTip.test.tsx new file mode 100644 index 0000000000000..cbaae40b04361 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/MapToolTip.test.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { render, shallow } from 'enzyme'; +import React from 'react'; + +import { MapToolTip } from '../MapToolTip'; + +describe('Map Tooltip', () => { + test('it shallow renders', () => { + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders', () => { + const wrapper = render(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts new file mode 100644 index 0000000000000..c45f8b27d7d3e --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts @@ -0,0 +1,151 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const mockLayerList = [ + { + sourceDescriptor: { type: 'EMS_TMS', isAutoSelect: true }, + id: 'b7af286d-2580-4f47-be93-9653d594ce7e', + label: null, + minZoom: 0, + maxZoom: 24, + alpha: 1, + visible: true, + style: { type: 'TILE' }, + type: 'VECTOR_TILE', + }, + { + joins: [ + { + leftField: 'iso2', + right: { + type: 'ES_TERM_SOURCE', + id: '3657625d-17b0-41ef-99ba-3a2b2938655c', + indexPatternTitle: 'apm-*', + term: 'client.geo.country_iso_code', + metrics: [ + { + type: 'avg', + field: 'transaction.duration.us', + label: 'Page load duration', + }, + ], + indexPatternId: 'apm_static_index_pattern_id', + applyGlobalQuery: true, + }, + }, + ], + sourceDescriptor: { + type: 'EMS_FILE', + id: 'world_countries', + tooltipProperties: ['name'], + applyGlobalQuery: true, + }, + style: { + type: 'VECTOR', + properties: { + icon: { type: 'STATIC', options: { value: 'marker' } }, + fillColor: { + type: 'DYNAMIC', + options: { + color: 'Blue to Red', + colorCategory: 'palette_0', + fieldMetaOptions: { isEnabled: true, sigma: 3 }, + type: 'ORDINAL', + field: { + name: + '__kbnjoin__avg_of_transaction.duration.us__3657625d-17b0-41ef-99ba-3a2b2938655c', + origin: 'join', + }, + useCustomColorRamp: false, + }, + }, + lineColor: { + type: 'DYNAMIC', + options: { color: '#3d3d3d', fieldMetaOptions: { isEnabled: true } }, + }, + lineWidth: { type: 'STATIC', options: { size: 1 } }, + iconSize: { type: 'STATIC', options: { size: 6 } }, + iconOrientation: { type: 'STATIC', options: { orientation: 0 } }, + labelText: { type: 'STATIC', options: { value: '' } }, + labelColor: { type: 'STATIC', options: { color: '#000000' } }, + labelSize: { type: 'STATIC', options: { size: 14 } }, + labelBorderColor: { type: 'STATIC', options: { color: '#FFFFFF' } }, + symbolizeAs: { options: { value: 'circle' } }, + labelBorderSize: { options: { size: 'SMALL' } }, + }, + isTimeAware: true, + }, + id: 'e8d1d974-eed8-462f-be2c-f0004b7619b2', + label: null, + minZoom: 0, + maxZoom: 24, + alpha: 0.75, + visible: true, + type: 'VECTOR', + }, + { + joins: [ + { + leftField: 'region_iso_code', + right: { + type: 'ES_TERM_SOURCE', + id: 'e62a1b9c-d7ff-4fd4-a0f6-0fdc44bb9e41', + indexPatternTitle: 'apm-*', + term: 'client.geo.region_iso_code', + metrics: [{ type: 'avg', field: 'transaction.duration.us' }], + indexPatternId: 'apm_static_index_pattern_id', + }, + }, + ], + sourceDescriptor: { + type: 'EMS_FILE', + id: 'administrative_regions_lvl2', + tooltipProperties: ['region_iso_code', 'region_name'], + }, + style: { + type: 'VECTOR', + properties: { + icon: { type: 'STATIC', options: { value: 'marker' } }, + fillColor: { + type: 'DYNAMIC', + options: { + color: 'Blue to Red', + colorCategory: 'palette_0', + fieldMetaOptions: { isEnabled: true, sigma: 3 }, + type: 'ORDINAL', + field: { + name: + '__kbnjoin__avg_of_transaction.duration.us__e62a1b9c-d7ff-4fd4-a0f6-0fdc44bb9e41', + origin: 'join', + }, + useCustomColorRamp: false, + }, + }, + lineColor: { + type: 'DYNAMIC', + options: { color: '#3d3d3d', fieldMetaOptions: { isEnabled: true } }, + }, + lineWidth: { type: 'STATIC', options: { size: 1 } }, + iconSize: { type: 'STATIC', options: { size: 6 } }, + iconOrientation: { type: 'STATIC', options: { orientation: 0 } }, + labelText: { type: 'STATIC', options: { value: '' } }, + labelColor: { type: 'STATIC', options: { color: '#000000' } }, + labelSize: { type: 'STATIC', options: { size: 14 } }, + labelBorderColor: { type: 'STATIC', options: { color: '#FFFFFF' } }, + symbolizeAs: { options: { value: 'circle' } }, + labelBorderSize: { options: { size: 'SMALL' } }, + }, + isTimeAware: true, + }, + id: '0e936d41-8765-41c9-97f0-05e166391366', + label: null, + minZoom: 3, + maxZoom: 24, + alpha: 0.75, + visible: true, + type: 'VECTOR', + }, +]; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/EmbeddedMap.test.tsx.snap b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/EmbeddedMap.test.tsx.snap new file mode 100644 index 0000000000000..67f79c9fc747e --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/EmbeddedMap.test.tsx.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Embedded Map it renders 1`] = ` +.c0 { + z-index: auto; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + height: 100%; + position: relative; +} + +.c0 .embPanel__content { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex: 1 1 100%; + -ms-flex: 1 1 100%; + flex: 1 1 100%; + z-index: 1; + min-height: 0; +} + +.c0.c0.c0 .mapboxgl-canvas { + -webkit-animation: none !important; + animation: none !important; +} + +
+
+
+`; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/MapToolTip.test.tsx.snap b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/MapToolTip.test.tsx.snap new file mode 100644 index 0000000000000..860727a7a0f86 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/MapToolTip.test.tsx.snap @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Map Tooltip it renders 1`] = ` +Array [ +
, + .c1.c1.c1 { + width: 25%; +} + +.c0.c0.c0 { + width: 75%; +} + +
+
+ Average page load duration +
+
+
, +] +`; + +exports[`Map Tooltip it shallow renders 1`] = ` + + + + + Average page load duration + + + + +`; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/index.tsx new file mode 100644 index 0000000000000..44bfe5abbaca2 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/index.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiTitle, EuiSpacer } from '@elastic/eui'; +import { EmbeddedMap } from './EmbeddedMap'; +import { I18LABELS } from '../translations'; + +export function VisitorBreakdownMap() { + return ( + <> + +

{I18LABELS.pageLoadDurationByRegion}

+
+ +
+ +
+ + ); +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts new file mode 100644 index 0000000000000..357e04c538e68 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState } from 'react'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { FieldFilter as Filter } from '../../../../../../../../src/plugins/data/common'; +import { + CLIENT_GEO_COUNTRY_ISO_CODE, + SERVICE_NAME, + USER_AGENT_DEVICE, + USER_AGENT_NAME, + USER_AGENT_OS, +} from '../../../../../common/elasticsearch_fieldnames'; + +import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../../../../src/plugins/apm_oss/public'; + +const getMatchFilter = (field: string, value: string): Filter => { + return { + meta: { + index: APM_STATIC_INDEX_PATTERN_ID, + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: field, + params: { query: value }, + }, + query: { match_phrase: { [field]: value } }, + }; +}; + +const getMultiMatchFilter = (field: string, values: string[]): Filter => { + return { + meta: { + index: APM_STATIC_INDEX_PATTERN_ID, + type: 'phrases', + key: field, + value: values.join(', '), + params: values, + alias: null, + negate: false, + disabled: false, + }, + query: { + bool: { + should: values.map((value) => ({ match_phrase: { [field]: value } })), + minimum_should_match: 1, + }, + }, + }; +}; +export const useMapFilters = (): Filter[] => { + const { urlParams, uiFilters } = useUrlParams(); + + const { serviceName } = urlParams; + + const { browser, device, os, location } = uiFilters; + + const [mapFilters, setMapFilters] = useState([]); + + const existFilter: Filter = { + meta: { + index: APM_STATIC_INDEX_PATTERN_ID, + alias: null, + negate: false, + disabled: false, + type: 'exists', + key: 'transaction.marks.navigationTiming.fetchStart', + value: 'exists', + }, + exists: { + field: 'transaction.marks.navigationTiming.fetchStart', + }, + }; + + useEffect(() => { + const filters = [existFilter]; + if (serviceName) { + filters.push(getMatchFilter(SERVICE_NAME, serviceName)); + } + if (browser) { + filters.push(getMultiMatchFilter(USER_AGENT_NAME, browser)); + } + if (device) { + filters.push(getMultiMatchFilter(USER_AGENT_DEVICE, device)); + } + if (os) { + filters.push(getMultiMatchFilter(USER_AGENT_OS, os)); + } + if (location) { + filters.push(getMultiMatchFilter(CLIENT_GEO_COUNTRY_ISO_CODE, location)); + } + + setMapFilters(filters); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [serviceName, browser, device, os, location]); + + return mapFilters; +}; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx index 8d1959ec14d15..fa0551252b6a1 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx @@ -58,7 +58,7 @@ export function RumOverview() { return ( <> - + diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts index 660ed5a92a0e6..ec135168729b4 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts @@ -64,6 +64,18 @@ export const I18LABELS = { defaultMessage: 'Operating system', } ), + avgPageLoadDuration: i18n.translate( + 'xpack.apm.rum.visitorBreakdownMap.avgPageLoadDuration', + { + defaultMessage: 'Average page load duration', + } + ), + pageLoadDurationByRegion: i18n.translate( + 'xpack.apm.rum.visitorBreakdownMap.pageLoadDurationByRegion', + { + defaultMessage: 'Page load duration by region', + } + ), }; export const VisitorBreakdownLabel = i18n.translate( diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index b950b493c0f19..33e6a4b50a742 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -37,6 +37,7 @@ import { import { AlertType } from '../common/alert_types'; import { featureCatalogueEntry } from './featureCatalogueEntry'; import { toggleAppLinkInNav } from './toggleAppLinkInNav'; +import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; export type ApmPluginSetup = void; export type ApmPluginStart = void; @@ -57,6 +58,7 @@ export interface ApmPluginStartDeps { home: void; licensing: void; triggers_actions_ui: TriggersAndActionsUIPublicPluginStart; + embeddable: EmbeddableStart; } export class ApmPlugin implements Plugin { @@ -127,12 +129,18 @@ export class ApmPlugin implements Plugin { async mount(params: AppMountParameters) { // Load application bundle and Get start service - const [{ renderApp }, [coreStart]] = await Promise.all([ + const [{ renderApp }, [coreStart, corePlugins]] = await Promise.all([ import('./application/csmApp'), core.getStartServices(), ]); - return renderApp(coreStart, pluginSetupDeps, params, config); + return renderApp( + coreStart, + pluginSetupDeps, + params, + config, + corePlugins as ApmPluginStartDeps + ); }, }); } diff --git a/x-pack/plugins/maps/public/index.ts b/x-pack/plugins/maps/public/index.ts index 7b5521443d974..f220f32d346e7 100644 --- a/x-pack/plugins/maps/public/index.ts +++ b/x-pack/plugins/maps/public/index.ts @@ -17,3 +17,5 @@ export const plugin: PluginInitializer = ( }; export { MAP_SAVED_OBJECT_TYPE } from '../common/constants'; + +export { RenderTooltipContentParams } from './classes/tooltips/tooltip_property'; From 8f54c50363fc45e2d84ae990ef1fc54a4a85590a Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 14 Sep 2020 17:51:19 +0100 Subject: [PATCH 04/63] filter invalid SOs from the searc hresults in Task Manager (#76891) Filters out invalid SOs from search results to prevent a never ending loop and spamming of logs in Task Manager. --- .../task_manager/server/task_store.test.ts | 113 ++++++++++++++++-- .../plugins/task_manager/server/task_store.ts | 1 + 2 files changed, 106 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/task_manager/server/task_store.test.ts b/x-pack/plugins/task_manager/server/task_store.test.ts index a02123c4a3f8d..45c41b4d1d69d 100644 --- a/x-pack/plugins/task_manager/server/task_store.test.ts +++ b/x-pack/plugins/task_manager/server/task_store.test.ts @@ -633,7 +633,7 @@ if (doc['task.runAt'].size()!=0) { const runAt = new Date(); const tasks = [ { - _id: 'aaa', + _id: 'task:aaa', _source: { type: 'task', task: { @@ -654,7 +654,104 @@ if (doc['task.runAt'].size()!=0) { sort: ['a', 1], }, { + // this is invalid as it doesn't have the `type` prefix _id: 'bbb', + _source: { + type: 'task', + task: { + runAt, + taskType: 'bar', + schedule: { interval: '5m' }, + attempts: 2, + status: 'claiming', + params: '{ "shazm": 1 }', + state: '{ "henry": "The 8th" }', + user: 'dabo', + scope: ['reporting', 'ceo'], + ownerId: taskManagerId, + }, + }, + _seq_no: 3, + _primary_term: 4, + sort: ['b', 2], + }, + ]; + const { + result: { docs }, + args: { + search: { + body: { query }, + }, + }, + } = await testClaimAvailableTasks({ + opts: { + taskManagerId, + }, + claimingOpts: { + claimOwnershipUntil, + size: 10, + }, + hits: tasks, + }); + + expect(query.bool.must).toContainEqual({ + bool: { + must: [ + { + term: { + 'task.ownerId': taskManagerId, + }, + }, + { term: { 'task.status': 'claiming' } }, + ], + }, + }); + + expect(docs).toMatchObject([ + { + attempts: 0, + id: 'aaa', + schedule: undefined, + params: { hello: 'world' }, + runAt, + scope: ['reporting'], + state: { baby: 'Henhen' }, + status: 'claiming', + taskType: 'foo', + user: 'jimbo', + ownerId: taskManagerId, + }, + ]); + }); + + test('it filters out invalid tasks that arent SavedObjects', async () => { + const taskManagerId = uuid.v1(); + const claimOwnershipUntil = new Date(Date.now()); + const runAt = new Date(); + const tasks = [ + { + _id: 'task:aaa', + _source: { + type: 'task', + task: { + runAt, + taskType: 'foo', + schedule: undefined, + attempts: 0, + status: 'claiming', + params: '{ "hello": "world" }', + state: '{ "baby": "Henhen" }', + user: 'jimbo', + scope: ['reporting'], + ownerId: taskManagerId, + }, + }, + _seq_no: 1, + _primary_term: 2, + sort: ['a', 1], + }, + { + _id: 'task:bbb', _source: { type: 'task', task: { @@ -729,7 +826,7 @@ if (doc['task.runAt'].size()!=0) { const runAt = new Date(); const tasks = [ { - _id: 'aaa', + _id: 'task:aaa', _source: { type: 'task', task: { @@ -750,7 +847,7 @@ if (doc['task.runAt'].size()!=0) { sort: ['a', 1], }, { - _id: 'bbb', + _id: 'task:bbb', _source: { type: 'task', task: { @@ -1069,7 +1166,7 @@ if (doc['task.runAt'].size()!=0) { const runAt = new Date(); const tasks = [ { - _id: 'claimed-by-id', + _id: 'task:claimed-by-id', _source: { type: 'task', task: { @@ -1093,7 +1190,7 @@ if (doc['task.runAt'].size()!=0) { sort: ['a', 1], }, { - _id: 'claimed-by-schedule', + _id: 'task:claimed-by-schedule', _source: { type: 'task', task: { @@ -1117,7 +1214,7 @@ if (doc['task.runAt'].size()!=0) { sort: ['b', 2], }, { - _id: 'already-running', + _id: 'task:already-running', _source: { type: 'task', task: { @@ -1378,8 +1475,8 @@ if (doc['task.runAt'].size()!=0) { }); function generateFakeTasks(count: number = 1) { - return _.times(count, () => ({ - _id: 'aaa', + return _.times(count, (index) => ({ + _id: `task:id-${index}`, _source: { type: 'task', task: {}, diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index f2da41053e6ab..acd19bd75f7a3 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -451,6 +451,7 @@ export class TaskStore { return { docs: (rawDocs as SavedObjectsRawDoc[]) + .filter((doc) => this.serializer.isRawSavedObject(doc)) .map((doc) => this.serializer.rawToSavedObject(doc)) .map((doc) => omit(doc, 'namespace') as SavedObject) .map(savedObjectToConcreteTaskInstance), From 5c457d1e820320586db98ac965278d649ff7ef7c Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Mon, 14 Sep 2020 12:56:46 -0400 Subject: [PATCH 05/63] [Security Solution][Resolver] Analyzed event styling (#77115) --- .../public/resolver/view/assets.tsx | 4 +-- .../resolver/view/process_event_dot.tsx | 26 ++++++++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx index 1317c0ee94b60..a066eb9421fc1 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx @@ -477,7 +477,7 @@ export const useResolverTheme = (): { ), isLabelFilled: false, labelButtonFill: 'primary', - strokeColor: `${theme.euiColorPrimary}33`, // 33 = 20% opacity + strokeColor: theme.euiColorPrimary, }, terminatedTriggerCube: { backingFill: colorMap.triggerBackingFill, @@ -491,7 +491,7 @@ export const useResolverTheme = (): { ), isLabelFilled: false, labelButtonFill: 'danger', - strokeColor: `${theme.euiColorDanger}33`, + strokeColor: theme.euiColorDanger, }, }; diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 5d7112dd1547a..f4a7ad120e7db 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -10,6 +10,7 @@ import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { htmlIdGenerator, EuiButton, EuiI18nNumber, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useSelector } from 'react-redux'; +import { FormattedMessage } from '@kbn/i18n/react'; import { NodeSubMenu, subMenuAssets } from './submenu'; import { applyMatrix3 } from '../models/vector2'; import { Vector2, Matrix3, ResolverState } from '../types'; @@ -119,6 +120,7 @@ const UnstyledProcessEventDot = React.memo( // Node (html id=) IDs const ariaActiveDescendant = useSelector(selectors.ariaActiveDescendant); const selectedNode = useSelector(selectors.selectedNode); + const originID = useSelector(selectors.originID); const nodeID: string | undefined = eventModel.entityIDSafeVersion(event); if (nodeID === undefined) { // NB: this component should be taking nodeID as a `string` instead of handling this logic here @@ -231,6 +233,7 @@ const UnstyledProcessEventDot = React.memo( const isAriaCurrent = nodeID === ariaActiveDescendant; const isAriaSelected = nodeID === selectedNode; + const isOrigin = nodeID === originID; const dispatch = useResolverDispatch(); @@ -359,6 +362,20 @@ const UnstyledProcessEventDot = React.memo( height={markerSize * 1.5} className="backing" /> + {isOrigin && ( + + )} - {descriptionText} +
= 2 ? 'euiButton' : 'euiButton euiButton--small'} From 003fcb1332fdf7a4838286b17f4a1ea1bd556c44 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Mon, 14 Sep 2020 13:52:14 -0400 Subject: [PATCH 06/63] Add Lens to Recently Accessed (#77249) added lens to recently accessed on load and save in app.tsx --- x-pack/plugins/lens/common/constants.ts | 4 +++ .../lens/public/app_plugin/app.test.tsx | 36 +++++++++++++++++++ x-pack/plugins/lens/public/app_plugin/app.tsx | 3 ++ 3 files changed, 43 insertions(+) diff --git a/x-pack/plugins/lens/common/constants.ts b/x-pack/plugins/lens/common/constants.ts index 16397d340d951..ea2331a577743 100644 --- a/x-pack/plugins/lens/common/constants.ts +++ b/x-pack/plugins/lens/common/constants.ts @@ -16,3 +16,7 @@ export function getBasePath() { export function getEditPath(id: string) { return `#/edit/${encodeURIComponent(id)}`; } + +export function getFullPath(id: string) { + return `/app/${PLUGIN_ID}${getEditPath(id)}`; +} diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 2b979f064b8eb..b70e0a4fff02e 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -300,6 +300,29 @@ describe('Lens App', () => { ]); }); + it('adds to the recently viewed list on load', async () => { + const defaultArgs = makeDefaultArgs(); + instance = mount(); + + (defaultArgs.docStorage.load as jest.Mock).mockResolvedValue({ + id: '1234', + title: 'Daaaaaaadaumching!', + state: { + query: 'fake query', + filters: [], + }, + references: [], + }); + await act(async () => { + instance.setProps({ docId: '1234' }); + }); + expect(defaultArgs.core.chrome.recentlyAccessed.add).toHaveBeenCalledWith( + '/app/lens#/edit/1234', + 'Daaaaaaadaumching!', + '1234' + ); + }); + it('sets originatingApp breadcrumb when the document title changes', async () => { const defaultArgs = makeDefaultArgs(); defaultArgs.originatingApp = 'ultraCoolDashboard'; @@ -591,6 +614,19 @@ describe('Lens App', () => { expect(args.docStorage.load).not.toHaveBeenCalled(); }); + it('adds to the recently viewed list on save', async () => { + const { args } = await save({ + initialDocId: undefined, + newCopyOnSave: false, + newTitle: 'hello there', + }); + expect(args.core.chrome.recentlyAccessed.add).toHaveBeenCalledWith( + '/app/lens#/edit/aaa', + 'hello there', + 'aaa' + ); + }); + it('saves the latest doc as a copy', async () => { const { args, instance: inst } = await save({ initialDocId: '1234', diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 021ca8b182b2b..3f1f6d0e5509d 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -39,6 +39,7 @@ import { IndexPatternsContract, SavedQuery, } from '../../../../../src/plugins/data/public'; +import { getFullPath } from '../../common'; interface State { indicateNoData: boolean; @@ -271,6 +272,7 @@ export function App({ docStorage .load(docId) .then((doc) => { + core.chrome.recentlyAccessed.add(getFullPath(docId), doc.title, docId); getAllIndexPatterns( _.uniq( doc.references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id) @@ -365,6 +367,7 @@ export function App({ docStorage .save(doc) .then(({ id }) => { + core.chrome.recentlyAccessed.add(getFullPath(id), doc.title, id); // Prevents unnecessary network request and disables save button const newDoc = { ...doc, id }; const currentOriginatingApp = state.originatingApp; From 2055f5eecde8e540ae6c18b9007740736abb7f46 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Mon, 14 Sep 2020 14:04:33 -0400 Subject: [PATCH 07/63] [Monitoring] Handle no mappings found for sort and collapse fields (#77099) * Handle no mappings found for sort and collapse fields * Add comment * Fix sort usage * Ensure we query off MB for new api calls as well Co-authored-by: Elastic Machine --- .../monitoring/server/alerts/base_alert.ts | 14 ++++++-- .../alerts/cluster_health_alert.test.ts | 6 +++- .../server/alerts/cpu_usage_alert.test.ts | 6 +++- .../server/alerts/cpu_usage_alert.ts | 3 +- ...asticsearch_version_mismatch_alert.test.ts | 6 +++- .../kibana_version_mismatch_alert.test.ts | 6 +++- .../alerts/license_expiration_alert.test.ts | 6 +++- .../logstash_version_mismatch_alert.test.ts | 6 +++- .../server/alerts/nodes_changed_alert.test.ts | 6 +++- .../server/lib/alerts/append_mb_index.ts | 11 +++++++ .../lib/alerts/get_ccs_index_pattern.ts | 13 ++++---- .../server/lib/apm/_get_time_of_last_event.js | 1 + .../monitoring/server/lib/apm/get_apm_info.js | 2 +- .../monitoring/server/lib/apm/get_apms.js | 4 +-- .../server/lib/beats/get_beat_summary.js | 2 +- .../monitoring/server/lib/beats/get_beats.js | 4 +-- .../monitoring/server/lib/ccs_utils.js | 2 +- .../lib/cluster/flag_supported_clusters.js | 2 +- .../server/lib/cluster/get_cluster_license.js | 2 +- .../server/lib/cluster/get_clusters_state.js | 2 +- .../server/lib/cluster/get_clusters_stats.js | 2 +- .../server/lib/elasticsearch/ccr.js | 2 +- .../lib/elasticsearch/get_last_recovery.js | 2 +- .../server/lib/elasticsearch/get_ml_jobs.js | 2 +- .../indices/get_index_summary.js | 2 +- .../lib/elasticsearch/indices/get_indices.js | 2 +- .../elasticsearch/nodes/get_node_summary.js | 2 +- .../nodes/get_nodes/get_nodes.js | 2 +- .../get_indices_unassigned_shard_stats.js | 2 +- .../shards/get_nodes_shard_count.js | 2 +- .../elasticsearch/shards/get_shard_stats.js | 2 +- .../server/lib/kibana/get_kibana_info.js | 2 +- .../server/lib/kibana/get_kibanas.js | 2 +- .../server/lib/logs/get_log_types.js | 2 +- .../monitoring/server/lib/logs/get_logs.js | 2 +- .../server/lib/logstash/get_node_info.js | 2 +- .../server/lib/logstash/get_nodes.js | 2 +- .../logstash/get_pipeline_state_document.js | 2 +- .../lib/logstash/get_pipeline_versions.js | 2 +- .../monitoring/server/lib/mb_safe_query.ts | 33 +++++++++++++++++++ x-pack/plugins/monitoring/server/plugin.ts | 5 ++- .../server/routes/api/v1/elasticsearch/ccr.js | 2 +- .../routes/api/v1/elasticsearch/ccr_shard.js | 2 +- .../telemetry_collection/get_beats_stats.ts | 2 +- .../telemetry_collection/get_es_stats.ts | 2 +- .../get_high_level_stats.ts | 2 +- .../telemetry_collection/get_licenses.ts | 2 +- 47 files changed, 140 insertions(+), 52 deletions(-) create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/append_mb_index.ts create mode 100644 x-pack/plugins/monitoring/server/lib/mb_safe_query.ts diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts index 016acf2737f9b..f583b4882f83c 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -9,6 +9,7 @@ import { ILegacyCustomClusterClient, Logger, IUiSettingsClient, + LegacyCallAPIOptions, } from 'kibana/server'; import { i18n } from '@kbn/i18n'; import { @@ -36,6 +37,8 @@ import { MonitoringConfig } from '../config'; import { AlertSeverity } from '../../common/enums'; import { CommonAlertFilter, CommonAlertParams, CommonBaseAlert } from '../../common/types'; import { MonitoringLicenseService } from '../types'; +import { mbSafeQuery } from '../lib/mb_safe_query'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; export class BaseAlert { public type!: string; @@ -212,13 +215,20 @@ export class BaseAlert { `Executing alert with params: ${JSON.stringify(params)} and state: ${JSON.stringify(state)}` ); - const callCluster = this.monitoringCluster + const _callCluster = this.monitoringCluster ? this.monitoringCluster.callAsInternalUser : services.callCluster; + const callCluster = async ( + endpoint: string, + clientParams?: Record, + options?: LegacyCallAPIOptions + ) => { + return await mbSafeQuery(async () => _callCluster(endpoint, clientParams, options)); + }; const availableCcs = this.config.ui.ccs.enabled ? await fetchAvailableCcs(callCluster) : []; // Support CCS use cases by querying to find available remote clusters // and then adding those to the index pattern we are searching against - let esIndexPattern = INDEX_PATTERN_ELASTICSEARCH; + let esIndexPattern = appendMetricbeatIndex(this.config, INDEX_PATTERN_ELASTICSEARCH); if (availableCcs) { esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); } diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts index 4b083787f58cb..66085b53516a2 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts @@ -66,7 +66,11 @@ describe('ClusterHealthAlert', () => { }); const monitoringCluster = null; const config = { - ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + ui: { + ccs: { enabled: true }, + container: { elasticsearch: { enabled: false } }, + metricbeat: { index: 'metricbeat-*' }, + }, }; const kibanaUrl = 'http://localhost:5601'; diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts index c330e977e53d8..2705a77e0fce4 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts @@ -70,7 +70,11 @@ describe('CpuUsageAlert', () => { }); const monitoringCluster = null; const config = { - ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + ui: { + ccs: { enabled: true }, + container: { elasticsearch: { enabled: false } }, + metricbeat: { index: 'metricbeat-*' }, + }, }; const kibanaUrl = 'http://localhost:5601'; diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts index afe5abcf1ebd7..5bca84e33da3c 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts @@ -31,6 +31,7 @@ import { CommonAlertParams, CommonAlertParamDetail, } from '../../common/types'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; const RESOLVED = i18n.translate('xpack.monitoring.alerts.cpuUsage.resolved', { defaultMessage: 'resolved', @@ -137,7 +138,7 @@ export class CpuUsageAlert extends BaseAlert { uiSettings: IUiSettingsClient, availableCcs: string[] ): Promise { - let esIndexPattern = INDEX_PATTERN_ELASTICSEARCH; + let esIndexPattern = appendMetricbeatIndex(this.config, INDEX_PATTERN_ELASTICSEARCH); if (availableCcs) { esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); } diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts index ed300c211215b..1db85f915d794 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts @@ -69,7 +69,11 @@ describe('ElasticsearchVersionMismatchAlert', () => { }); const monitoringCluster = null; const config = { - ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + ui: { + ccs: { enabled: true }, + container: { elasticsearch: { enabled: false } }, + metricbeat: { index: 'metricbeat-*' }, + }, }; const kibanaUrl = 'http://localhost:5601'; diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts index dd3b37b5755e7..362532a995f2d 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts @@ -72,7 +72,11 @@ describe('KibanaVersionMismatchAlert', () => { }); const monitoringCluster = null; const config = { - ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + ui: { + ccs: { enabled: true }, + container: { elasticsearch: { enabled: false } }, + metricbeat: { index: 'metricbeat-*' }, + }, }; const kibanaUrl = 'http://localhost:5601'; diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts index e2f21b34efe21..da94e4af83802 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts @@ -76,7 +76,11 @@ describe('LicenseExpirationAlert', () => { }); const monitoringCluster = null; const config = { - ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + ui: { + ccs: { enabled: true }, + container: { elasticsearch: { enabled: false } }, + metricbeat: { index: 'metricbeat-*' }, + }, }; const kibanaUrl = 'http://localhost:5601'; diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts index fbb4a01d5b4ed..5ed189014cc6e 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts @@ -69,7 +69,11 @@ describe('LogstashVersionMismatchAlert', () => { }); const monitoringCluster = null; const config = { - ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + ui: { + ccs: { enabled: true }, + container: { elasticsearch: { enabled: false } }, + metricbeat: { index: 'metricbeat-*' }, + }, }; const kibanaUrl = 'http://localhost:5601'; diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts index 4b3e3d2d6cb6d..ec2b19eb5dfae 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts @@ -82,7 +82,11 @@ describe('NodesChangedAlert', () => { }); const monitoringCluster = null; const config = { - ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + ui: { + ccs: { enabled: true }, + container: { elasticsearch: { enabled: false } }, + metricbeat: { index: 'metricbeat-*' }, + }, }; const kibanaUrl = 'http://localhost:5601'; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/append_mb_index.ts b/x-pack/plugins/monitoring/server/lib/alerts/append_mb_index.ts new file mode 100644 index 0000000000000..683a0dfeccb1f --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/append_mb_index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MonitoringConfig } from '../../config'; + +export function appendMetricbeatIndex(config: MonitoringConfig, indexPattern: string) { + return `${indexPattern},${config.ui.metricbeat.index}`; +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts b/x-pack/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts index 7fdbc79685f7c..1907d2b4b3401 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ export function getCcsIndexPattern(indexPattern: string, remotes: string[]): string { - return `${indexPattern},${indexPattern - .split(',') - .map((pattern) => { - return remotes.map((remoteName) => `${remoteName}:${pattern}`).join(','); - }) - .join(',')}`; + const patternsToAdd = []; + for (const index of indexPattern.split(',')) { + for (const remote of remotes) { + patternsToAdd.push(`${remote}:${index}`); + } + } + return [...indexPattern.split(','), ...patternsToAdd].join(','); } diff --git a/x-pack/plugins/monitoring/server/lib/apm/_get_time_of_last_event.js b/x-pack/plugins/monitoring/server/lib/apm/_get_time_of_last_event.js index f08f92bffe790..37e739d0066a0 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/_get_time_of_last_event.js +++ b/x-pack/plugins/monitoring/server/lib/apm/_get_time_of_last_event.js @@ -26,6 +26,7 @@ export async function getTimeOfLastEvent({ { timestamp: { order: 'desc', + unmapped_type: 'long', }, }, ], diff --git a/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.js b/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.js index cc3682ef764c8..0b2e833933177 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.js +++ b/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.js @@ -73,7 +73,7 @@ export async function getApmInfo(req, apmIndexPattern, { clusterUuid, apmUuid, s 'hits.hits.inner_hits.first_hit.hits.hits._source.beats_stats.metrics.libbeat.output.write.bytes', ], body: { - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ start, end, diff --git a/x-pack/plugins/monitoring/server/lib/apm/get_apms.js b/x-pack/plugins/monitoring/server/lib/apm/get_apms.js index 19ed8298391d7..03a395e87d860 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/get_apms.js +++ b/x-pack/plugins/monitoring/server/lib/apm/get_apms.js @@ -128,8 +128,8 @@ export async function getApms(req, apmIndexPattern, clusterUuid) { }, }, sort: [ - { 'beats_stats.beat.uuid': { order: 'asc' } }, // need to keep duplicate uuids grouped - { timestamp: { order: 'desc' } }, // need oldest timestamp to come first for rate calcs to work + { 'beats_stats.beat.uuid': { order: 'asc', unmapped_type: 'long' } }, // need to keep duplicate uuids grouped + { timestamp: { order: 'desc', unmapped_type: 'long' } }, // need oldest timestamp to come first for rate calcs to work ], }, }; diff --git a/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.js b/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.js index 30ec728546ce9..962018f88354d 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.js +++ b/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.js @@ -78,7 +78,7 @@ export async function getBeatSummary( 'hits.hits.inner_hits.first_hit.hits.hits._source.beats_stats.metrics.libbeat.output.write.bytes', ], body: { - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createBeatsQuery({ start, end, diff --git a/x-pack/plugins/monitoring/server/lib/beats/get_beats.js b/x-pack/plugins/monitoring/server/lib/beats/get_beats.js index a5d43d1da7ebc..af4b6c31a3e5e 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/get_beats.js +++ b/x-pack/plugins/monitoring/server/lib/beats/get_beats.js @@ -126,8 +126,8 @@ export async function getBeats(req, beatsIndexPattern, clusterUuid) { }, }, sort: [ - { 'beats_stats.beat.uuid': { order: 'asc' } }, // need to keep duplicate uuids grouped - { timestamp: { order: 'desc' } }, // need oldest timestamp to come first for rate calcs to work + { 'beats_stats.beat.uuid': { order: 'asc', unmapped_type: 'long' } }, // need to keep duplicate uuids grouped + { timestamp: { order: 'desc', unmapped_type: 'long' } }, // need oldest timestamp to come first for rate calcs to work ], }, }; diff --git a/x-pack/plugins/monitoring/server/lib/ccs_utils.js b/x-pack/plugins/monitoring/server/lib/ccs_utils.js index bef07124fb430..96910dd86a94d 100644 --- a/x-pack/plugins/monitoring/server/lib/ccs_utils.js +++ b/x-pack/plugins/monitoring/server/lib/ccs_utils.js @@ -13,7 +13,7 @@ export function appendMetricbeatIndex(config, indexPattern) { if (isFunction(config.get)) { mbIndex = config.get('monitoring.ui.metricbeat.index'); } else { - mbIndex = get(config, 'monitoring.ui.metricbeat.index'); + mbIndex = get(config, 'ui.metricbeat.index'); } const newIndexPattern = `${indexPattern},${mbIndex}`; diff --git a/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.js b/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.js index 8e0d125d122aa..a1674b2f5eb36 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.js @@ -31,7 +31,7 @@ async function findSupportedBasicLicenseCluster( ignoreUnavailable: true, filterPath: 'hits.hits._source.cluster_uuid', body: { - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: { bool: { filter: [ diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_license.js b/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_license.js index a167837969bd0..bd84fbb66f962 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_license.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_license.js @@ -18,7 +18,7 @@ export function getClusterLicense(req, esIndexPattern, clusterUuid) { ignoreUnavailable: true, filterPath: 'hits.hits._source.license', body: { - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ type: 'cluster_stats', clusterUuid, diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_state.js b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_state.js index 33e4ec96676b2..fa5526728086e 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_state.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_state.js @@ -70,7 +70,7 @@ export function getClustersState(req, esIndexPattern, clusters) { collapse: { field: 'cluster_uuid', }, - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, }, }; diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.js b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.js index 945bf1f2e19a2..8ddd33837f56e 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.js @@ -67,7 +67,7 @@ function fetchClusterStats(req, esIndexPattern, clusterUuid) { collapse: { field: 'cluster_uuid', }, - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, }, }; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.js index 209a48cce369c..0f0ba49f229b0 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.js @@ -37,7 +37,7 @@ export async function checkCcrEnabled(req, esIndexPattern) { clusterUuid, metric: metricFields, }), - sort: [{ timestamp: { order: 'desc' } }], + sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], }, filterPath: ['hits.hits._source.stack_stats.xpack.ccr'], }; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/get_last_recovery.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_last_recovery.js index db8c89c364463..00e750b17d57b 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/get_last_recovery.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_last_recovery.js @@ -61,7 +61,7 @@ export function getLastRecovery(req, esIndexPattern) { ignoreUnavailable: true, body: { _source: ['index_recovery.shards'], - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ type: 'index_recovery', start, end, clusterUuid, metric }), }, }; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.js index 74d4bd6d2b5df..71f3633406c9b 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.js @@ -42,7 +42,7 @@ export function getMlJobs(req, esIndexPattern) { 'hits.hits._source.job_stats.node.name', ], body: { - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, collapse: { field: 'job_stats.job_id' }, query: createQuery({ type: 'job_stats', start, end, clusterUuid, metric }), }, diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_index_summary.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_index_summary.js index 524eaca191eec..6a0935f2b2d67 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_index_summary.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_index_summary.js @@ -69,7 +69,7 @@ export function getIndexSummary( size: 1, ignoreUnavailable: true, body: { - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ type: 'index_stats', start, end, clusterUuid, metric, filters }), }, }; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js index ba6d0cb926f06..cc3dec9f085b7 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js @@ -129,7 +129,7 @@ export function buildGetIndicesQuery( sort: [{ timestamp: 'asc' }], }, }, - sort: [{ timestamp: { order: 'desc' } }], + sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], }, }; } diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.js index 84384021a3593..06f5d5488a1ae 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.js @@ -109,7 +109,7 @@ export function getNodeSummary( size: 1, ignoreUnavailable: true, body: { - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ type: 'node_stats', start, end, clusterUuid, metric, filters }), }, }; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js index c2794b7e7fa44..3766845d39b4f 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js @@ -92,7 +92,7 @@ export async function getNodes(req, esIndexPattern, pageOfNodes, clusterStats, n }, }, }, - sort: [{ timestamp: { order: 'desc' } }], + sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], }, filterPath: [ 'hits.hits._source.source_node', diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_indices_unassigned_shard_stats.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_indices_unassigned_shard_stats.js index e8728e9c53ec5..f39233b29a1ce 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_indices_unassigned_shard_stats.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_indices_unassigned_shard_stats.js @@ -20,7 +20,7 @@ async function getUnassignedShardData(req, esIndexPattern, cluster) { size: 0, ignoreUnavailable: true, body: { - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ type: 'shards', clusterUuid: cluster.cluster_uuid, diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_nodes_shard_count.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_nodes_shard_count.js index 7823884dc749d..41a4740675637 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_nodes_shard_count.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_nodes_shard_count.js @@ -19,7 +19,7 @@ async function getShardCountPerNode(req, esIndexPattern, cluster) { size: 0, ignoreUnavailable: true, body: { - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ type: 'shards', clusterUuid: cluster.cluster_uuid, diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stats.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stats.js index 1154655ab6a22..2ac1e99add4de 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stats.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stats.js @@ -57,7 +57,7 @@ export function getShardStats( size: 0, ignoreUnavailable: true, body: { - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ type: 'shards', clusterUuid: cluster.cluster_uuid, diff --git a/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.js b/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.js index 533354f1e27b3..5a3e2dea930e0 100644 --- a/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.js +++ b/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.js @@ -41,7 +41,7 @@ export function getKibanaInfo(req, kbnIndexPattern, { clusterUuid, kibanaUuid }) }, }, collapse: { field: 'kibana_stats.kibana.uuid' }, - sort: [{ timestamp: { order: 'desc' } }], + sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], }, }; diff --git a/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas.js b/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas.js index f0e3f961a498f..b65f7770119fc 100644 --- a/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas.js +++ b/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas.js @@ -44,7 +44,7 @@ export function getKibanas(req, kbnIndexPattern, { clusterUuid }) { collapse: { field: 'kibana_stats.kibana.uuid', }, - sort: [{ timestamp: { order: 'desc' } }], + sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], _source: [ 'timestamp', 'kibana_stats.process.memory.resident_set_size_in_bytes', diff --git a/x-pack/plugins/monitoring/server/lib/logs/get_log_types.js b/x-pack/plugins/monitoring/server/lib/logs/get_log_types.js index fd7b5d457409f..7947a5b6797ae 100644 --- a/x-pack/plugins/monitoring/server/lib/logs/get_log_types.js +++ b/x-pack/plugins/monitoring/server/lib/logs/get_log_types.js @@ -65,7 +65,7 @@ export async function getLogTypes( filterPath: ['aggregations.levels.buckets', 'aggregations.types.buckets'], ignoreUnavailable: true, body: { - sort: { '@timestamp': { order: 'desc' } }, + sort: { '@timestamp': { order: 'desc', unmapped_type: 'long' } }, query: { bool: { filter, diff --git a/x-pack/plugins/monitoring/server/lib/logs/get_logs.js b/x-pack/plugins/monitoring/server/lib/logs/get_logs.js index bb453e09454af..7952bc02b91c2 100644 --- a/x-pack/plugins/monitoring/server/lib/logs/get_logs.js +++ b/x-pack/plugins/monitoring/server/lib/logs/get_logs.js @@ -82,7 +82,7 @@ export async function getLogs( ], ignoreUnavailable: true, body: { - sort: { '@timestamp': { order: 'desc' } }, + sort: { '@timestamp': { order: 'desc', unmapped_type: 'long' } }, query: { bool: { filter, diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.js b/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.js index 929dd53f74776..fdfc523e53527 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.js @@ -46,7 +46,7 @@ export function getNodeInfo(req, lsIndexPattern, { clusterUuid, logstashUuid }) }, }, collapse: { field: 'logstash_stats.logstash.uuid' }, - sort: [{ timestamp: { order: 'desc' } }], + sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], }, }; diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_nodes.js b/x-pack/plugins/monitoring/server/lib/logstash/get_nodes.js index 57adaff9be1c4..9b8786f8ae017 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_nodes.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_nodes.js @@ -44,7 +44,7 @@ export function getNodes(req, lsIndexPattern, { clusterUuid }) { collapse: { field: 'logstash_stats.logstash.uuid', }, - sort: [{ timestamp: { order: 'desc' } }], + sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], _source: [ 'timestamp', 'logstash_stats.process.cpu.percent', diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_state_document.js b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_state_document.js index d844e3604ca79..dae8d52e6c57b 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_state_document.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_state_document.js @@ -37,7 +37,7 @@ export async function getPipelineStateDocument( ignoreUnavailable: true, body: { _source: { excludes: 'logstash_state.pipeline.representation.plugins' }, - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query, terminate_after: 1, // Safe to do because all these documents are functionally identical }, diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_versions.js b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_versions.js index 91ac158b22494..c51f0f3ea1c03 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_versions.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_versions.js @@ -83,7 +83,7 @@ function fetchPipelineVersions(...args) { size: 0, ignoreUnavailable: true, body: { - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query, aggs, }, diff --git a/x-pack/plugins/monitoring/server/lib/mb_safe_query.ts b/x-pack/plugins/monitoring/server/lib/mb_safe_query.ts new file mode 100644 index 0000000000000..86bf5de8601e0 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/mb_safe_query.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * This function is designed to enable us to query against `.monitoring-*` and `metricbeat-*` + * indices SAFELY. We are adding the proper aliases into `metricbeat-*` to ensure all existing + * queries/aggs continue to work but we need to handle the reality that these aliases will not + * exist for older metricbeat-* indices, created before the aliases existed. + * + * Certain parts of a query will fail in this scenario, throwing an exception because of unmapped fields. + * So far, this is known to affect `sort` and `collapse` search query parameters. We have a way + * to handle this error elegantly with `sort` but not with `collapse` so we handle it manually in this spot. + * + * We can add future edge cases in here as well. + * + * @param queryExecutor + */ +export const mbSafeQuery = async (queryExecutor: () => Promise) => { + try { + return await queryExecutor(); + } catch (err) { + if ( + err.message.includes('no mapping found for') && + err.message.includes('in order to collapse on') + ) { + return {}; + } + throw err; + } +}; diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index d874c868ae8e8..fb0bf4ac530b1 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -34,6 +34,7 @@ import { requireUIRoutes } from './routes'; import { initBulkUploader } from './kibana_monitoring'; // @ts-ignore import { initInfraSource } from './lib/logs/init_infra_source'; +import { mbSafeQuery } from './lib/mb_safe_query'; import { instantiateClient } from './es_client/instantiate_client'; import { registerCollectors } from './kibana_monitoring/collectors'; import { registerMonitoringCollection } from './telemetry_collection'; @@ -351,7 +352,9 @@ export class Plugin { callWithRequest: async (_req: any, endpoint: string, params: any) => { const client = name === 'monitoring' ? cluster : this.legacyShimDependencies.esDataClient; - return client.asScoped(req).callAsCurrentUser(endpoint, params); + return mbSafeQuery(() => + client.asScoped(req).callAsCurrentUser(endpoint, params) + ); }, }), }, diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.js index 9999ba774b28d..fbaac56aa7400 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.js @@ -99,7 +99,7 @@ function buildRequest(req, config, esIndexPattern) { 'aggregations.by_follower_index.buckets.by_shard_id.buckets.follower_lag_ops.value', ], body: { - sort: [{ timestamp: { order: 'desc' } }], + sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], query: { bool: { must: [ diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.js index 4ee6cfe7fc54f..0a4b60b173254 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.js @@ -37,7 +37,7 @@ async function getCcrStat(req, esIndexPattern, filters) { 'hits.hits.inner_hits.oldest.hits.hits._source.ccr_stats.failed_read_requests', ], body: { - sort: [{ timestamp: { order: 'desc' } }], + sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], query: { bool: { must: [ diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_beats_stats.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_beats_stats.ts index 0585ec2c08274..d153c40bbe58b 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_beats_stats.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_beats_stats.ts @@ -355,7 +355,7 @@ async function fetchBeatsByType( }), from: page * HITS_SIZE, collapse: { field: `${type}.beat.uuid` }, - sort: [{ [`${type}.timestamp`]: 'desc' }], + sort: [{ [`${type}.timestamp`]: { order: 'desc', unmapped_type: 'long' } }], size: HITS_SIZE, }, }; diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_es_stats.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_es_stats.ts index 708bef31d8ac8..6325ed0c4b052 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_es_stats.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_es_stats.ts @@ -64,7 +64,7 @@ export function fetchElasticsearchStats( }, }, collapse: { field: 'cluster_uuid' }, - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, }, }; diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts index 0f6a86af79e45..481afc86fd115 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts @@ -329,7 +329,7 @@ export async function fetchHighLevelStats< // a more ideal field would be the concatenation of the uuid + transport address for duped UUIDs (copied installations) field: `${product}_stats.${product}.uuid`, }, - sort: [{ timestamp: 'desc' }], + sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], }, }; diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.ts index 0d41ac0f46814..a8b68929e84b8 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.ts @@ -59,7 +59,7 @@ export function fetchLicenses( }, }, collapse: { field: 'cluster_uuid' }, - sort: { timestamp: { order: 'desc' } }, + sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, }, }; From 1831fb3d2dadae43350a1ac77d085760a762120d Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Mon, 14 Sep 2020 20:27:27 +0100 Subject: [PATCH 08/63] [ML] DF Analytics creation wizard: Fixing field loading race condition (#77326) --- .../data_frame_analytics/pages/analytics_creation/page.tsx | 3 --- .../routes/data_frame_analytics/analytics_job_creation.tsx | 6 +++++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx index da5caf8e3875a..e72af6a0e30c2 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx @@ -21,7 +21,6 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useMlContext } from '../../../contexts/ml'; -import { newJobCapsService } from '../../../services/new_job_capabilities_service'; import { ml } from '../../../services/ml_api_service'; import { useCreateAnalyticsForm } from '../analytics_management/hooks/use_create_analytics_form'; import { CreateAnalyticsAdvancedEditor } from './components/create_analytics_advanced_editor'; @@ -62,8 +61,6 @@ export const Page: FC = ({ jobId }) => { if (currentIndexPattern) { (async function () { - await newJobCapsService.initializeFromIndexPattern(currentIndexPattern, false, false); - if (jobId !== undefined) { const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); if ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx index 8c45398098b2f..4ce2abf3fef60 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx @@ -16,6 +16,7 @@ import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../data_frame_analytics/pages/analytics_creation'; import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; +import { loadNewJobCapabilities } from '../../../services/new_job_capabilities_service'; export const analyticsJobsCreationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/data_frame_analytics/new_job', @@ -36,7 +37,10 @@ const PageWrapper: FC = ({ location, deps }) => { sort: false, }); - const { context } = useResolver(index, savedSearchId, deps.config, basicResolvers(deps)); + const { context } = useResolver(index, savedSearchId, deps.config, { + ...basicResolvers(deps), + jobCaps: () => loadNewJobCapabilities(index, savedSearchId, deps.indexPatterns), + }); return ( From 61c4e6fd8d7410bfabe3d108211d7aa1d54c7ef5 Mon Sep 17 00:00:00 2001 From: Michail Yasonik Date: Mon, 14 Sep 2020 15:32:30 -0400 Subject: [PATCH 09/63] Stacked headers and navigational search (#72331) Co-authored-by: Poff Poffenberger Co-authored-by: Ryan Keairns Co-authored-by: pgayvallet Co-authored-by: cchaos --- .github/CODEOWNERS | 1 + .../architecture/code-exploration.asciidoc | 598 + docs/developer/plugin-list.asciidoc | 4 + ...na-plugin-core-public.chromenavcontrols.md | 5 +- ...public.chromenavcontrols.registercenter.md | 24 + ...e-public.chromenavcontrols.registerleft.md | 2 +- ...-public.chromenavcontrols.registerright.md | 2 +- ...gin-core-public.chromestart.getnavtype_.md | 17 - .../kibana-plugin-core-public.chromestart.md | 1 - .../lib/config/schema.ts | 2 +- src/core/public/_variables.scss | 3 + .../public/application/ui/app_container.tsx | 6 +- src/core/public/chrome/chrome_service.mock.ts | 13 +- src/core/public/chrome/chrome_service.tsx | 16 +- .../nav_controls/nav_controls_service.ts | 17 +- .../collapsible_nav.test.tsx.snap | 464 +- .../header/__snapshots__/header.test.tsx.snap | 15154 ++++------------ src/core/public/chrome/ui/header/_index.scss | 9 - .../chrome/ui/header/collapsible_nav.test.tsx | 18 +- .../chrome/ui/header/collapsible_nav.tsx | 6 +- .../public/chrome/ui/header/header.test.tsx | 15 +- src/core/public/chrome/ui/header/header.tsx | 188 +- .../ui/header/header_action_menu.test.tsx | 139 + .../chrome/ui/header/header_action_menu.tsx | 64 + .../public/chrome/ui/header/header_logo.tsx | 4 +- .../chrome/ui/header/header_nav_controls.tsx | 7 +- .../public/chrome/ui/header/nav_drawer.tsx | 83 - src/core/public/index.scss | 2 +- src/core/public/public.api.md | 4 +- src/core/public/rendering/_base.scss | 70 +- src/core/public/styles/_base.scss | 11 - .../management_app/advanced_settings.tsx | 2 +- .../management_app/components/_index.scss | 1 - .../management_app/components/form/_form.scss | 15 - .../components/form/_index.scss | 1 - .../management_app/components/form/form.tsx | 19 +- .../public/management_app/index.scss | 2 - src/plugins/console/public/styles/_app.scss | 5 - .../public/application/_dashboard_app.scss | 1 - .../public/application/application.ts | 2 + .../application/dashboard_app_controller.tsx | 9 +- src/plugins/dashboard/public/plugin.tsx | 3 +- src/plugins/dev_tools/public/index.scss | 4 - src/plugins/dev_tools/public/plugin.ts | 2 +- .../public/application/angular/discover.html | 1 + .../public/application/angular/discover.js | 2 + .../discover/public/kibana_services.ts | 6 +- src/plugins/discover/public/plugin.ts | 5 +- .../public/angular/kbn_top_nav.js | 1 + src/plugins/kibana_react/public/util/index.ts | 1 + .../public/util/mount_point_portal.tsx | 23 +- src/plugins/kibana_react/public/util/utils.ts | 38 + src/plugins/management/public/plugin.ts | 2 +- .../public/top_nav_menu/_index.scss | 17 +- .../public/top_nav_menu/top_nav_menu.test.tsx | 5 +- .../public/top_nav_menu/top_nav_menu.tsx | 23 +- .../public/top_nav_menu/top_nav_menu_item.tsx | 10 +- .../components/newsfeed_header_nav_button.tsx | 2 +- src/plugins/timelion/public/_app.scss | 3 - src/plugins/timelion/public/plugin.ts | 2 +- .../components/visualize_top_nav.tsx | 3 + .../visualize/public/application/types.ts | 2 + src/plugins/visualize/public/plugin.ts | 4 +- test/accessibility/apps/management.ts | 3 +- test/functional/page_objects/time_picker.ts | 4 +- test/functional/services/listing_table.ts | 2 +- x-pack/.i18nrc.json | 6 +- x-pack/plugins/apm/public/plugin.ts | 3 +- .../__snapshots__/asset.stories.storyshot | 178 - .../components/fullscreen/fullscreen.scss | 13 +- .../plugins/canvas/public/lib/fullscreen.js | 4 + x-pack/plugins/canvas/public/plugin.tsx | 2 +- .../canvas/public/services/platform.ts | 3 + .../canvas/public/services/stubs/platform.ts | 1 + .../enterprise_search/common/constants.ts | 1 + .../enterprise_search/public/plugin.ts | 15 +- x-pack/plugins/global_search_bar/README.md | 3 + x-pack/plugins/global_search_bar/kibana.json | 10 + .../__snapshots__/search_bar.test.tsx.snap | 75 + .../public/components/search_bar.test.tsx | 97 + .../public/components/search_bar.tsx | 217 + .../plugins/global_search_bar/public/index.ts | 10 + .../global_search_bar/public/plugin.tsx | 46 + .../graph/public/angular/templates/index.html | 2 +- x-pack/plugins/graph/public/app.js | 2 + x-pack/plugins/graph/public/application.ts | 2 + .../plugins/graph/public/components/_app.scss | 2 +- x-pack/plugins/graph/public/plugin.ts | 2 +- x-pack/plugins/infra/public/plugin.ts | 4 +- .../ingest_manager/layouts/default.tsx | 10 +- .../create_package_policy_page/index.tsx | 22 +- .../components/settings/index.tsx | 29 +- .../edit_package_policy_page/index.tsx | 26 +- .../plugins/ingest_manager/public/plugin.ts | 2 +- .../lens/public/app_plugin/app.test.tsx | 3 + x-pack/plugins/lens/public/app_plugin/app.tsx | 3 + .../lens/public/app_plugin/mounter.tsx | 1 + x-pack/plugins/maps/common/constants.ts | 1 + x-pack/plugins/maps/public/_main.scss | 4 +- x-pack/plugins/maps/public/plugin.ts | 4 +- .../maps/public/routing/maps_router.tsx | 17 +- .../routes/maps_app/load_map_and_render.tsx | 2 + .../routing/routes/maps_app/maps_app_view.tsx | 2 + x-pack/plugins/ml/common/constants/app.ts | 1 + x-pack/plugins/ml/public/plugin.ts | 4 +- x-pack/plugins/observability/public/plugin.ts | 1 + .../public/application/components/main.tsx | 27 +- .../application/components/main_controls.tsx | 24 +- .../painless_lab/public/styles/_index.scss | 4 +- .../searchprofiler/public/styles/_index.scss | 4 +- .../security_solution/common/constants.ts | 1 + .../security_solution/cypress/tasks/login.ts | 4 +- .../security_solution/public/plugin.tsx | 16 +- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - x-pack/plugins/uptime/public/apps/plugin.ts | 2 +- .../apps/dashboard/reporting/screenshots.ts | 3 + .../functional/page_objects/graph_page.ts | 2 +- .../global_search/global_search_bar.ts | 39 + .../test_suites/global_search/index.ts | 1 + 120 files changed, 5205 insertions(+), 12930 deletions(-) create mode 100644 docs/developer/architecture/code-exploration.asciidoc create mode 100644 docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registercenter.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.chromestart.getnavtype_.md create mode 100644 src/core/public/_variables.scss create mode 100644 src/core/public/chrome/ui/header/header_action_menu.test.tsx create mode 100644 src/core/public/chrome/ui/header/header_action_menu.tsx delete mode 100644 src/core/public/chrome/ui/header/nav_drawer.tsx delete mode 100644 src/plugins/advanced_settings/public/management_app/components/_index.scss delete mode 100644 src/plugins/advanced_settings/public/management_app/components/form/_form.scss delete mode 100644 src/plugins/advanced_settings/public/management_app/components/form/_index.scss create mode 100644 src/plugins/kibana_react/public/util/utils.ts create mode 100644 x-pack/plugins/global_search_bar/README.md create mode 100644 x-pack/plugins/global_search_bar/kibana.json create mode 100644 x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap create mode 100644 x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx create mode 100644 x-pack/plugins/global_search_bar/public/components/search_bar.tsx create mode 100644 x-pack/plugins/global_search_bar/public/index.ts create mode 100644 x-pack/plugins/global_search_bar/public/plugin.tsx create mode 100644 x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5efbaba32e00a..03a4f9520c2ba 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -85,6 +85,7 @@ # Exclude tutorial resources folder for now because they are not owned by Kibana app and most will move out soon /src/legacy/core_plugins/kibana/public/home/*.ts @elastic/kibana-core-ui /src/legacy/core_plugins/kibana/public/home/np_ready/ @elastic/kibana-core-ui +/x-pack/plugins/global_search_bar/ @elastic/kibana-core-ui # Observability UIs /x-pack/legacy/plugins/infra/ @elastic/logs-metrics-ui diff --git a/docs/developer/architecture/code-exploration.asciidoc b/docs/developer/architecture/code-exploration.asciidoc new file mode 100644 index 0000000000000..4a390336da34f --- /dev/null +++ b/docs/developer/architecture/code-exploration.asciidoc @@ -0,0 +1,598 @@ +//// + +NOTE: + This is an automatically generated file. Please do not edit directly. Instead, run the + following from within the kibana repository: + + node scripts/build_plugin_list_docs + + You can update the template within packages/kbn-dev-utils/target/plugin_list/generate_plugin_list.js + +//// + +[[code-exploration]] +== Exploring Kibana code + +The goals of our folder heirarchy are: + +- Easy for developers to know where to add new services, plugins and applications. +- Easy for developers to know where to find the code from services, plugins and applications. +- Easy to browse and understand our folder structure. + +To that aim, we strive to: + +- Avoid too many files in any given folder. +- Choose clear, unambigious folder names. +- Organize by domain. +- Every folder should contain a README that describes the contents of that folder. + +[discrete] +[[kibana-services-applications]] +=== Services and Applications + +[discrete] +==== src/plugins + +- {kib-repo}blob/{branch}/src/plugins/advanced_settings[advancedSettings] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/apm_oss[apmOss] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/bfetch/README.md[bfetch] + +bfetch allows to batch HTTP requests and streams responses back. + + +- {kib-repo}blob/{branch}/src/plugins/charts/README.md[charts] + +The Charts plugin is a way to create easier integration of shared colors, themes, types and other utilities across all Kibana charts and visualizations. + + +- {kib-repo}blob/{branch}/src/plugins/console[console] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/dashboard/README.md[dashboard] + +Contains the dashboard application. + + +- {kib-repo}blob/{branch}/src/plugins/data/README.md[data] + +data plugin provides common data access services. + + +- {kib-repo}blob/{branch}/src/plugins/dev_tools/README.md[devTools] + +The ui/registry/dev_tools is removed in favor of the devTools plugin which exposes a register method in the setup contract. +Registering app works mostly the same as registering apps in core.application.register. +Routing will be handled by the id of the dev tool - your dev tool will be mounted when the URL matches /app/dev_tools#/. +This API doesn't support angular, for registering angular dev tools, bootstrap a local module on mount into the given HTML element. + + +- {kib-repo}blob/{branch}/src/plugins/discover/README.md[discover] + +Contains the Discover application and the saved search embeddable. + + +- {kib-repo}blob/{branch}/src/plugins/embeddable/README.md[embeddable] + +Embeddables are re-usable widgets that can be rendered in any environment or plugin. Developers can embed them directly in their plugin. End users can dynamically add them to any embeddable containers. + + +- {kib-repo}blob/{branch}/src/plugins/es_ui_shared/README.md[esUiShared] + +This plugin contains reusable code in the form of self-contained modules (or libraries). Each of these modules exports a set of functionality relevant to the domain of the module. + + +- {kib-repo}blob/{branch}/src/plugins/expressions/README.md[expressions] + +This plugin provides methods which will parse & execute an expression pipeline +string for you, as well as a series of registries for advanced users who might +want to incorporate their own functions, types, and renderers into the service +for use in their own application. + + +- {kib-repo}blob/{branch}/src/plugins/home/README.md[home] + +Moves the legacy ui/registry/feature_catalogue module for registering "features" that should be shown in the home page's feature catalogue to a service within a "home" plugin. The feature catalogue refered to here should not be confused with the "feature" plugin for registering features used to derive UI capabilities for feature controls. + + +- {kib-repo}blob/{branch}/src/plugins/index_pattern_management[indexPatternManagement] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/input_control_vis/README.md[inputControlVis] + +Contains the input control visualization allowing to place custom filter controls on a dashboard. + + +- {kib-repo}blob/{branch}/src/plugins/inspector/README.md[inspector] + +The inspector is a contextual tool to gain insights into different elements +in Kibana, e.g. visualizations. It has the form of a flyout panel. + + +- {kib-repo}blob/{branch}/src/plugins/kibana_legacy/README.md[kibanaLegacy] + +This plugin will contain several helpers and services to integrate pieces of the legacy Kibana app with the new Kibana platform. + + +- {kib-repo}blob/{branch}/src/plugins/kibana_react/README.md[kibanaReact] + +Tools for building React applications in Kibana. + + +- {kib-repo}blob/{branch}/src/plugins/kibana_usage_collection/README.md[kibanaUsageCollection] + +This plugin registers the basic usage collectors from Kibana: + + +- {kib-repo}blob/{branch}/src/plugins/kibana_utils/README.md[kibanaUtils] + +Utilities for building Kibana plugins. + + +- {kib-repo}blob/{branch}/src/plugins/legacy_export[legacyExport] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/management[management] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/maps_legacy[mapsLegacy] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/navigation/README.md[navigation] + +The navigation plugins exports the TopNavMenu component. +It also provides a stateful version of it on the start contract. + + +- {kib-repo}blob/{branch}/src/plugins/newsfeed[newsfeed] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/region_map[regionMap] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/saved_objects[savedObjects] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/saved_objects_management[savedObjectsManagement] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/share/README.md[share] + +Replaces the legacy ui/share module for registering share context menus. + + +- {kib-repo}blob/{branch}/src/plugins/telemetry/README.md[telemetry] + +Telemetry allows Kibana features to have usage tracked in the wild. The general term "telemetry" refers to multiple things: + + +- {kib-repo}blob/{branch}/src/plugins/telemetry_collection_manager/README.md[telemetryCollectionManager] + +Telemetry's collection manager to go through all the telemetry sources when fetching it before reporting. + + +- {kib-repo}blob/{branch}/src/plugins/telemetry_management_section/README.md[telemetryManagementSection] + +This plugin adds the Advanced Settings section for the Usage Data collection (aka Telemetry). + + +- {kib-repo}blob/{branch}/src/plugins/tile_map[tileMap] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/timelion/README.md[timelion] + +Contains the deprecated timelion application. For the timelion visualization, +which also contains the timelion APIs and backend, look at the vis_type_timelion plugin. + + +- {kib-repo}blob/{branch}/src/plugins/ui_actions/README.md[uiActions] + +An API for: + + +- {kib-repo}blob/{branch}/src/plugins/usage_collection/README.md[usageCollection] + +Usage Collection allows collecting usage data for other services to consume (telemetry and monitoring). +To integrate with the telemetry services for usage collection of your feature, there are 2 steps: + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_markdown/README.md[visTypeMarkdown] + +The markdown visualization that can be used to place text panels on dashboards. + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_metric/README.md[visTypeMetric] + +Contains the metric visualization. + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_table/README.md[visTypeTable] + +Contains the data table visualization, that allows presenting data in a simple table format. + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_tagcloud/README.md[visTypeTagcloud] + +Contains the tagcloud visualization. + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_timelion/README.md[visTypeTimelion] + +Contains the timelion visualization and the timelion backend. + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_timeseries/README.md[visTypeTimeseries] + +Contains everything around TSVB (the editor, visualizatin implementations and backends). + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_vega/README.md[visTypeVega] + +Contains the Vega visualization. + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_vislib/README.md[visTypeVislib] + +Contains the vislib visualizations. These are the classical area/line/bar, pie, gauge/goal and +heatmap charts. + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_xy/README.md[visTypeXy] + +Contains the new xy-axis chart using the elastic-charts library, which will eventually +replace the vislib xy-axis (bar, area, line) charts. + + +- {kib-repo}blob/{branch}/src/plugins/visualizations/README.md[visualizations] + +Contains most of the visualization infrastructure, e.g. the visualization type registry or the +visualization embeddable. + + +- {kib-repo}blob/{branch}/src/plugins/visualize/README.md[visualize] + +Contains the visualize application which includes the listing page and the app frame, +which will load the visualization's editor. + + +[discrete] +==== x-pack/plugins + +- {kib-repo}blob/{branch}/x-pack/plugins/actions/README.md[actions] + +The Kibana actions plugin provides a framework to create executable actions. You can: + + +- {kib-repo}blob/{branch}/x-pack/plugins/alerting_builtins/README.md[alertingBuiltins] + +This plugin provides alertTypes shipped with Kibana for use with the +the alerts plugin. When enabled, it will register +the built-in alertTypes with the alerting plugin, register associated HTTP +routes, etc. + + +- {kib-repo}blob/{branch}/x-pack/plugins/alerts/README.md[alerts] + +The Kibana alerting plugin provides a common place to set up alerts. You can: + + +- {kib-repo}blob/{branch}/x-pack/plugins/apm/readme.md[apm] + +To access an elasticsearch instance that has live data you have two options: + + +- {kib-repo}blob/{branch}/x-pack/plugins/audit_trail[auditTrail] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/beats_management/readme.md[beatsManagement] + +Notes: +Failure to have auth enabled in Kibana will make for a broken UI. UI-based errors not yet in place + + +- {kib-repo}blob/{branch}/x-pack/plugins/canvas/README.md[canvas] + +"Never look back. The past is done. The future is a blank canvas." ― Suzy Kassem, Rise Up and Salute the Sun + + +- {kib-repo}blob/{branch}/x-pack/plugins/case/README.md[case] + +Experimental Feature + + +- {kib-repo}blob/{branch}/x-pack/plugins/cloud[cloud] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/code[code] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/console_extensions[consoleExtensions] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/cross_cluster_replication/README.md[crossClusterReplication] + +You can run a local cluster and simulate a remote cluster within a single Kibana directory. + + +- {kib-repo}blob/{branch}/x-pack/plugins/dashboard_enhanced/README.md[dashboardEnhanced] + +Contains the enhancements to the OSS dashboard app. + + +- {kib-repo}blob/{branch}/x-pack/plugins/dashboard_mode/README.md[dashboardMode] + +The deprecated dashboard only mode. + + +- {kib-repo}blob/{branch}/x-pack/plugins/data_enhanced[dataEnhanced] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/discover_enhanced/README.md[discoverEnhanced] + +Contains the enhancements to the OSS discover app. + + +- {kib-repo}blob/{branch}/x-pack/plugins/embeddable_enhanced[embeddableEnhanced] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/encrypted_saved_objects/README.md[encryptedSavedObjects] + +The purpose of this plugin is to provide a way to encrypt/decrypt attributes on the custom Saved Objects that works with +security and spaces filtering as well as performing audit logging. + + +- {kib-repo}blob/{branch}/x-pack/plugins/enterprise_search/README.md[enterpriseSearch] + +This plugin's goal is to provide a Kibana user interface to the Enterprise Search solution's products (App Search and Workplace Search). In it's current MVP state, the plugin provides the following with the goal of gathering user feedback and raising product awareness: + + +- {kib-repo}blob/{branch}/x-pack/plugins/event_log/README.md[eventLog] + +The purpose of this plugin is to provide a way to persist a history of events +occuring in Kibana, initially just for the Make It Action project - alerts +and actions. + + +- {kib-repo}blob/{branch}/x-pack/plugins/features[features] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/file_upload[fileUpload] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/global_search/README.md[globalSearch] + +The GlobalSearch plugin provides an easy way to search for various objects, such as applications +or dashboards from the Kibana instance, from both server and client-side plugins + + +- {kib-repo}blob/{branch}/x-pack/plugins/global_search_bar/README.md[globalSearchBar] + +The GlobalSearchBar plugin provides a search interface for navigating Kibana. (It is the UI to the GlobalSearch plugin.) + + +- {kib-repo}blob/{branch}/x-pack/plugins/global_search_providers[globalSearchProviders] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/graph/README.md[graph] + +This is the main source folder of the Graph plugin. It contains all of the Kibana server and client source code. x-pack/test/functional/apps/graph contains additional functional tests. + + +- {kib-repo}blob/{branch}/x-pack/plugins/grokdebugger/README.md[grokdebugger] + +- {kib-repo}blob/{branch}/x-pack/plugins/index_lifecycle_management/README.md[indexLifecycleManagement] + +You can test that the Frozen badge, phase filtering, and lifecycle information is surfaced in +Index Management by running this series of requests in Console: + + +- {kib-repo}blob/{branch}/x-pack/plugins/index_management[indexManagement] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/infra/README.md[infra] + +This is the home of the infra plugin, which aims to provide a solution for +the infrastructure monitoring use-case within Kibana. + + +- {kib-repo}blob/{branch}/x-pack/plugins/ingest_manager/README.md[ingestManager] + +Fleet needs to have Elasticsearch API keys enabled, and also to have TLS enabled on kibana, (if you want to run Kibana without TLS you can provide the following config flag --xpack.ingestManager.fleet.tlsCheckDisabled=false) + + +- {kib-repo}blob/{branch}/x-pack/plugins/ingest_pipelines/README.md[ingestPipelines] + +The ingest_pipelines plugin provides Kibana support for Elasticsearch's ingest nodes. Please refer to the Elasticsearch documentation for more details. + + +- {kib-repo}blob/{branch}/x-pack/plugins/lens/readme.md[lens] + +Run all tests from the x-pack root directory + + +- {kib-repo}blob/{branch}/x-pack/plugins/license_management[licenseManagement] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/licensing/README.md[licensing] + +The licensing plugin retrieves license data from Elasticsearch at regular configurable intervals. + + +- {kib-repo}blob/{branch}/x-pack/plugins/lists/README.md[lists] + +README.md for developers working on the backend lists on how to get started +using the CURL scripts in the scripts folder. + + +- {kib-repo}blob/{branch}/x-pack/plugins/logstash[logstash] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/maps/README.md[maps] + +Visualize geo data from Elasticsearch or 3rd party geo-services. + + +- {kib-repo}blob/{branch}/x-pack/plugins/maps_legacy_licensing/README.md[mapsLegacyLicensing] + +This plugin provides access to the detailed tile map services from Elastic. + + +- {kib-repo}blob/{branch}/x-pack/plugins/ml[ml] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/monitoring[monitoring] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/observability/README.md[observability] + +This plugin provides shared components and services for use across observability solutions, as well as the observability landing page UI. + + +- {kib-repo}blob/{branch}/x-pack/plugins/oss_telemetry[ossTelemetry] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/painless_lab[painlessLab] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/remote_clusters[remoteClusters] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/reporting/README.md[reporting] + +An awesome Kibana reporting plugin + + +- {kib-repo}blob/{branch}/x-pack/plugins/rollup/README.md[rollup] + +Welcome to the Kibana rollup plugin! This plugin provides Kibana support for Elasticsearch's rollup feature. Please refer to the Elasticsearch documentation to understand rollup indices and how to create rollup jobs. + + +- {kib-repo}blob/{branch}/x-pack/plugins/searchprofiler[searchprofiler] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/security/README.md[security] + +See Configuring security in Kibana. + + +- {kib-repo}blob/{branch}/x-pack/plugins/security_solution/README.md[securitySolution] + +Welcome to the Kibana Security Solution plugin! This README will go over getting started with development and testing. + + +- {kib-repo}blob/{branch}/x-pack/plugins/snapshot_restore/README.md[snapshotRestore] + +or + + +- {kib-repo}blob/{branch}/x-pack/plugins/spaces[spaces] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/task_manager[taskManager] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/telemetry_collection_xpack/README.md[telemetryCollectionXpack] + +Gathers all usage collection, retrieving them from both: OSS and X-Pack plugins. + + +- {kib-repo}blob/{branch}/x-pack/plugins/transform[transform] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/translations[translations] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/triggers_actions_ui/README.md[triggers_actions_ui] + +The Kibana alerts and actions UI plugin provides a user interface for managing alerts and actions. +As a developer you can reuse and extend built-in alerts and actions UI functionality: + + +- {kib-repo}blob/{branch}/x-pack/plugins/ui_actions_enhanced/README.md[uiActionsEnhanced] + +- {kib-repo}blob/{branch}/x-pack/plugins/upgrade_assistant[upgradeAssistant] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/uptime/README.md[uptime] + +The purpose of this plugin is to provide users of Heartbeat more visibility of what's happening +in their infrastructure. + + +- {kib-repo}blob/{branch}/x-pack/plugins/watcher/README.md[watcher] + +This plugins adopts some conventions in addition to or in place of conventions in Kibana (at the time of the plugin's creation): + diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 275fdf8fb69ad..7727cd322181f 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -341,6 +341,10 @@ and actions. or dashboards from the Kibana instance, from both server and client-side plugins +|{kib-repo}blob/{branch}/x-pack/plugins/global_search_bar/README.md[globalSearchBar] +|The GlobalSearchBar plugin provides a search interface for navigating Kibana. (It is the UI to the GlobalSearch plugin.) + + |{kib-repo}blob/{branch}/x-pack/plugins/global_search_providers[globalSearchProviders] |WARNING: Missing README. diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.md b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.md index bca69adeef66b..47365782599ed 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.md @@ -30,6 +30,7 @@ chrome.navControls.registerLeft({ | Method | Description | | --- | --- | -| [registerLeft(navControl)](./kibana-plugin-core-public.chromenavcontrols.registerleft.md) | Register a nav control to be presented on the left side of the chrome header. | -| [registerRight(navControl)](./kibana-plugin-core-public.chromenavcontrols.registerright.md) | Register a nav control to be presented on the right side of the chrome header. | +| [registerCenter(navControl)](./kibana-plugin-core-public.chromenavcontrols.registercenter.md) | Register a nav control to be presented on the top-center side of the chrome header. | +| [registerLeft(navControl)](./kibana-plugin-core-public.chromenavcontrols.registerleft.md) | Register a nav control to be presented on the bottom-left side of the chrome header. | +| [registerRight(navControl)](./kibana-plugin-core-public.chromenavcontrols.registerright.md) | Register a nav control to be presented on the top-right side of the chrome header. | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registercenter.md b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registercenter.md new file mode 100644 index 0000000000000..2f921050e58dd --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registercenter.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeNavControls](./kibana-plugin-core-public.chromenavcontrols.md) > [registerCenter](./kibana-plugin-core-public.chromenavcontrols.registercenter.md) + +## ChromeNavControls.registerCenter() method + +Register a nav control to be presented on the top-center side of the chrome header. + +Signature: + +```typescript +registerCenter(navControl: ChromeNavControl): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| navControl | ChromeNavControl | | + +Returns: + +`void` + diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registerleft.md b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registerleft.md index c5c78bf9fb1da..514c44bd9d710 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registerleft.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registerleft.md @@ -4,7 +4,7 @@ ## ChromeNavControls.registerLeft() method -Register a nav control to be presented on the left side of the chrome header. +Register a nav control to be presented on the bottom-left side of the chrome header. Signature: diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registerright.md b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registerright.md index 12058f1d16ab9..eb56e0e38c6c9 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registerright.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registerright.md @@ -4,7 +4,7 @@ ## ChromeNavControls.registerRight() method -Register a nav control to be presented on the right side of the chrome header. +Register a nav control to be presented on the top-right side of the chrome header. Signature: diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.getnavtype_.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.getnavtype_.md deleted file mode 100644 index 09864be43996d..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.getnavtype_.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeStart](./kibana-plugin-core-public.chromestart.md) > [getNavType$](./kibana-plugin-core-public.chromestart.getnavtype_.md) - -## ChromeStart.getNavType$() method - -Get the navigation type TODO \#64541 Can delete - -Signature: - -```typescript -getNavType$(): Observable; -``` -Returns: - -`Observable` - diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.md index e983ad50d2afe..2594848ef0847 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.md @@ -59,7 +59,6 @@ core.chrome.setHelpExtension(elem => { | [getHelpExtension$()](./kibana-plugin-core-public.chromestart.gethelpextension_.md) | Get an observable of the current custom help conttent | | [getIsNavDrawerLocked$()](./kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md) | Get an observable of the current locked state of the nav drawer. | | [getIsVisible$()](./kibana-plugin-core-public.chromestart.getisvisible_.md) | Get an observable of the current visibility state of the chrome. | -| [getNavType$()](./kibana-plugin-core-public.chromestart.getnavtype_.md) | Get the navigation type TODO \#64541 Can delete | | [removeApplicationClass(className)](./kibana-plugin-core-public.chromestart.removeapplicationclass.md) | Remove a className added with addApplicationClass(). If className is unknown it is ignored. | | [setAppTitle(appTitle)](./kibana-plugin-core-public.chromestart.setapptitle.md) | Sets the current app's title | | [setBadge(badge)](./kibana-plugin-core-public.chromestart.setbadge.md) | Override the current badge | diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index e1d3bf1a8d901..701171876ad2c 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -262,7 +262,7 @@ export const schema = Joi.object() // settings for the find service layout: Joi.object() .keys({ - fixedHeaderHeight: Joi.number().default(50), + fixedHeaderHeight: Joi.number().default(100), }) .default(), diff --git a/src/core/public/_variables.scss b/src/core/public/_variables.scss new file mode 100644 index 0000000000000..8c054e770bd4b --- /dev/null +++ b/src/core/public/_variables.scss @@ -0,0 +1,3 @@ +@import '@elastic/eui/src/global_styling/variables/header'; + +$kbnHeaderOffset: $euiHeaderHeightCompensation * 2; diff --git a/src/core/public/application/ui/app_container.tsx b/src/core/public/application/ui/app_container.tsx index f668cf851da55..089d1cf3f3ced 100644 --- a/src/core/public/application/ui/app_container.tsx +++ b/src/core/public/application/ui/app_container.tsx @@ -94,8 +94,10 @@ export const AppContainer: FunctionComponent = ({ // eslint-disable-next-line no-console console.error(e); } finally { - setShowSpinner(false); - setIsMounting(false); + if (elementRef.current) { + setShowSpinner(false); + setIsMounting(false); + } } }; diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index 5862ee7175f71..0ae8b132f1d86 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -17,14 +17,7 @@ * under the License. */ import { BehaviorSubject } from 'rxjs'; -import { - ChromeBadge, - ChromeBrand, - ChromeBreadcrumb, - ChromeService, - InternalChromeStart, - NavType, -} from './'; +import { ChromeBadge, ChromeBrand, ChromeBreadcrumb, ChromeService, InternalChromeStart } from './'; const createStartContractMock = () => { const startContract: DeeplyMockedKeys = { @@ -50,8 +43,10 @@ const createStartContractMock = () => { }, navControls: { registerLeft: jest.fn(), + registerCenter: jest.fn(), registerRight: jest.fn(), getLeft$: jest.fn(), + getCenter$: jest.fn(), getRight$: jest.fn(), }, setAppTitle: jest.fn(), @@ -70,7 +65,6 @@ const createStartContractMock = () => { setHelpExtension: jest.fn(), setHelpSupportUrl: jest.fn(), getIsNavDrawerLocked$: jest.fn(), - getNavType$: jest.fn(), getCustomNavLink$: jest.fn(), setCustomNavLink: jest.fn(), }; @@ -83,7 +77,6 @@ const createStartContractMock = () => { startContract.getCustomNavLink$.mockReturnValue(new BehaviorSubject(undefined)); startContract.getHelpExtension$.mockReturnValue(new BehaviorSubject(undefined)); startContract.getIsNavDrawerLocked$.mockReturnValue(new BehaviorSubject(false)); - startContract.getNavType$.mockReturnValue(new BehaviorSubject('modern' as NavType)); return startContract; }; diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index b96c34cd9fbe8..b01f120b81305 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -37,7 +37,6 @@ import { ChromeNavControls, NavControlsService } from './nav_controls'; import { ChromeNavLinks, NavLinksService, ChromeNavLink } from './nav_links'; import { ChromeRecentlyAccessed, RecentlyAccessedService } from './recently_accessed'; import { Header } from './ui'; -import { NavType } from './ui/header'; import { ChromeHelpExtensionMenuLink } from './ui/header/header_help_menu'; export { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle }; @@ -172,10 +171,6 @@ export class ChromeService { const getIsNavDrawerLocked$ = isNavDrawerLocked$.pipe(takeUntil(this.stop$)); - // TODO #64541 - // Can delete - const getNavType$ = uiSettings.get$('pageNavigation').pipe(takeUntil(this.stop$)); - const isIE = () => { const ua = window.navigator.userAgent; const msie = ua.indexOf('MSIE '); // IE 10 or older @@ -241,10 +236,10 @@ export class ChromeService { navLinks$={navLinks.getNavLinks$()} recentlyAccessed$={recentlyAccessed.get$()} navControlsLeft$={navControls.getLeft$()} + navControlsCenter$={navControls.getCenter$()} navControlsRight$={navControls.getRight$()} onIsLockedUpdate={setIsNavDrawerLocked} isLocked$={getIsNavDrawerLocked$} - navType$={getNavType$} /> ), @@ -305,8 +300,6 @@ export class ChromeService { getIsNavDrawerLocked$: () => getIsNavDrawerLocked$, - getNavType$: () => getNavType$, - getCustomNavLink$: () => customNavLink$.pipe(takeUntil(this.stop$)), setCustomNavLink: (customNavLink?: ChromeNavLink) => { @@ -468,13 +461,6 @@ export interface ChromeStart { * Get an observable of the current locked state of the nav drawer. */ getIsNavDrawerLocked$(): Observable; - - /** - * Get the navigation type - * TODO #64541 - * Can delete - */ - getNavType$(): Observable; } /** @internal */ diff --git a/src/core/public/chrome/nav_controls/nav_controls_service.ts b/src/core/public/chrome/nav_controls/nav_controls_service.ts index 167948e01cb36..2638f40c77dc4 100644 --- a/src/core/public/chrome/nav_controls/nav_controls_service.ts +++ b/src/core/public/chrome/nav_controls/nav_controls_service.ts @@ -45,14 +45,18 @@ export interface ChromeNavControl { * @public */ export interface ChromeNavControls { - /** Register a nav control to be presented on the left side of the chrome header. */ + /** Register a nav control to be presented on the bottom-left side of the chrome header. */ registerLeft(navControl: ChromeNavControl): void; - /** Register a nav control to be presented on the right side of the chrome header. */ + /** Register a nav control to be presented on the top-right side of the chrome header. */ registerRight(navControl: ChromeNavControl): void; + /** Register a nav control to be presented on the top-center side of the chrome header. */ + registerCenter(navControl: ChromeNavControl): void; /** @internal */ getLeft$(): Observable; /** @internal */ getRight$(): Observable; + /** @internal */ + getCenter$(): Observable; } /** @internal */ @@ -62,6 +66,7 @@ export class NavControlsService { public start() { const navControlsLeft$ = new BehaviorSubject>(new Set()); const navControlsRight$ = new BehaviorSubject>(new Set()); + const navControlsCenter$ = new BehaviorSubject>(new Set()); return { // In the future, registration should be moved to the setup phase. This @@ -72,6 +77,9 @@ export class NavControlsService { registerRight: (navControl: ChromeNavControl) => navControlsRight$.next(new Set([...navControlsRight$.value.values(), navControl])), + registerCenter: (navControl: ChromeNavControl) => + navControlsCenter$.next(new Set([...navControlsCenter$.value.values(), navControl])), + getLeft$: () => navControlsLeft$.pipe( map((controls) => sortBy([...controls.values()], 'order')), @@ -82,6 +90,11 @@ export class NavControlsService { map((controls) => sortBy([...controls.values()], 'order')), takeUntil(this.stop$) ), + getCenter$: () => + navControlsCenter$.pipe( + map((controls) => sortBy([...controls.values()], 'order')), + takeUntil(this.stop$) + ), }; } diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index a770ece8496e4..86cacfe98f767 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -121,7 +121,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` homeHref="/" id="collapsibe-nav" isLocked={false} - isOpen={true} + isNavOpen={true} navLinks$={ BehaviorSubject { "_isScalar": false, @@ -2105,7 +2105,7 @@ exports[`CollapsibleNav renders the default nav 1`] = ` homeHref="/" id="collapsibe-nav" isLocked={false} - isOpen={false} + isNavOpen={false} navLinks$={ BehaviorSubject { "_isScalar": false, @@ -2339,6 +2339,7 @@ exports[`CollapsibleNav renders the default nav 2`] = ` homeHref="/" id="collapsibe-nav" isLocked={false} + isNavOpen={false} isOpen={true} navLinks$={ BehaviorSubject { @@ -2454,461 +2455,9 @@ exports[`CollapsibleNav renders the default nav 2`] = ` data-test-subj="collapsibleNav" id="collapsibe-nav" isDocked={false} - isOpen={true} + isOpen={false} onClose={[Function]} - > - - - -
- -
-
- + /> `; @@ -3025,6 +2574,7 @@ exports[`CollapsibleNav renders the default nav 3`] = ` homeHref="/" id="collapsibe-nav" isLocked={true} + isNavOpen={false} isOpen={true} navLinks$={ BehaviorSubject { @@ -3140,7 +2690,7 @@ exports[`CollapsibleNav renders the default nav 3`] = ` data-test-subj="collapsibleNav" id="collapsibe-nav" isDocked={true} - isOpen={true} + isOpen={false} onClose={[Function]} > - -
-
-
- - -`; - -exports[`Header renders 2`] = ` -
+ -
- -
- -
- -
- - - -
-
- -
+ - - - -
- - - - -
- - , + ], + }, + Object { + "borders": "none", + "items": Array [ + -
- - - - - - - - - , ], - "thrownError": null, - } - } - /> - -
- -
+ }, + Object { + "borders": "none", + "items": Array [ , + , + ], + }, + ] + } + theme="dark" + > +
+ +
+ + + + +
+ +
+ +
+
+
+
+ +
+ +
+ - - - - } - closePopover={[Function]} - data-test-subj="helpMenuButton" - display="inlineBlock" - hasArrow={true} - id="headerHelpMenu" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - repositionOnScroll={true} - > - -
-
- - - -
-
-
-
- -
-
-
- -
-
-
- - - - -
-
-`; - -exports[`Header renders 3`] = ` -
- -
-
-
- -
- -
- -
- -
- - - -
-
- -
- - - - -
- - - - -
- - -
- - - - - - - - - - -
- -
- - - - - - } - closePopover={[Function]} - data-test-subj="helpMenuButton" - display="inlineBlock" - hasArrow={true} - id="headerHelpMenu" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - repositionOnScroll={true} - > - -
-
- - - -
-
-
-
-
-
-
-
- -
-
-
- - - - - -
- -
-
-
-
-
-
-`; - -exports[`Header renders 4`] = ` -
- -
-
-
- -
- -
- -
- - -
- - - -
-
-
- -
- - - - -
- - - - -
- - -
- - - - - - - - - - -
- -
- - + + + + } + closePopover={[Function]} + data-test-subj="helpMenuButton" + display="inlineBlock" + hasArrow={true} + id="headerHelpMenu" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + repositionOnScroll={true} + > + +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ + +
+ +
+ +
+ + + +
+
+ +
+ + +
+ + + + + + + + + + +
+ +
+ - - - - } - closePopover={[Function]} - data-test-subj="helpMenuButton" - display="inlineBlock" - hasArrow={true} - id="headerHelpMenu" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - repositionOnScroll={true} - > - -
-
- - - -
-
-
-
- - -
-
- -
-
-
- - + +
+ +
+ +
+ +
+ - - + - - - - + close + + + + + + + + +
+ + + `; diff --git a/src/core/public/chrome/ui/header/_index.scss b/src/core/public/chrome/ui/header/_index.scss index e3b73abbcabc2..44cd864278325 100644 --- a/src/core/public/chrome/ui/header/_index.scss +++ b/src/core/public/chrome/ui/header/_index.scss @@ -1,14 +1,5 @@ @include euiHeaderAffordForFixed; -// TODO #64541 -// Delete this block -.chrHeaderWrapper:not(.headerWrapper) { - width: 100%; - position: fixed; - top: 0; - z-index: 10; -} - .chrHeaderHelpMenu__version { text-transform: none; } diff --git a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx index d4325e0caf88c..e33e76a45580e 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx @@ -59,7 +59,7 @@ function mockProps() { basePath: httpServiceMock.createSetupContract({ basePath: '/test' }).basePath, id: 'collapsibe-nav', isLocked: false, - isOpen: false, + isNavOpen: false, homeHref: '/', navLinks$: new BehaviorSubject([]), recentlyAccessed$: new BehaviorSubject([]), @@ -123,7 +123,7 @@ describe('CollapsibleNav', () => { const component = mount( { const component = mount( @@ -147,9 +147,9 @@ describe('CollapsibleNav', () => { clickGroup(component, 'kibana'); clickGroup(component, 'recentlyViewed'); expectShownNavLinksCount(component, 1); - component.setProps({ isOpen: false }); + component.setProps({ isNavOpen: false }); expectNavIsClosed(component); - component.setProps({ isOpen: true }); + component.setProps({ isNavOpen: true }); expectShownNavLinksCount(component, 1); }); @@ -160,14 +160,14 @@ describe('CollapsibleNav', () => { const component = mount( ); component.setProps({ closeNav: () => { - component.setProps({ isOpen: false }); + component.setProps({ isNavOpen: false }); onClose(); }, }); @@ -175,11 +175,11 @@ describe('CollapsibleNav', () => { component.find('[data-test-subj="collapsibleNavGroup-recentlyViewed"] a').simulate('click'); expect(onClose.callCount).toEqual(1); expectNavIsClosed(component); - component.setProps({ isOpen: true }); + component.setProps({ isNavOpen: true }); component.find('[data-test-subj="collapsibleNavGroup-kibana"] a').simulate('click'); expect(onClose.callCount).toEqual(2); expectNavIsClosed(component); - component.setProps({ isOpen: true }); + component.setProps({ isNavOpen: true }); component.find('[data-test-subj="collapsibleNavGroup-noCategory"] a').simulate('click'); expect(onClose.callCount).toEqual(3); expectNavIsClosed(component); diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index a5f42c0949562..01cdb9c38881a 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -79,7 +79,7 @@ interface Props { basePath: HttpStart['basePath']; id: string; isLocked: boolean; - isOpen: boolean; + isNavOpen: boolean; homeHref: string; navLinks$: Rx.Observable; recentlyAccessed$: Rx.Observable; @@ -94,7 +94,7 @@ export function CollapsibleNav({ basePath, id, isLocked, - isOpen, + isNavOpen, homeHref, storage = window.localStorage, onIsLockedUpdate, @@ -129,7 +129,7 @@ export function CollapsibleNav({ aria-label={i18n.translate('core.ui.primaryNav.screenReaderLabel', { defaultMessage: 'Primary', })} - isOpen={isOpen} + isOpen={isNavOpen} isDocked={isLocked} onClose={closeNav} > diff --git a/src/core/public/chrome/ui/header/header.test.tsx b/src/core/public/chrome/ui/header/header.test.tsx index 04eb256f30f37..7309b9af49388 100644 --- a/src/core/public/chrome/ui/header/header.test.tsx +++ b/src/core/public/chrome/ui/header/header.test.tsx @@ -21,7 +21,6 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { BehaviorSubject } from 'rxjs'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { NavType } from '.'; import { httpServiceMock } from '../../../http/http_service.mock'; import { applicationServiceMock } from '../../../mocks'; import { Header } from './header'; @@ -51,10 +50,10 @@ function mockProps() { helpExtension$: new BehaviorSubject(undefined), helpSupportUrl$: new BehaviorSubject(''), navControlsLeft$: new BehaviorSubject([]), + navControlsCenter$: new BehaviorSubject([]), navControlsRight$: new BehaviorSubject([]), basePath: http.basePath, isLocked$: new BehaviorSubject(false), - navType$: new BehaviorSubject('modern' as NavType), loadingCount$: new BehaviorSubject(0), onIsLockedUpdate: () => {}, }; @@ -71,7 +70,6 @@ describe('Header', () => { const isVisible$ = new BehaviorSubject(false); const breadcrumbs$ = new BehaviorSubject([{ text: 'test' }]); const isLocked$ = new BehaviorSubject(false); - const navType$ = new BehaviorSubject('modern' as NavType); const navLinks$ = new BehaviorSubject([ { id: 'kibana', title: 'kibana', baseUrl: '', href: '' }, ]); @@ -92,22 +90,19 @@ describe('Header', () => { navLinks$={navLinks$} recentlyAccessed$={recentlyAccessed$} isLocked$={isLocked$} - navType$={navType$} customNavLink$={customNavLink$} /> ); - expect(component).toMatchSnapshot(); + expect(component.find('EuiHeader').exists()).toBeFalsy(); act(() => isVisible$.next(true)); component.update(); - expect(component).toMatchSnapshot(); + expect(component.find('EuiHeader').exists()).toBeTruthy(); + expect(component.find('nav[aria-label="Primary"]').exists()).toBeFalsy(); act(() => isLocked$.next(true)); component.update(); - expect(component).toMatchSnapshot(); - - act(() => navType$.next('legacy' as NavType)); - component.update(); + expect(component.find('nav[aria-label="Primary"]').exists()).toBeTruthy(); expect(component).toMatchSnapshot(); }); }); diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index c0b3fc72930dc..7ec03ea4c6da6 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -23,8 +23,6 @@ import { EuiHeaderSectionItem, EuiHeaderSectionItemButton, EuiIcon, - EuiNavDrawer, - EuiShowFor, htmlIdGenerator, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -43,14 +41,14 @@ import { import { InternalApplicationStart } from '../../../application/types'; import { HttpStart } from '../../../http'; import { ChromeHelpExtension } from '../../chrome_service'; -import { NavType, OnIsLockedUpdate } from './'; +import { OnIsLockedUpdate } from './'; import { CollapsibleNav } from './collapsible_nav'; import { HeaderBadge } from './header_badge'; import { HeaderBreadcrumbs } from './header_breadcrumbs'; import { HeaderHelpMenu } from './header_help_menu'; import { HeaderLogo } from './header_logo'; import { HeaderNavControls } from './header_nav_controls'; -import { NavDrawer } from './nav_drawer'; +import { HeaderActionMenu } from './header_action_menu'; export interface HeaderProps { kibanaVersion: string; @@ -68,27 +66,14 @@ export interface HeaderProps { helpExtension$: Observable; helpSupportUrl$: Observable; navControlsLeft$: Observable; + navControlsCenter$: Observable; navControlsRight$: Observable; basePath: HttpStart['basePath']; isLocked$: Observable; - navType$: Observable; loadingCount$: ReturnType; onIsLockedUpdate: OnIsLockedUpdate; } -function renderMenuTrigger(toggleOpen: () => void) { - return ( - - - - ); -} - export function Header({ kibanaVersion, kibanaDocLink, @@ -98,125 +83,116 @@ export function Header({ homeHref, ...observables }: HeaderProps) { - const isVisible = useObservable(observables.isVisible$, true); - const navType = useObservable(observables.navType$, 'modern'); + const isVisible = useObservable(observables.isVisible$, false); const isLocked = useObservable(observables.isLocked$, false); - const [isOpen, setIsOpen] = useState(false); + const [isNavOpen, setIsNavOpen] = useState(false); if (!isVisible) { return ; } - const navDrawerRef = createRef(); const toggleCollapsibleNavRef = createRef(); const navId = htmlIdGenerator()(); - const className = classnames( - 'chrHeaderWrapper', // TODO #64541 - delete this - 'hide-for-sharing', - { - 'chrHeaderWrapper--navIsLocked': isLocked, - headerWrapper: navType === 'modern', - } - ); + const className = classnames('hide-for-sharing', 'headerGlobalNav'); return ( <>
- - - {navType === 'modern' ? ( +
+ , + ], + borders: 'none', + }, + { + ...(observables.navControlsCenter$ && { + items: [], + }), + borders: 'none', + }, + { + items: [ + , + , + ], + borders: 'none', + }, + ]} + /> + + + setIsOpen(!isOpen)} - aria-expanded={isOpen} - aria-pressed={isOpen} + onClick={() => setIsNavOpen(!isNavOpen)} + aria-expanded={isNavOpen} + aria-pressed={isNavOpen} aria-controls={navId} ref={toggleCollapsibleNavRef} > - ) : ( - // TODO #64541 - // Delete this block - - - {renderMenuTrigger(() => navDrawerRef.current?.toggleOpen())} - - - )} - - - + - - + + - + - + - - - - + + + + + + +
- -
-
- {navType === 'modern' ? ( - { - setIsOpen(false); - if (toggleCollapsibleNavRef.current) { - toggleCollapsibleNavRef.current.focus(); - } - }} - customNavLink$={observables.customNavLink$} - /> - ) : ( - // TODO #64541 - // Delete this block - - )} + { + setIsNavOpen(false); + if (toggleCollapsibleNavRef.current) { + toggleCollapsibleNavRef.current.focus(); + } + }} + customNavLink$={observables.customNavLink$} + />
); diff --git a/src/core/public/chrome/ui/header/header_action_menu.test.tsx b/src/core/public/chrome/ui/header/header_action_menu.test.tsx new file mode 100644 index 0000000000000..a124c8ab66969 --- /dev/null +++ b/src/core/public/chrome/ui/header/header_action_menu.test.tsx @@ -0,0 +1,139 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { mount, ReactWrapper } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import { BehaviorSubject } from 'rxjs'; +import { HeaderActionMenu } from './header_action_menu'; +import { MountPoint, UnmountCallback } from '../../../types'; + +type MockedUnmount = jest.MockedFunction; + +describe('HeaderActionMenu', () => { + let component: ReactWrapper; + let menuMount$: BehaviorSubject; + let unmounts: Record; + + beforeEach(() => { + menuMount$ = new BehaviorSubject(undefined); + unmounts = {}; + }); + + const refresh = () => { + new Promise(async (resolve) => { + if (component) { + act(() => { + component.update(); + }); + } + setImmediate(() => resolve(component)); // flushes any pending promises + }); + }; + + const createMountPoint = (id: string, content: string = id): MountPoint => ( + root + ): MockedUnmount => { + const container = document.createElement('DIV'); + // eslint-disable-next-line no-unsanitized/property + container.innerHTML = content; + root.appendChild(container); + const unmount = jest.fn(() => container.remove()); + unmounts[id] = unmount; + return unmount; + }; + + it('mounts the current value of the provided observable', async () => { + component = mount(); + + act(() => { + menuMount$.next(createMountPoint('FOO')); + }); + await refresh(); + + expect(component.html()).toMatchInlineSnapshot( + `"
FOO
"` + ); + }); + + it('clears the content of the component when emitting undefined', async () => { + component = mount(); + + act(() => { + menuMount$.next(createMountPoint('FOO')); + }); + await refresh(); + + expect(component.html()).toMatchInlineSnapshot( + `"
FOO
"` + ); + + act(() => { + menuMount$.next(undefined); + }); + await refresh(); + + expect(component.html()).toMatchInlineSnapshot( + `"
"` + ); + }); + + it('updates the dom when a new mount point is emitted', async () => { + component = mount(); + + act(() => { + menuMount$.next(createMountPoint('FOO')); + }); + await refresh(); + + expect(component.html()).toMatchInlineSnapshot( + `"
FOO
"` + ); + + act(() => { + menuMount$.next(createMountPoint('BAR')); + }); + await refresh(); + + expect(component.html()).toMatchInlineSnapshot( + `"
BAR
"` + ); + }); + + it('calls the previous mount point `unmount` when mounting a new mount point', async () => { + component = mount(); + + act(() => { + menuMount$.next(createMountPoint('FOO')); + }); + await refresh(); + + expect(Object.keys(unmounts)).toEqual(['FOO']); + expect(unmounts.FOO).not.toHaveBeenCalled(); + + act(() => { + menuMount$.next(createMountPoint('BAR')); + }); + await refresh(); + + expect(Object.keys(unmounts)).toEqual(['FOO', 'BAR']); + expect(unmounts.FOO).toHaveBeenCalledTimes(1); + expect(unmounts.BAR).not.toHaveBeenCalled(); + }); +}); diff --git a/src/core/public/chrome/ui/header/header_action_menu.tsx b/src/core/public/chrome/ui/header/header_action_menu.tsx new file mode 100644 index 0000000000000..3a7a09608ba66 --- /dev/null +++ b/src/core/public/chrome/ui/header/header_action_menu.tsx @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { FC, useRef, useLayoutEffect, useState } from 'react'; +import { Observable } from 'rxjs'; +import { MountPoint, UnmountCallback } from '../../../types'; + +interface HeaderActionMenuProps { + actionMenu$: Observable; +} + +export const HeaderActionMenu: FC = ({ actionMenu$ }) => { + // useObservable relies on useState under the hood. The signature is type SetStateAction = S | ((prevState: S) => S); + // As we got a Observable here, React's setState setter assume he's getting a `(prevState: S) => S` signature, + // therefore executing the mount method, causing everything to crash. + // piping the observable before calling `useObservable` causes the effect to always having a new reference, as + // the piped observable is a new instance on every render, causing infinite loops. + // this is why we use `useLayoutEffect` manually here. + const [mounter, setMounter] = useState<{ mount: MountPoint | undefined }>({ mount: undefined }); + useLayoutEffect(() => { + const s = actionMenu$.subscribe((value) => { + setMounter({ mount: value }); + }); + return () => s.unsubscribe(); + }, [actionMenu$]); + + const elementRef = useRef(null); + const unmountRef = useRef(null); + + useLayoutEffect(() => { + if (unmountRef.current) { + unmountRef.current(); + unmountRef.current = null; + } + + if (mounter.mount && elementRef.current) { + try { + unmountRef.current = mounter.mount(elementRef.current); + } catch (e) { + // TODO: use client-side logger when feature is implemented + // eslint-disable-next-line no-console + console.error(e); + } + } + }, [mounter]); + + return
; +}; diff --git a/src/core/public/chrome/ui/header/header_logo.tsx b/src/core/public/chrome/ui/header/header_logo.tsx index 9bec946b6b76e..dee93ecb1a804 100644 --- a/src/core/public/chrome/ui/header/header_logo.tsx +++ b/src/core/public/chrome/ui/header/header_logo.tsx @@ -105,6 +105,8 @@ export function HeaderLogo({ href, navigateToApp, ...observables }: Props) { aria-label={i18n.translate('core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel', { defaultMessage: 'Go to home page', })} - /> + > + Elastic + ); } diff --git a/src/core/public/chrome/ui/header/header_nav_controls.tsx b/src/core/public/chrome/ui/header/header_nav_controls.tsx index 0941f7b27b662..8d9d8097fd8e3 100644 --- a/src/core/public/chrome/ui/header/header_nav_controls.tsx +++ b/src/core/public/chrome/ui/header/header_nav_controls.tsx @@ -26,7 +26,7 @@ import { HeaderExtension } from './header_extension'; interface Props { navControls$: Observable; - side: 'left' | 'right'; + side?: 'left' | 'right'; } export function HeaderNavControls({ navControls$, side }: Props) { @@ -41,7 +41,10 @@ export function HeaderNavControls({ navControls$, side }: Props) { return ( <> {navControls.map((navControl: ChromeNavControl, index: number) => ( - + ))} diff --git a/src/core/public/chrome/ui/header/nav_drawer.tsx b/src/core/public/chrome/ui/header/nav_drawer.tsx deleted file mode 100644 index fc080fbafc303..0000000000000 --- a/src/core/public/chrome/ui/header/nav_drawer.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { EuiHorizontalRule, EuiNavDrawer, EuiNavDrawerGroup } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useObservable } from 'react-use'; -import { Observable } from 'rxjs'; -import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem, CoreStart } from '../../..'; -import { InternalApplicationStart } from '../../../application/types'; -import { HttpStart } from '../../../http'; -import { OnIsLockedUpdate } from './'; -import { createEuiListItem, createRecentNavLink } from './nav_link'; -import { RecentLinks } from './recent_links'; - -export interface Props { - appId$: InternalApplicationStart['currentAppId$']; - basePath: HttpStart['basePath']; - isLocked?: boolean; - navLinks$: Observable; - recentlyAccessed$: Observable; - navigateToApp: CoreStart['application']['navigateToApp']; - onIsLockedUpdate?: OnIsLockedUpdate; -} - -function NavDrawerRenderer( - { isLocked, onIsLockedUpdate, basePath, navigateToApp, ...observables }: Props, - ref: React.Ref -) { - const appId = useObservable(observables.appId$, ''); - const navLinks = useObservable(observables.navLinks$, []).filter((link) => !link.hidden); - const recentNavLinks = useObservable(observables.recentlyAccessed$, []).map((link) => - createRecentNavLink(link, navLinks, basePath) - ); - - return ( - - {RecentLinks({ recentNavLinks })} - - - createEuiListItem({ - link, - appId, - basePath, - navigateToApp, - dataTestSubj: 'navDrawerAppsMenuLink', - }) - )} - aria-label={i18n.translate('core.ui.primaryNavList.screenReaderLabel', { - defaultMessage: 'Primary navigation links', - })} - /> - - ); -} - -export const NavDrawer = React.forwardRef(NavDrawerRenderer); diff --git a/src/core/public/index.scss b/src/core/public/index.scss index c2ad2841d5a77..6ba9254e5d381 100644 --- a/src/core/public/index.scss +++ b/src/core/public/index.scss @@ -1,6 +1,6 @@ +@import './variables'; @import './core'; @import './chrome/index'; @import './overlays/index'; @import './rendering/index'; @import './styles/index'; - diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index d90b8f780b674..a9bea7bcfdef1 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -272,10 +272,13 @@ export interface ChromeNavControl { // @public export interface ChromeNavControls { + // @internal (undocumented) + getCenter$(): Observable; // @internal (undocumented) getLeft$(): Observable; // @internal (undocumented) getRight$(): Observable; + registerCenter(navControl: ChromeNavControl): void; registerLeft(navControl: ChromeNavControl): void; registerRight(navControl: ChromeNavControl): void; } @@ -345,7 +348,6 @@ export interface ChromeStart { getHelpExtension$(): Observable; getIsNavDrawerLocked$(): Observable; getIsVisible$(): Observable; - getNavType$(): Observable; navControls: ChromeNavControls; navLinks: ChromeNavLinks; recentlyAccessed: ChromeRecentlyAccessed; diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss index 211e9c03beea5..b806ac270331d 100644 --- a/src/core/public/rendering/_base.scss +++ b/src/core/public/rendering/_base.scss @@ -1,5 +1,4 @@ -@import '@elastic/eui/src/global_styling/variables/header'; -@import '@elastic/eui/src/components/nav_drawer/variables'; +@include euiHeaderAffordForFixed($kbnHeaderOffset); /** * stretch the root element of the Kibana application to set the base-size that @@ -12,74 +11,11 @@ min-height: 100%; } -// TODO #64541 -// Delete this block -.chrHeaderWrapper:not(.headerWrapper) ~ .app-wrapper { +.app-wrapper { display: flex; flex-flow: column nowrap; - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - z-index: 5; margin: 0 auto; - - &:not(.hidden-chrome) { - top: $euiHeaderChildSize; - left: $euiHeaderChildSize; - - // HOTFIX: Temporary fix for flyouts not inside portals - // SASSTODO: Find an actual solution - .euiFlyout { - top: $euiHeaderChildSize; - height: calc(100% - #{$euiHeaderChildSize}); - } - - @include euiBreakpoint('xs', 's') { - left: 0; - } - } - - /** - * 1. Dirty, but we need to override the .kbnGlobalNav-isOpen state - * when we're looking at the log-in screen. - */ - &.hidden-chrome { - left: 0 !important; /* 1 */ - } - - .navbar-right { - margin-right: 0; - } -} - -// TODO #64541 -// Delete this block -@include euiBreakpoint('xl') { - .chrHeaderWrapper--navIsLocked:not(.headerWrapper) { - ~ .app-wrapper:not(.hidden-chrome) { - // Shrink the content from the left so it's no longer overlapped by the nav drawer (ALWAYS) - left: $euiNavDrawerWidthExpanded !important; // sass-lint:disable-line no-important - transition: left $euiAnimSpeedFast $euiAnimSlightResistance; - } - } -} - -// TODO #64541 -// Remove .headerWrapper and header conditionals -.headerWrapper ~ .app-wrapper, -:not(header) ~ .app-wrapper { - display: flex; - flex-flow: column nowrap; - margin: 0 auto; - min-height: calc(100vh - #{$euiHeaderHeightCompensation}); - - @include internetExplorerOnly { - // IE specific bug with min-height in flex container, described in the next link - // https://github.com/philipwalton/flexbugs#3-min-height-on-a-flex-container-wont-apply-to-its-flex-items - height: calc(100vh - #{$euiHeaderHeightCompensation}); - } + min-height: calc(100vh - #{$kbnHeaderOffset}); &.hidden-chrome { min-height: 100vh; diff --git a/src/core/public/styles/_base.scss b/src/core/public/styles/_base.scss index 427c6b7735435..bfb07c1b51427 100644 --- a/src/core/public/styles/_base.scss +++ b/src/core/public/styles/_base.scss @@ -7,17 +7,6 @@ // Application Layout -// chrome-context -// TODO #64541 -// Delete this block -.chrHeaderWrapper:not(.headerWrapper) .content { - display: flex; - flex-flow: row nowrap; - width: 100%; - height: 100%; - overflow: hidden; -} - .application, .app-container { > * { diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx index bbc3f3632bf64..8c9e3847844d9 100644 --- a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx @@ -121,7 +121,7 @@ export class AdvancedSettingsComponent extends Component< setTimeout(() => { const id = hash.replace('#', ''); const element = document.getElementById(id); - const globalNavOffset = document.getElementById('headerGlobalNav')?.offsetHeight || 0; + const globalNavOffset = document.getElementById('globalHeaderBars')?.offsetHeight || 0; if (element) { element.scrollIntoView(); diff --git a/src/plugins/advanced_settings/public/management_app/components/_index.scss b/src/plugins/advanced_settings/public/management_app/components/_index.scss deleted file mode 100644 index d2d2e38947f76..0000000000000 --- a/src/plugins/advanced_settings/public/management_app/components/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './form/index'; diff --git a/src/plugins/advanced_settings/public/management_app/components/form/_form.scss b/src/plugins/advanced_settings/public/management_app/components/form/_form.scss deleted file mode 100644 index 8d768d200fdd2..0000000000000 --- a/src/plugins/advanced_settings/public/management_app/components/form/_form.scss +++ /dev/null @@ -1,15 +0,0 @@ -@import '@elastic/eui/src/global_styling/variables/header'; -@import '@elastic/eui/src/components/nav_drawer/variables'; - -// TODO #64541 -// Delete this whole file -.mgtAdvancedSettingsForm__bottomBar { - margin-left: $euiNavDrawerWidthCollapsed; - z-index: 9; // Puts it inuder the nav drawer when expanded - &--pushForNav { - margin-left: $euiNavDrawerWidthExpanded; - } - @include euiBreakpoint('xs', 's') { - margin-left: 0; - } -} diff --git a/src/plugins/advanced_settings/public/management_app/components/form/_index.scss b/src/plugins/advanced_settings/public/management_app/components/form/_index.scss deleted file mode 100644 index 2ef4ef1d20ce9..0000000000000 --- a/src/plugins/advanced_settings/public/management_app/components/form/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './form'; diff --git a/src/plugins/advanced_settings/public/management_app/components/form/form.tsx b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx index 5533f684870d9..0378d816fd2c3 100644 --- a/src/plugins/advanced_settings/public/management_app/components/form/form.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx @@ -18,7 +18,6 @@ */ import React, { PureComponent, Fragment } from 'react'; -import classNames from 'classnames'; import { EuiFlexGroup, @@ -45,7 +44,6 @@ import { Field, getEditableValue } from '../field'; import { FieldSetting, SettingsChanges, FieldState } from '../../types'; type Category = string; -const NAV_IS_LOCKED_KEY = 'core.chrome.isLocked'; interface FormProps { settings: Record; @@ -326,23 +324,8 @@ export class Form extends PureComponent { renderBottomBar = () => { const areChangesInvalid = this.areChangesInvalid(); - - // TODO #64541 - // Delete these classes - let bottomBarClasses = ''; - const pageNav = this.props.settings.general.find( - (setting) => setting.name === 'pageNavigation' - ); - - if (pageNav?.value === 'legacy') { - bottomBarClasses = classNames('mgtAdvancedSettingsForm__bottomBar', { - // eslint-disable-next-line @typescript-eslint/naming-convention - 'mgtAdvancedSettingsForm__bottomBar--pushForNav': - localStorage.getItem(NAV_IS_LOCKED_KEY) === 'true', - }); - } return ( - + ScopedHistory; + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; savedObjects: SavedObjectsStart; restorePreviousUrl: () => void; } diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index 92d6f2ed91dde..dd5eb1ee5ccaa 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -153,6 +153,7 @@ export class DashboardAppController { i18n: i18nStart, }, history, + setHeaderActionMenu, kbnUrlStateStorage, usageCollection, navigation, @@ -709,7 +710,13 @@ export class DashboardAppController { }; const dashboardNavBar = document.getElementById('dashboardChrome'); const updateNavBar = () => { - ReactDOM.render(, dashboardNavBar); + ReactDOM.render( + , + dashboardNavBar + ); }; const unmountNavBar = () => { diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 49584f62215ea..5a45229a58a7d 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -310,7 +310,7 @@ export class DashboardPlugin id: DashboardConstants.DASHBOARDS_ID, title: 'Dashboard', order: 2500, - euiIconType: 'dashboardApp', + euiIconType: 'logoKibana', defaultPath: `#${DashboardConstants.LANDING_PAGE_PATH}`, updater$: this.appStateUpdater, category: DEFAULT_APP_CATEGORIES.kibana, @@ -352,6 +352,7 @@ export class DashboardPlugin localStorage: new Storage(localStorage), usageCollection, scopedHistory: () => this.currentHistory!, + setHeaderActionMenu: params.setHeaderActionMenu, savedObjects, restorePreviousUrl, }; diff --git a/src/plugins/dev_tools/public/index.scss b/src/plugins/dev_tools/public/index.scss index c9d8dc7470656..4bec602ea42db 100644 --- a/src/plugins/dev_tools/public/index.scss +++ b/src/plugins/dev_tools/public/index.scss @@ -16,10 +16,6 @@ } } -.devApp { - height: 100%; -} - .devAppWrapper { display: flex; flex-direction: column; diff --git a/src/plugins/dev_tools/public/plugin.ts b/src/plugins/dev_tools/public/plugin.ts index fcc6a57361a94..8c4743c93fab3 100644 --- a/src/plugins/dev_tools/public/plugin.ts +++ b/src/plugins/dev_tools/public/plugin.ts @@ -60,7 +60,7 @@ export class DevToolsPlugin implements Plugin { defaultMessage: 'Dev Tools', }), updater$: this.appStateUpdater, - euiIconType: 'devToolsApp', + euiIconType: 'logoElastic', order: 9010, category: DEFAULT_APP_CATEGORIES.management, mount: async (params: AppMountParameters) => { diff --git a/src/plugins/discover/public/application/angular/discover.html b/src/plugins/discover/public/application/angular/discover.html index 94f13e1cd8132..e0e452aaa41c5 100644 --- a/src/plugins/discover/public/application/angular/discover.html +++ b/src/plugins/discover/public/application/angular/discover.html @@ -5,6 +5,7 @@

{{screenTitle}}

(uiActions = pluginUiActions); export const getUiActions = () => uiActions; +export const [getHeaderActionMenuMounter, setHeaderActionMenuMounter] = createGetterSetter< + AppMountParameters['setHeaderActionMenu'] +>('headerActionMenuMounter'); + export const [getUrlTracker, setUrlTracker] = createGetterSetter<{ setTrackedUrl: (url: string) => void; restorePreviousUrl: () => void; diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index b6960c8a20abf..dd9b57b568e42 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -54,6 +54,7 @@ import { setUrlTracker, setAngularModule, setServices, + setHeaderActionMenuMounter, setUiActions, setScopedHistory, getScopedHistory, @@ -240,7 +241,7 @@ export class DiscoverPlugin title: 'Discover', updater$: this.appStateUpdater.asObservable(), order: 1000, - euiIconType: 'discoverApp', + euiIconType: 'logoKibana', defaultPath: '#/', category: DEFAULT_APP_CATEGORIES.kibana, mount: async (params: AppMountParameters) => { @@ -251,6 +252,7 @@ export class DiscoverPlugin throw Error('Discover plugin method initializeInnerAngular is undefined'); } setScopedHistory(params.history); + setHeaderActionMenuMounter(params.setHeaderActionMenu); syncHistoryLocations(); appMounted(); const { @@ -264,6 +266,7 @@ export class DiscoverPlugin params.element.classList.add('dscAppWrapper'); const unmount = await renderApp(innerAngularName, params.element); return () => { + params.element.classList.remove('dscAppWrapper'); unmount(); appUnMounted(); }; diff --git a/src/plugins/kibana_legacy/public/angular/kbn_top_nav.js b/src/plugins/kibana_legacy/public/angular/kbn_top_nav.js index b3fbe8baadec3..c34e2487b32d4 100644 --- a/src/plugins/kibana_legacy/public/angular/kbn_top_nav.js +++ b/src/plugins/kibana_legacy/public/angular/kbn_top_nav.js @@ -74,6 +74,7 @@ export function createTopNavDirective() { export const createTopNavHelper = ({ TopNavMenu }) => (reactDirective) => { return reactDirective(TopNavMenu, [ ['config', { watchDepth: 'value' }], + ['setMenuMountPoint', { watchDepth: 'reference' }], ['disabledButtons', { watchDepth: 'reference' }], ['query', { watchDepth: 'reference' }], diff --git a/src/plugins/kibana_react/public/util/index.ts b/src/plugins/kibana_react/public/util/index.ts index a6f3f87535f46..030dd4aec0f12 100644 --- a/src/plugins/kibana_react/public/util/index.ts +++ b/src/plugins/kibana_react/public/util/index.ts @@ -19,3 +19,4 @@ export { toMountPoint } from './to_mount_point'; export { MountPointPortal } from './mount_point_portal'; +export { useIfMounted } from './utils'; diff --git a/src/plugins/kibana_react/public/util/mount_point_portal.tsx b/src/plugins/kibana_react/public/util/mount_point_portal.tsx index b762fba88791e..0249302763772 100644 --- a/src/plugins/kibana_react/public/util/mount_point_portal.tsx +++ b/src/plugins/kibana_react/public/util/mount_point_portal.tsx @@ -21,6 +21,7 @@ import { i18n } from '@kbn/i18n'; import React, { useRef, useEffect, useState, Component } from 'react'; import ReactDOM from 'react-dom'; import { MountPoint } from 'kibana/public'; +import { useIfMounted } from './utils'; interface MountPointPortalProps { setMountPoint: (mountPoint: MountPoint) => void; @@ -33,20 +34,30 @@ export const MountPointPortal: React.FC = ({ children, se // state used to force re-renders when the element changes const [shouldRender, setShouldRender] = useState(false); const el = useRef(); + const ifMounted = useIfMounted(); useEffect(() => { setMountPoint((element) => { - el.current = element; - setShouldRender(true); + ifMounted(() => { + el.current = element; + setShouldRender(true); + }); return () => { - setShouldRender(false); - el.current = undefined; + // the component can be unmounted from the dom before the portal target actually + // calls the `unmount` function. This is a no-op but show a scary warning in the console + // so we use a ifMounted effect to avoid it. + ifMounted(() => { + setShouldRender(false); + el.current = undefined; + }); }; }); return () => { - setShouldRender(false); - el.current = undefined; + ifMounted(() => { + setShouldRender(false); + el.current = undefined; + }); }; }, [setMountPoint]); diff --git a/src/plugins/kibana_react/public/util/utils.ts b/src/plugins/kibana_react/public/util/utils.ts new file mode 100644 index 0000000000000..23d4ac2ec4851 --- /dev/null +++ b/src/plugins/kibana_react/public/util/utils.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useCallback, useEffect, useRef } from 'react'; + +export const useIfMounted = () => { + const isMounted = useRef(true); + useEffect( + () => () => { + isMounted.current = false; + }, + [] + ); + + const ifMounted = useCallback((func) => { + if (isMounted.current && func) { + func(); + } + }, []); + + return ifMounted; +}; diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index 794bbc0d0613b..fafedf46c2bda 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -74,7 +74,7 @@ export class ManagementPlugin implements Plugin * > * { + // TEMP fix to adjust spacing between EuiHeaderList__list items + margin: 0 $euiSizeXS; } diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx index f21e5680e8f61..212bc19208ca8 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx @@ -164,7 +164,10 @@ describe('TopNavMenu', () => { // menu is rendered outside of the component expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0); - expect(portalTarget.getElementsByTagName('BUTTON').length).toBe(menuItems.length); + + const buttons = portalTarget.querySelectorAll('button'); + expect(buttons.length).toBe(menuItems.length + 1); // should be n+1 buttons in mobile for popover button + expect(buttons[buttons.length - 1].getAttribute('aria-label')).toBe('Open navigation menu'); // last button should be mobile button }); }); }); diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx index b284c60bac5de..a27addeb14393 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx @@ -18,7 +18,7 @@ */ import React, { ReactElement } from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiHeaderLinks } from '@elastic/eui'; import classNames from 'classnames'; import { MountPoint } from '../../../../core/public'; @@ -81,31 +81,16 @@ export function TopNavMenu(props: TopNavMenuProps): ReactElement | null { function renderItems(): ReactElement[] | null { if (!config || config.length === 0) return null; return config.map((menuItem: TopNavMenuData, i: number) => { - return ( - - - - ); + return ; }); } function renderMenu(className: string): ReactElement | null { if (!config || config.length === 0) return null; return ( - + {renderItems()} - + ); } diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx index b058ef0de448b..96a205b737273 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx @@ -19,9 +19,7 @@ import { upperFirst, isFunction } from 'lodash'; import React, { MouseEvent } from 'react'; -import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; - -import { EuiButton } from '@elastic/eui'; +import { EuiToolTip, EuiButton, EuiHeaderLink } from '@elastic/eui'; import { TopNavMenuData } from './top_nav_menu_data'; export function TopNavMenuItem(props: TopNavMenuData) { @@ -50,13 +48,13 @@ export function TopNavMenuItem(props: TopNavMenuData) { }; const btn = props.emphasize ? ( - + {upperFirst(props.label || props.id!)} ) : ( - + {upperFirst(props.label || props.id!)} - + ); const tooltip = getTooltip(); diff --git a/src/plugins/newsfeed/public/components/newsfeed_header_nav_button.tsx b/src/plugins/newsfeed/public/components/newsfeed_header_nav_button.tsx index 888b807b5296f..628cfde18b0d5 100644 --- a/src/plugins/newsfeed/public/components/newsfeed_header_nav_button.tsx +++ b/src/plugins/newsfeed/public/components/newsfeed_header_nav_button.tsx @@ -68,7 +68,7 @@ export const NewsfeedNavButton = ({ apiFetchResult }: Props) => { aria-label="Newsfeed menu" onClick={showFlyout} > - + {showBadge ? ( ▪ diff --git a/src/plugins/timelion/public/_app.scss b/src/plugins/timelion/public/_app.scss index 3142e1d23cf10..8b9078caba5a8 100644 --- a/src/plugins/timelion/public/_app.scss +++ b/src/plugins/timelion/public/_app.scss @@ -1,8 +1,5 @@ -@import '@elastic/eui/src/global_styling/variables/header'; - .timApp { position: relative; - min-height: calc(100vh - #{$euiHeaderChildSize}); background: $euiColorEmptyShade; [ng-click] { diff --git a/src/plugins/timelion/public/plugin.ts b/src/plugins/timelion/public/plugin.ts index a92ced20cb6d1..24d1b9eb3fb65 100644 --- a/src/plugins/timelion/public/plugin.ts +++ b/src/plugins/timelion/public/plugin.ts @@ -91,7 +91,7 @@ export class TimelionPlugin implements Plugin { title: 'Timelion', order: 8000, defaultPath: '#/', - euiIconType: 'timelionApp', + euiIconType: 'logoKibana', category: DEFAULT_APP_CATEGORIES.kibana, updater$: this.appStateUpdater.asObservable(), mount: async (params: AppMountParameters) => { diff --git a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx index 130561b6245ae..dfd3c09f51ed5 100644 --- a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx +++ b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx @@ -61,6 +61,7 @@ const TopNav = ({ }: VisualizeTopNavProps) => { const { services } = useKibana(); const { TopNavMenu } = services.navigation.ui; + const { setHeaderActionMenu } = services; const { embeddableHandler, vis } = visInstance; const [inspectorSession, setInspectorSession] = useState(); const openInspector = useCallback(() => { @@ -151,6 +152,7 @@ const TopNav = ({ void; scopedHistory: ScopedHistory; dashboard: DashboardStart; + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; } export interface SavedVisInstance { diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 95d5343d5d695..86159a13379a1 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -140,7 +140,7 @@ export class VisualizePlugin id: 'visualize', title: 'Visualize', order: 8000, - euiIconType: 'visualizeApp', + euiIconType: 'logoKibana', defaultPath: '#/', category: DEFAULT_APP_CATEGORIES.kibana, updater$: this.appStateUpdater.asObservable(), @@ -196,12 +196,14 @@ export class VisualizePlugin scopedHistory: params.history, restorePreviousUrl, dashboard: pluginsStart.dashboard, + setHeaderActionMenu: params.setHeaderActionMenu, }; params.element.classList.add('visAppWrapper'); const { renderApp } = await import('./application'); const unmount = renderApp(params, services); return () => { + params.element.classList.remove('visAppWrapper'); unlistenParentHistory(); unmount(); appUnMounted(); diff --git a/test/accessibility/apps/management.ts b/test/accessibility/apps/management.ts index b37500f5b15dd..08177f54ee881 100644 --- a/test/accessibility/apps/management.ts +++ b/test/accessibility/apps/management.ts @@ -64,8 +64,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await a11y.testAppSnapshot(); }); - // Will be enabling this and field formatters after this issue is addressed: https://github.com/elastic/kibana/issues/60030 - it.skip('Edit field type', async () => { + it('Edit field type', async () => { await PageObjects.settings.clickEditFieldFormat(); await a11y.testAppSnapshot(); }); diff --git a/test/functional/page_objects/time_picker.ts b/test/functional/page_objects/time_picker.ts index 8a726cee444c1..237dc8946ae0e 100644 --- a/test/functional/page_objects/time_picker.ts +++ b/test/functional/page_objects/time_picker.ts @@ -39,7 +39,7 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo const find = getService('find'); const browser = getService('browser'); const testSubjects = getService('testSubjects'); - const { header, common } = getPageObjects(['header', 'common']); + const { header } = getPageObjects(['header']); const kibanaServer = getService('kibanaServer'); class TimePicker { @@ -127,7 +127,7 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo await testSubjects.click('superDatePickerAbsoluteTab'); await testSubjects.click('superDatePickerAbsoluteDateInput'); await this.inputValue('superDatePickerAbsoluteDateInput', toTime); - await common.sleep(500); + await browser.pressKeys(browser.keys.ESCAPE); // close popover because sometimes browser can't find start input // set from time await testSubjects.click('superDatePickerstartDatePopoverButton'); diff --git a/test/functional/services/listing_table.ts b/test/functional/services/listing_table.ts index 3f9775d7a75f3..aebdc734cfb39 100644 --- a/test/functional/services/listing_table.ts +++ b/test/functional/services/listing_table.ts @@ -33,7 +33,7 @@ export function ListingTableProvider({ getService, getPageObjects }: FtrProvider */ class ListingTable { private async getSearchFilter() { - const searchFilter = await find.allByCssSelector('.euiFieldSearch'); + const searchFilter = await find.allByCssSelector('main .euiFieldSearch'); return searchFilter[0]; } diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 7d21f958bb80b..bdd0fbea35fa8 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -2,7 +2,10 @@ "prefix": "xpack", "paths": { "xpack.actions": "plugins/actions", - "xpack.uiActionsEnhanced": ["plugins/ui_actions_enhanced", "examples/ui_actions_enhanced_examples"], + "xpack.uiActionsEnhanced": [ + "plugins/ui_actions_enhanced", + "examples/ui_actions_enhanced_examples" + ], "xpack.alerts": "plugins/alerts", "xpack.eventLog": "plugins/event_log", "xpack.alertingBuiltins": "plugins/alerting_builtins", @@ -21,6 +24,7 @@ "xpack.features": "plugins/features", "xpack.fileUpload": "plugins/file_upload", "xpack.globalSearch": ["plugins/global_search"], + "xpack.globalSearchBar": ["plugins/global_search_bar"], "xpack.graph": ["plugins/graph"], "xpack.grokDebugger": "plugins/grokdebugger", "xpack.idxMgmt": "plugins/index_management", diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 33e6a4b50a742..51ac6673251fb 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -105,7 +105,7 @@ export class ApmPlugin implements Plugin { id: 'apm', title: 'APM', order: 8300, - euiIconType: 'apmApp', + euiIconType: 'logoObservability', appRoute: '/app/apm', icon: 'plugins/apm/public/icon.svg', category: DEFAULT_APP_CATEGORIES.observability, @@ -125,6 +125,7 @@ export class ApmPlugin implements Plugin { id: 'csm', title: 'Client Side Monitoring', order: 8500, + euiIconType: 'logoObservability', category: DEFAULT_APP_CATEGORIES.observability, async mount(params: AppMountParameters) { diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot index 87205b363e697..14791cd3d8b25 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot @@ -355,181 +355,3 @@ exports[`Storyshots components/Assets/Asset marker 1`] = `
`; - -exports[`Storyshots components/Assets/Asset redux 1`] = ` -
-
-
-
-
- Asset thumbnail -
-
-
-
-

- - airplane - -
- - - ( - 1 - kb) - - -

-
-
-
-
- - - -
-
- -
- -
-
-
-
- -
- -
-
-
-
- - - -
-
-
-
-
-`; diff --git a/x-pack/plugins/canvas/public/components/fullscreen/fullscreen.scss b/x-pack/plugins/canvas/public/components/fullscreen/fullscreen.scss index 7110a22408fe2..edd681a2c33e8 100644 --- a/x-pack/plugins/canvas/public/components/fullscreen/fullscreen.scss +++ b/x-pack/plugins/canvas/public/components/fullscreen/fullscreen.scss @@ -1,5 +1,5 @@ body.canvas-isFullscreen { // sass-lint:disable-line no-qualifying-elements - // following two rules are for overriding the header bar padding + // following two rules are for overriding the header bar padding &.euiBody--headerIsFixed { padding-top: 0; } @@ -13,28 +13,17 @@ body.canvas-isFullscreen { // sass-lint:disable-line no-qualifying-elements padding-left: 0 !important; // sass-lint:disable-line no-important } - // hide global loading indicator .kbnLoadingIndicator { display: none; } - // remove space for global nav elements - // TODO #64541 - // Can delete this block - .chrHeaderWrapper ~ .app-wrapper { - // Override locked nav at all breakpoints - left: 0 !important; // sass-lint:disable-line no-important - top: 0; - } - // set the background color .canvasLayout { background: $euiColorInk; } // hide all the interface parts - .chrHeaderWrapper, // K7 global top nav .canvasLayout__stageHeader, .canvasLayout__sidebar, .canvasLayout__footer, diff --git a/x-pack/plugins/canvas/public/lib/fullscreen.js b/x-pack/plugins/canvas/public/lib/fullscreen.js index bb9990d4f5457..6ad52d591c9c4 100644 --- a/x-pack/plugins/canvas/public/lib/fullscreen.js +++ b/x-pack/plugins/canvas/public/lib/fullscreen.js @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { platformService } from '../services'; + export const fullscreenClass = 'canvas-isFullscreen'; export function setFullscreen(fullscreen, doc = document) { @@ -13,8 +15,10 @@ export function setFullscreen(fullscreen, doc = document) { const isFullscreen = bodyClassList.contains(fullscreenClass); if (enabled && !isFullscreen) { + platformService.getService().setFullscreen(false); bodyClassList.add(fullscreenClass); } else if (!enabled && isFullscreen) { bodyClassList.remove(fullscreenClass); + platformService.getService().setFullscreen(true); } } diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index b02fb9db28612..fbca1e51bd5c6 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -88,7 +88,7 @@ export class CanvasPlugin category: DEFAULT_APP_CATEGORIES.kibana, id: 'canvas', title: 'Canvas', - euiIconType: 'canvasApp', + euiIconType: 'logoKibana', order: 3000, updater$: this.appUpdater, mount: async (params: AppMountParameters) => { diff --git a/x-pack/plugins/canvas/public/services/platform.ts b/x-pack/plugins/canvas/public/services/platform.ts index 92c378e9aa597..a27c57f32cf9f 100644 --- a/x-pack/plugins/canvas/public/services/platform.ts +++ b/x-pack/plugins/canvas/public/services/platform.ts @@ -10,6 +10,7 @@ import { IUiSettingsClient, ChromeBreadcrumb, IBasePath, + ChromeStart, } from '../../../../../src/core/public'; import { CanvasServiceFactory } from '.'; @@ -22,6 +23,7 @@ export interface PlatformService { getUISetting: (key: string, defaultValue?: any) => any; setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => void; setRecentlyAccessed: (link: string, label: string, id: string) => void; + setFullscreen: ChromeStart['setIsVisible']; // TODO: these should go away. We want thin accessors, not entire objects. // Entire objects are hard to mock, and hide our dependency on the external service. @@ -45,6 +47,7 @@ export const platformServiceFactory: CanvasServiceFactory = ( getUISetting: coreStart.uiSettings.get.bind(coreStart.uiSettings), setBreadcrumbs: coreStart.chrome.setBreadcrumbs, setRecentlyAccessed: coreStart.chrome.recentlyAccessed.add, + setFullscreen: coreStart.chrome.setIsVisible, // TODO: these should go away. We want thin accessors, not entire objects. // Entire objects are hard to mock, and hide our dependency on the external service. diff --git a/x-pack/plugins/canvas/public/services/stubs/platform.ts b/x-pack/plugins/canvas/public/services/stubs/platform.ts index 9ada579573502..bef3b2609537c 100644 --- a/x-pack/plugins/canvas/public/services/stubs/platform.ts +++ b/x-pack/plugins/canvas/public/services/stubs/platform.ts @@ -20,4 +20,5 @@ export const platformService: PlatformService = { getSavedObjects: noop, getSavedObjectsClient: noop, getUISettings: noop, + setFullscreen: noop, }; diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts index c6ca0d532ce07..d6a51e8b482d0 100644 --- a/x-pack/plugins/enterprise_search/common/constants.ts +++ b/x-pack/plugins/enterprise_search/common/constants.ts @@ -29,6 +29,7 @@ export const ENTERPRISE_SEARCH_PLUGIN = { }), ], URL: '/app/enterprise_search/overview', + LOGO: 'logoEnterpriseSearch', }; export const APP_SEARCH_PLUGIN = { diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index b735db7c49520..63f334811ce31 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -5,26 +5,25 @@ */ import { - Plugin, - PluginInitializerContext, + AppMountParameters, CoreSetup, CoreStart, - AppMountParameters, HttpSetup, + Plugin, + PluginInitializerContext, } from 'src/core/public'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { FeatureCatalogueCategory, HomePublicPluginSetup, } from '../../../../src/plugins/home/public'; -import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { LicensingPluginSetup } from '../../licensing/public'; - -import { IInitialAppData } from '../common/types'; import { - ENTERPRISE_SEARCH_PLUGIN, APP_SEARCH_PLUGIN, + ENTERPRISE_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN, } from '../common/constants'; +import { IInitialAppData } from '../common/types'; import { ExternalUrl, IExternalUrl } from './applications/shared/enterprise_search_url'; export interface ClientConfigType { @@ -73,6 +72,7 @@ export class EnterpriseSearchPlugin implements Plugin { core.application.register({ id: APP_SEARCH_PLUGIN.ID, title: APP_SEARCH_PLUGIN.NAME, + euiIconType: ENTERPRISE_SEARCH_PLUGIN.LOGO, appRoute: APP_SEARCH_PLUGIN.URL, category: DEFAULT_APP_CATEGORIES.enterpriseSearch, mount: async (params: AppMountParameters) => { @@ -92,6 +92,7 @@ export class EnterpriseSearchPlugin implements Plugin { core.application.register({ id: WORKPLACE_SEARCH_PLUGIN.ID, title: WORKPLACE_SEARCH_PLUGIN.NAME, + euiIconType: ENTERPRISE_SEARCH_PLUGIN.LOGO, appRoute: WORKPLACE_SEARCH_PLUGIN.URL, category: DEFAULT_APP_CATEGORIES.enterpriseSearch, mount: async (params: AppMountParameters) => { diff --git a/x-pack/plugins/global_search_bar/README.md b/x-pack/plugins/global_search_bar/README.md new file mode 100644 index 0000000000000..e16aac39e3f4e --- /dev/null +++ b/x-pack/plugins/global_search_bar/README.md @@ -0,0 +1,3 @@ +# Kibana GlobalSearchBar plugin + +The GlobalSearchBar plugin provides a search interface for navigating Kibana. (It is the UI to the GlobalSearch plugin.) diff --git a/x-pack/plugins/global_search_bar/kibana.json b/x-pack/plugins/global_search_bar/kibana.json new file mode 100644 index 0000000000000..2d4ffa34d346f --- /dev/null +++ b/x-pack/plugins/global_search_bar/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "globalSearchBar", + "version": "8.0.0", + "kibanaVersion": "kibana", + "server": false, + "ui": true, + "requiredPlugins": ["globalSearch"], + "optionalPlugins": [], + "configPath": ["xpack", "global_search_bar"] +} diff --git a/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap b/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap new file mode 100644 index 0000000000000..f7e4bfd1c961c --- /dev/null +++ b/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SearchBar correctly filters and sorts results 1`] = ` +Array [ + Object { + "append": undefined, + "className": "euiSelectableTemplateSitewide__listItem", + "key": "Canvas", + "label": "Canvas", + "prepend": undefined, + "title": "Canvasundefinedundefined", + "url": "/app/test/Canvas", + }, + Object { + "append": undefined, + "className": "euiSelectableTemplateSitewide__listItem", + "key": "Discover", + "label": "Discover", + "prepend": undefined, + "title": "Discoverundefinedundefined", + "url": "/app/test/Discover", + }, + Object { + "append": undefined, + "className": "euiSelectableTemplateSitewide__listItem", + "key": "Graph", + "label": "Graph", + "prepend": undefined, + "title": "Graphundefinedundefined", + "url": "/app/test/Graph", + }, +] +`; + +exports[`SearchBar correctly filters and sorts results 2`] = ` +Array [ + Object { + "append": undefined, + "className": "euiSelectableTemplateSitewide__listItem", + "key": "Discover", + "label": "Discover", + "prepend": undefined, + "title": "Discoverundefinedundefined", + "url": "/app/test/Discover", + }, + Object { + "append": undefined, + "className": "euiSelectableTemplateSitewide__listItem", + "key": "My Dashboard", + "label": "My Dashboard", + "meta": Array [ + Object { + "text": "Test", + }, + ], + "prepend": undefined, + "title": "My Dashboard • Test", + "url": "/app/test/My Dashboard", + }, +] +`; + +exports[`SearchBar supports keyboard shortcuts 1`] = ` + +`; diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx new file mode 100644 index 0000000000000..0d1e8725b4911 --- /dev/null +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { wait } from '@testing-library/react'; +import { of } from 'rxjs'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { + GlobalSearchBatchedResults, + GlobalSearchPluginStart, + GlobalSearchResult, +} from '../../../global_search/public'; +import { globalSearchPluginMock } from '../../../global_search/public/mocks'; +import { SearchBar } from '../components/search_bar'; + +type Result = { id: string; type: string } | string; + +const createResult = (result: Result): GlobalSearchResult => { + const id = typeof result === 'string' ? result : result.id; + const type = typeof result === 'string' ? 'application' : result.type; + + return { + id, + type, + title: id, + url: `/app/test/${id}`, + score: 42, + }; +}; + +const createBatch = (...results: Result[]): GlobalSearchBatchedResults => ({ + results: results.map(createResult), +}); + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => 'mockId', +})); + +const getSelectableProps: any = (component: any) => component.find('EuiSelectable').props(); +const getSearchProps: any = (component: any) => component.find('EuiFieldSearch').props(); + +describe('SearchBar', () => { + let searchService: GlobalSearchPluginStart; + let findSpy: jest.SpyInstance; + + beforeEach(() => { + searchService = globalSearchPluginMock.createStartContract(); + findSpy = jest.spyOn(searchService, 'find'); + jest.useFakeTimers(); + }); + + it('correctly filters and sorts results', async () => { + const navigate = jest.fn(); + findSpy + .mockReturnValueOnce( + of( + createBatch('Discover', 'Canvas'), + createBatch({ id: 'Visualize', type: 'test' }, 'Graph') + ) + ) + .mockReturnValueOnce(of(createBatch('Discover', { id: 'My Dashboard', type: 'test' }))); + + const component = mountWithIntl( + + ); + + expect(findSpy).toHaveBeenCalledTimes(0); + component.find('input[data-test-subj="header-search"]').simulate('focus'); + jest.runAllTimers(); + component.update(); + expect(findSpy).toHaveBeenCalledTimes(1); + expect(findSpy).toHaveBeenCalledWith('', {}); + expect(getSelectableProps(component).options).toMatchSnapshot(); + await wait(() => getSearchProps(component).onSearch('d')); + jest.runAllTimers(); + component.update(); + expect(getSelectableProps(component).options).toMatchSnapshot(); + expect(findSpy).toHaveBeenCalledTimes(2); + expect(findSpy).toHaveBeenCalledWith('d', {}); + }); + + it('supports keyboard shortcuts', () => { + mountWithIntl(); + + const searchEvent = new KeyboardEvent('keydown', { + key: '/', + ctrlKey: true, + metaKey: true, + } as any); + window.dispatchEvent(searchEvent); + + expect(document.activeElement).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx new file mode 100644 index 0000000000000..25ca1b07321c7 --- /dev/null +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx @@ -0,0 +1,217 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiBadge, + EuiFlexGroup, + EuiFlexItem, + EuiSelectableTemplateSitewide, + EuiSelectableTemplateSitewideOption, + EuiText, + EuiSelectableMessage, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ApplicationStart } from 'kibana/public'; +import React, { useCallback, useState } from 'react'; +import useDebounce from 'react-use/lib/useDebounce'; +import useEvent from 'react-use/lib/useEvent'; +import useMountedState from 'react-use/lib/useMountedState'; +import { GlobalSearchPluginStart, GlobalSearchResult } from '../../../global_search/public'; + +interface Props { + globalSearch: GlobalSearchPluginStart['find']; + navigateToUrl: ApplicationStart['navigateToUrl']; +} + +const clearField = (field: HTMLInputElement) => { + const nativeInputValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); + const nativeInputValueSetter = nativeInputValue ? nativeInputValue.set : undefined; + if (nativeInputValueSetter) { + nativeInputValueSetter.call(field, ''); + } + + field.dispatchEvent(new Event('change')); +}; + +const cleanMeta = (str: string) => (str.charAt(0).toUpperCase() + str.slice(1)).replace(/-/g, ' '); +const blurEvent = new FocusEvent('blur'); + +export function SearchBar({ globalSearch, navigateToUrl }: Props) { + const isMounted = useMountedState(); + const [searchValue, setSearchValue] = useState(''); + const [searchRef, setSearchRef] = useState(null); + const [options, _setOptions] = useState([] as EuiSelectableTemplateSitewideOption[]); + const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0; + + const setOptions = useCallback( + (_options: GlobalSearchResult[]) => { + if (!isMounted()) return; + + _setOptions([ + ..._options.map((option) => ({ + key: option.id, + label: option.title, + url: option.url, + ...(option.icon && { icon: { type: option.icon } }), + ...(option.type && + option.type !== 'application' && { meta: [{ text: cleanMeta(option.type) }] }), + })), + ]); + }, + [isMounted, _setOptions] + ); + + useDebounce( + () => { + let arr: GlobalSearchResult[] = []; + globalSearch(searchValue, {}).subscribe({ + next: ({ results }) => { + if (searchValue.length > 0) { + arr = [...results, ...arr].sort((a, b) => { + if (a.score < b.score) return 1; + if (a.score > b.score) return -1; + return 0; + }); + setOptions(arr); + return; + } + + // if searchbar is empty, filter to only applications and sort alphabetically + results = results.filter(({ type }: GlobalSearchResult) => type === 'application'); + + arr = [...results, ...arr].sort((a, b) => { + const titleA = a.title.toUpperCase(); // ignore upper and lowercase + const titleB = b.title.toUpperCase(); // ignore upper and lowercase + if (titleA < titleB) return -1; + if (titleA > titleB) return 1; + return 0; + }); + + setOptions(arr); + }, + error: () => { + // TODO #74430 - add telemetry to see if errors are happening + // Not doing anything on error right now because it'll either just show the previous + // results or empty results which is basically what we want anyways + }, + complete: () => {}, + }); + }, + 250, + [searchValue] + ); + + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === '/' && (isMac ? event.metaKey : event.ctrlKey)) { + if (searchRef) { + event.preventDefault(); + searchRef.focus(); + } + } + }; + + const onChange = (selected: EuiSelectableTemplateSitewideOption[]) => { + // @ts-ignore - ts error is "union type is too complex to express" + const { url } = selected.find(({ checked }) => checked === 'on'); + + navigateToUrl(url); + (document.activeElement as HTMLElement).blur(); + if (searchRef) { + clearField(searchRef); + searchRef.dispatchEvent(blurEvent); + } + }; + + useEvent('keydown', onKeyDown); + + return ( + +

+ +

+

+ +

+ + } + popoverFooter={ + + + + + + ), + commandDescription: ( + + + {isMac ? ( + + ) : ( + + )} + + + ), + }} + /> + Shortcut, + how: ( + + {isMac ? 'Command + /' : 'Control + /'} + + ), + }} + /> + + + } + /> + ); +} diff --git a/x-pack/plugins/global_search_bar/public/index.ts b/x-pack/plugins/global_search_bar/public/index.ts new file mode 100644 index 0000000000000..9e11c43ce872c --- /dev/null +++ b/x-pack/plugins/global_search_bar/public/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializer } from 'src/core/public'; +import { GlobalSearchBarPlugin } from './plugin'; + +export const plugin: PluginInitializer<{}, {}, {}, {}> = () => new GlobalSearchBarPlugin(); diff --git a/x-pack/plugins/global_search_bar/public/plugin.tsx b/x-pack/plugins/global_search_bar/public/plugin.tsx new file mode 100644 index 0000000000000..0c8cc2e64726a --- /dev/null +++ b/x-pack/plugins/global_search_bar/public/plugin.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreStart, Plugin } from 'src/core/public'; +import React from 'react'; +import { I18nProvider } from '@kbn/i18n/react'; +import ReactDOM from 'react-dom'; +import { ApplicationStart } from 'kibana/public'; +import { SearchBar } from '../public/components/search_bar'; +import { GlobalSearchPluginStart } from '../../global_search/public'; + +export interface GlobalSearchBarPluginStartDeps { + globalSearch: GlobalSearchPluginStart; +} + +export class GlobalSearchBarPlugin implements Plugin<{}, {}> { + public async setup() { + return {}; + } + + public start(core: CoreStart, { globalSearch }: GlobalSearchBarPluginStartDeps) { + core.chrome.navControls.registerCenter({ + order: 1000, + mount: (target) => this.mount(target, globalSearch, core.application.navigateToUrl), + }); + return {}; + } + + private mount( + targetDomElement: HTMLElement, + globalSearch: GlobalSearchPluginStart, + navigateToUrl: ApplicationStart['navigateToUrl'] + ) { + ReactDOM.render( + + + , + targetDomElement + ); + + return () => ReactDOM.unmountComponentAtNode(targetDomElement); + } +} diff --git a/x-pack/plugins/graph/public/angular/templates/index.html b/x-pack/plugins/graph/public/angular/templates/index.html index 50385008d7b2b..10bbb2e8ec6c7 100644 --- a/x-pack/plugins/graph/public/angular/templates/index.html +++ b/x-pack/plugins/graph/public/angular/templates/index.html @@ -1,6 +1,6 @@
- +
diff --git a/x-pack/plugins/graph/public/app.js b/x-pack/plugins/graph/public/app.js index fd2b96e0570f6..183f8bddead11 100644 --- a/x-pack/plugins/graph/public/app.js +++ b/x-pack/plugins/graph/public/app.js @@ -53,6 +53,7 @@ export function initGraphApp(angularModule, deps) { graphSavePolicy, overlays, savedObjects, + setHeaderActionMenu, } = deps; const app = angularModule; @@ -465,6 +466,7 @@ export function initGraphApp(angularModule, deps) { }; // ===== Menubar configuration ========= + $scope.setHeaderActionMenu = setHeaderActionMenu; $scope.topNavMenu = []; $scope.topNavMenu.push({ key: 'new', diff --git a/x-pack/plugins/graph/public/application.ts b/x-pack/plugins/graph/public/application.ts index a9ba464016157..90e87ff4ec85e 100644 --- a/x-pack/plugins/graph/public/application.ts +++ b/x-pack/plugins/graph/public/application.ts @@ -27,6 +27,7 @@ import { SavedObjectsClientContract, ToastsStart, OverlayStart, + AppMountParameters, } from 'kibana/public'; // @ts-ignore import { initGraphApp } from './app'; @@ -73,6 +74,7 @@ export interface GraphDependencies { overlays: OverlayStart; savedObjects: SavedObjectsStart; kibanaLegacy: KibanaLegacyStart; + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; } export const renderApp = ({ appBasePath, element, kibanaLegacy, ...deps }: GraphDependencies) => { diff --git a/x-pack/plugins/graph/public/components/_app.scss b/x-pack/plugins/graph/public/components/_app.scss index f6984f5369a30..b9b23e596a05e 100644 --- a/x-pack/plugins/graph/public/components/_app.scss +++ b/x-pack/plugins/graph/public/components/_app.scss @@ -1,3 +1,3 @@ .gphGraph__bar { - margin: 0px $euiSizeS $euiSizeS $euiSizeS; + margin: $euiSizeS; } diff --git a/x-pack/plugins/graph/public/plugin.ts b/x-pack/plugins/graph/public/plugin.ts index e452b74ab35e8..6cf9d8be07bc9 100644 --- a/x-pack/plugins/graph/public/plugin.ts +++ b/x-pack/plugins/graph/public/plugin.ts @@ -74,7 +74,7 @@ export class GraphPlugin title: 'Graph', order: 6000, appRoute: '/app/graph', - euiIconType: 'graphApp', + euiIconType: 'logoKibana', category: DEFAULT_APP_CATEGORIES.kibana, mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart] = await core.getStartServices(); diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 8a1264d254e40..66715b3fee28b 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -52,7 +52,7 @@ export class Plugin implements InfraClientPluginClass { title: i18n.translate('xpack.infra.logs.pluginTitle', { defaultMessage: 'Logs', }), - euiIconType: 'logsApp', + euiIconType: 'logoObservability', order: 8100, appRoute: '/app/logs', category: DEFAULT_APP_CATEGORIES.observability, @@ -70,7 +70,7 @@ export class Plugin implements InfraClientPluginClass { title: i18n.translate('xpack.infra.metrics.pluginTitle', { defaultMessage: 'Metrics', }), - euiIconType: 'metricsApp', + euiIconType: 'logoObservability', order: 8200, appRoute: '/app/metrics', category: DEFAULT_APP_CATEGORIES.observability, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx index 30294779d1a3d..7da8330740532 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx @@ -5,11 +5,11 @@ */ import React from 'react'; import styled from 'styled-components'; -import { EuiTabs, EuiTab, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiButtonEmpty } from '@elastic/eui'; +import { EuiTabs, EuiTab, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { Section } from '../sections'; import { AlphaMessaging, SettingFlyout } from '../components'; -import { useLink, useConfig, useCore } from '../hooks'; +import { useLink, useConfig } from '../hooks'; interface Props { showSettings?: boolean; @@ -42,7 +42,6 @@ export const DefaultLayout: React.FunctionComponent = ({ }) => { const { getHref } = useLink(); const { fleet } = useConfig(); - const { uiSettings } = useCore(); const [isSettingsFlyoutOpen, setIsSettingsFlyoutOpen] = React.useState(false); return ( @@ -58,11 +57,6 @@ export const DefaultLayout: React.FunctionComponent = ({