From f95951acfbf1f4ebc8a13ca5db2cd343bd0d01db Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Thu, 23 Jul 2020 16:49:55 -0400 Subject: [PATCH 01/96] [Security Solution][Detections] Fixes exception modal bugs (#73119) --- .../exceptions/add_exception_modal/index.tsx | 10 ++-- .../common/components/exceptions/helpers.tsx | 52 ++++++++----------- .../alerts_table/default_config.tsx | 1 + 3 files changed, 30 insertions(+), 33 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index 0d93a1ea88714..d2fec1f34755f 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -296,9 +296,13 @@ export const AddExceptionModal = memo(function AddExceptionModal({

{i18n.ADD_EXCEPTION_FETCH_ERROR}

)} - {fetchOrCreateListError === false && isLoadingExceptionList === true && ( - - )} + {fetchOrCreateListError === false && + (isLoadingExceptionList || + isIndexPatternLoading || + isSignalIndexLoading || + isSignalIndexPatternLoading) && ( + + )} {fetchOrCreateListError === false && !isSignalIndexLoading && !isSignalIndexPatternLoading && diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index 384badefc34aa..a54f20f56d56f 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -383,6 +383,7 @@ export const defaultEndpointExceptionItems = ( fieldName: 'file.Ext.code_signature.trusted', }); const [sha1Hash] = getMappedNonEcsValue({ data: alertData, fieldName: 'file.hash.sha1' }); + const [eventCode] = getMappedNonEcsValue({ data: alertData, fieldName: 'event.code' }); const namespaceType = 'agnostic'; return [ @@ -390,49 +391,40 @@ export const defaultEndpointExceptionItems = ( ...getNewExceptionItem({ listType, listId, namespaceType, ruleName }), entries: [ { - field: 'file.path', - operator: 'included', - type: 'match', - value: filePath ?? '', - }, - ], - }, - { - ...getNewExceptionItem({ listType, listId, namespaceType, ruleName }), - entries: [ - { - field: 'file.Ext.code_signature.subject_name', - operator: 'included', - type: 'match', - value: signatureSigner ?? '', + field: 'file.Ext.code_signature', + type: 'nested', + entries: [ + { + field: 'subject_name', + operator: 'included', + type: 'match', + value: signatureSigner ?? '', + }, + { + field: 'trusted', + operator: 'included', + type: 'match', + value: signatureTrusted ?? '', + }, + ], }, { - field: 'file.Ext.code_signature.trusted', + field: 'file.path', operator: 'included', type: 'match', - value: signatureTrusted ?? '', + value: filePath ?? '', }, - ], - }, - { - ...getNewExceptionItem({ listType, listId, namespaceType, ruleName }), - entries: [ { field: 'file.hash.sha1', operator: 'included', type: 'match', value: sha1Hash ?? '', }, - ], - }, - { - ...getNewExceptionItem({ listType, listId, namespaceType, ruleName }), - entries: [ { - field: 'event.category', + field: 'event.code', operator: 'included', - type: 'match_any', - value: getMappedNonEcsValue({ data: alertData, fieldName: 'event.category' }), + type: 'match', + value: eventCode ?? '', }, ], }, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index a4ce6c0200eb3..010129d2d4593 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -204,6 +204,7 @@ export const requiredFieldsForActions = [ 'file.Ext.code_signature.trusted', 'file.hash.sha1', 'host.os.family', + 'event.code', ]; interface AlertActionArgs { From 75beedbadde23bdca3385fc8115f93aa5ab47315 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Thu, 23 Jul 2020 17:10:38 -0400 Subject: [PATCH 02/96] [Canvas] Provide service stubs (#72708) Co-authored-by: Elastic Machine --- x-pack/plugins/canvas/public/application.tsx | 21 ++--- .../canvas/public/components/app/index.js | 6 +- .../components/app/track_route_change.js | 23 ----- .../components/element_content/index.js | 8 +- .../components/embeddable_flyout/flyout.tsx | 85 +++++++++---------- .../components/embeddable_flyout/index.tsx | 14 +-- .../public/components/expression/index.js | 6 +- .../render_with_fn/render_with_fn.tsx | 5 +- .../components/saved_elements_modal/index.ts | 13 ++- .../public/components/var_config/index.tsx | 21 ++--- .../workpad_header/element_menu/index.tsx | 2 - .../workpad_header/share_menu/index.ts | 23 ++--- .../workpad_header/view_menu/index.ts | 2 - .../public/components/workpad_loader/index.js | 20 ++--- .../components/workpad_templates/index.tsx | 9 +- .../plugins/canvas/public/lib/breadcrumbs.ts | 3 +- .../public/lib/custom_element_service.ts | 2 +- .../canvas/public/lib/documentation_links.ts | 16 ++-- .../plugins/canvas/public/lib/es_service.ts | 6 +- .../canvas/public/lib/template_service.ts | 2 +- .../canvas/public/lib/workpad_service.js | 7 +- .../canvas/public/services/context.tsx | 58 +++++++++++++ .../canvas/public/services/embeddables.ts | 21 +++++ .../canvas/public/services/expressions.ts | 1 - .../plugins/canvas/public/services/index.ts | 19 ++++- .../canvas/public/services/nav_link.ts | 8 +- .../plugins/canvas/public/services/notify.ts | 2 +- .../canvas/public/services/platform.ts | 53 +++++++++--- .../public/services/stubs/embeddables.ts | 12 +++ .../public/services/stubs/expressions.ts | 27 ++++++ .../canvas/public/services/stubs/index.ts | 28 ++++++ .../canvas/public/services/stubs/nav_link.ts | 13 +++ .../canvas/public/services/stubs/notify.ts | 16 ++++ .../canvas/public/services/stubs/platform.ts | 23 +++++ .../canvas/public/state/initial_state.js | 4 +- .../canvas/public/state/reducers/workpad.js | 6 +- x-pack/plugins/canvas/storybook/config.js | 2 + 37 files changed, 394 insertions(+), 193 deletions(-) delete mode 100644 x-pack/plugins/canvas/public/components/app/track_route_change.js create mode 100644 x-pack/plugins/canvas/public/services/context.tsx create mode 100644 x-pack/plugins/canvas/public/services/embeddables.ts create mode 100644 x-pack/plugins/canvas/public/services/stubs/embeddables.ts create mode 100644 x-pack/plugins/canvas/public/services/stubs/expressions.ts create mode 100644 x-pack/plugins/canvas/public/services/stubs/index.ts create mode 100644 x-pack/plugins/canvas/public/services/stubs/nav_link.ts create mode 100644 x-pack/plugins/canvas/public/services/stubs/notify.ts create mode 100644 x-pack/plugins/canvas/public/services/stubs/platform.ts diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx index b2c836fe4805f..0bbf449ce11f9 100644 --- a/x-pack/plugins/canvas/public/application.tsx +++ b/x-pack/plugins/canvas/public/application.tsx @@ -31,7 +31,7 @@ import { init as initStatsReporter } from './lib/ui_metric'; import { CapabilitiesStrings } from '../i18n'; -import { startServices, services } from './services'; +import { startServices, services, ServicesProvider } from './services'; // @ts-expect-error untyped local import { createHistory, destroyHistory } from './lib/history_provider'; // @ts-expect-error untyped local @@ -52,19 +52,16 @@ export const renderApp = ( ) => { element.classList.add('canvas'); element.classList.add('canvasContainerWrapper'); - const canvasServices = Object.entries(services).reduce((reduction, [key, provider]) => { - reduction[key] = provider.getService(); - - return reduction; - }, {} as Record); ReactDOM.render( - - - - - - + + + + + + + + , element ); diff --git a/x-pack/plugins/canvas/public/components/app/index.js b/x-pack/plugins/canvas/public/components/app/index.js index a1e3b9c09554a..9a6e8719e7f40 100644 --- a/x-pack/plugins/canvas/public/components/app/index.js +++ b/x-pack/plugins/canvas/public/components/app/index.js @@ -8,7 +8,7 @@ import { connect } from 'react-redux'; import { compose, withProps } from 'recompose'; import { getAppReady, getBasePath } from '../../state/selectors/app'; import { appReady, appError } from '../../state/actions/app'; -import { withKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { withServices } from '../../services'; import { App as Component } from './app'; @@ -45,8 +45,8 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { export const App = compose( connect(mapStateToProps, mapDispatchToProps, mergeProps), - withKibana, + withServices, withProps((props) => ({ - onRouteChange: props.kibana.services.canvas.navLink.updatePath, + onRouteChange: props.services.navLink.updatePath, })) )(Component); diff --git a/x-pack/plugins/canvas/public/components/app/track_route_change.js b/x-pack/plugins/canvas/public/components/app/track_route_change.js deleted file mode 100644 index 2886aa868eb9e..0000000000000 --- a/x-pack/plugins/canvas/public/components/app/track_route_change.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get } from 'lodash'; -import { getWindow } from '../../lib/get_window'; -import { CANVAS_APP } from '../../../common/lib/constants'; -import { platformService } from '../../services'; - -export function trackRouteChange() { - const basePath = platformService.getService().coreStart.http.basePath.get(); - - platformService - .getService() - .startPlugins.__LEGACY.trackSubUrlForApp( - CANVAS_APP, - platformService - .getService() - .startPlugins.__LEGACY.absoluteToParsedUrl(get(getWindow(), 'location.href'), basePath) - ); -} diff --git a/x-pack/plugins/canvas/public/components/element_content/index.js b/x-pack/plugins/canvas/public/components/element_content/index.js index a138c3acb8ec7..63ece6ac32812 100644 --- a/x-pack/plugins/canvas/public/components/element_content/index.js +++ b/x-pack/plugins/canvas/public/components/element_content/index.js @@ -8,8 +8,8 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { compose, withProps } from 'recompose'; import { get } from 'lodash'; +import { withServices } from '../../services'; import { getSelectedPage, getPageById } from '../../state/selectors/workpad'; -import { withKibana } from '../../../../../../src/plugins/kibana_react/public'; import { ElementContent as Component } from './element_content'; const mapStateToProps = (state) => ({ @@ -18,9 +18,9 @@ const mapStateToProps = (state) => ({ export const ElementContent = compose( connect(mapStateToProps), - withKibana, - withProps(({ renderable, kibana }) => ({ - renderFunction: kibana.services.expressions.getRenderer(get(renderable, 'as')), + withServices, + withProps(({ renderable, services }) => ({ + renderFunction: services.expressions.getRenderer(get(renderable, 'as')), })) )(Component); diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx index df9dad3e7f678..0b5bd8adf8cb9 100644 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx @@ -4,15 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { FC } from 'react'; import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody, EuiTitle } from '@elastic/eui'; import { SavedObjectFinderUi, SavedObjectMetaData, } from '../../../../../../src/plugins/saved_objects/public/'; import { ComponentStrings } from '../../../i18n'; -import { CoreStart } from '../../../../../../src/core/public'; -import { CanvasStartDeps } from '../../plugin'; +import { useServices } from '../../services'; const { AddEmbeddableFlyout: strings } = ComponentStrings; @@ -20,14 +19,16 @@ export interface Props { onClose: () => void; onSelect: (id: string, embeddableType: string) => void; availableEmbeddables: string[]; - savedObjects: CoreStart['savedObjects']; - uiSettings: CoreStart['uiSettings']; - getEmbeddableFactories: CanvasStartDeps['embeddable']['getEmbeddableFactories']; } -export class AddEmbeddableFlyout extends React.Component { - onAddPanel = (id: string, savedObjectType: string, name: string) => { - const embeddableFactories = this.props.getEmbeddableFactories(); +export const AddEmbeddableFlyout: FC = ({ onSelect, availableEmbeddables, onClose }) => { + const services = useServices(); + const { embeddables, platform } = services; + const { getEmbeddableFactories } = embeddables; + const { getSavedObjects, getUISettings } = platform; + + const onAddPanel = (id: string, savedObjectType: string, name: string) => { + const embeddableFactories = getEmbeddableFactories(); // Find the embeddable type from the saved object type const found = Array.from(embeddableFactories).find((embeddableFactory) => { @@ -39,41 +40,39 @@ export class AddEmbeddableFlyout extends React.Component { const foundEmbeddableType = found ? found.type : 'unknown'; - this.props.onSelect(id, foundEmbeddableType); + onSelect(id, foundEmbeddableType); }; - render() { - const embeddableFactories = this.props.getEmbeddableFactories(); + const embeddableFactories = getEmbeddableFactories(); - const availableSavedObjects = Array.from(embeddableFactories) - .filter((factory) => { - return this.props.availableEmbeddables.includes(factory.type); - }) - .map((factory) => factory.savedObjectMetaData) - .filter>(function ( - maybeSavedObjectMetaData - ): maybeSavedObjectMetaData is SavedObjectMetaData<{}> { - return maybeSavedObjectMetaData !== undefined; - }); + const availableSavedObjects = Array.from(embeddableFactories) + .filter((factory) => { + return availableEmbeddables.includes(factory.type); + }) + .map((factory) => factory.savedObjectMetaData) + .filter>(function ( + maybeSavedObjectMetaData + ): maybeSavedObjectMetaData is SavedObjectMetaData<{}> { + return maybeSavedObjectMetaData !== undefined; + }); - return ( - - - -

{strings.getTitleText()}

-
-
- - - -
- ); - } -} + return ( + + + +

{strings.getTitleText()}

+
+
+ + + +
+ ); +}; diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx index 9462ba0411de4..62a073daf4c59 100644 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx @@ -14,8 +14,6 @@ import { AddEmbeddableFlyout, Props } from './flyout'; import { addElement } from '../../state/actions/elements'; import { getSelectedPage } from '../../state/selectors/workpad'; import { EmbeddableTypes } from '../../../canvas_plugin_src/expression_types/embeddable'; -import { WithKibanaProps } from '../../index'; -import { withKibana } from '../../../../../../src/plugins/kibana_react/public'; const allowedEmbeddables = { [EmbeddableTypes.map]: (id: string) => { @@ -74,10 +72,10 @@ const mergeProps = ( }; }; -export class EmbeddableFlyoutPortal extends React.Component { +export class EmbeddableFlyoutPortal extends React.Component { el?: HTMLElement; - constructor(props: Props & WithKibanaProps) { + constructor(props: Props) { super(props); this.el = document.createElement('div'); @@ -103,9 +101,6 @@ export class EmbeddableFlyoutPortal extends React.Component, this.el ); @@ -113,7 +108,6 @@ export class EmbeddableFlyoutPortal extends React.Component void }>( - connect(mapStateToProps, mapDispatchToProps, mergeProps), - withKibana +export const AddEmbeddablePanel = compose void }>( + connect(mapStateToProps, mapDispatchToProps, mergeProps) )(EmbeddableFlyoutPortal); diff --git a/x-pack/plugins/canvas/public/components/expression/index.js b/x-pack/plugins/canvas/public/components/expression/index.js index 4480169dd037d..146acbcd6c6ee 100644 --- a/x-pack/plugins/canvas/public/components/expression/index.js +++ b/x-pack/plugins/canvas/public/components/expression/index.js @@ -15,7 +15,7 @@ import { renderComponent, } from 'recompose'; import { fromExpression } from '@kbn/interpreter/common'; -import { withKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { withServices } from '../../services'; import { getSelectedPage, getSelectedElement } from '../../state/selectors/workpad'; import { setExpression, flushContext } from '../../state/actions/elements'; import { ElementNotSelected } from './element_not_selected'; @@ -46,7 +46,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { const { expression } = element; - const functions = Object.values(allProps.kibana.services.expressions.getFunctions()); + const functions = Object.values(allProps.services.expressions.getFunctions()); return { ...allProps, @@ -71,7 +71,7 @@ const expressionLifecycle = lifecycle({ }); export const Expression = compose( - withKibana, + withServices, connect(mapStateToProps, mapDispatchToProps, mergeProps), withState('formState', 'setFormState', ({ expression }) => ({ expression, diff --git a/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx b/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx index bc51128cf0c87..7939c1d04631a 100644 --- a/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx +++ b/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx @@ -7,7 +7,7 @@ import React, { useState, useEffect, useRef, FC, useCallback } from 'react'; import { useDebounce } from 'react-use'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { useNotifyService } from '../../services'; import { RenderToDom } from '../render_to_dom'; import { ErrorStrings } from '../../../i18n'; import { RendererHandlers } from '../../../types'; @@ -39,8 +39,7 @@ export const RenderWithFn: FC = ({ width, height, }) => { - const { services } = useKibana(); - const onError = services.canvas.notify.error; + const { error: onError } = useNotifyService(); const [domNode, setDomNode] = useState(null); diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts b/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts index c5c1dbc2fdd6e..da2955c146193 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts @@ -10,8 +10,7 @@ import { compose, withState } from 'recompose'; import { camelCase } from 'lodash'; import { cloneSubgraphs } from '../../lib/clone_subgraphs'; import * as customElementService from '../../lib/custom_element_service'; -import { withKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { WithKibanaProps } from '../../'; +import { withServices, WithServicesProps } from '../../services'; // @ts-expect-error untyped local import { selectToplevelNodes } from '../../state/actions/transient'; // @ts-expect-error untyped local @@ -63,7 +62,7 @@ const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ const mergeProps = ( stateProps: StateProps, dispatchProps: DispatchProps, - ownProps: OwnPropsWithState & WithKibanaProps + ownProps: OwnPropsWithState & WithServicesProps ): ComponentProps => { const { pageId } = stateProps; const { onClose, search, setCustomElements } = ownProps; @@ -91,7 +90,7 @@ const mergeProps = ( try { await findCustomElements(); } catch (err) { - ownProps.kibana.services.canvas.notify.error(err, { + ownProps.services.notify.error(err, { title: `Couldn't find custom elements`, }); } @@ -102,7 +101,7 @@ const mergeProps = ( await customElementService.remove(id); await findCustomElements(); } catch (err) { - ownProps.kibana.services.canvas.notify.error(err, { + ownProps.services.notify.error(err, { title: `Couldn't delete custom elements`, }); } @@ -118,7 +117,7 @@ const mergeProps = ( }); await findCustomElements(); } catch (err) { - ownProps.kibana.services.canvas.notify.error(err, { + ownProps.services.notify.error(err, { title: `Couldn't update custom elements`, }); } @@ -127,7 +126,7 @@ const mergeProps = ( }; export const SavedElementsModal = compose( - withKibana, + withServices, withState('search', 'setSearch', ''), withState('customElements', 'setCustomElements', []), connect(mapStateToProps, mapDispatchToProps, mergeProps) diff --git a/x-pack/plugins/canvas/public/components/var_config/index.tsx b/x-pack/plugins/canvas/public/components/var_config/index.tsx index 526037b79e0e0..ca40bd07877f0 100644 --- a/x-pack/plugins/canvas/public/components/var_config/index.tsx +++ b/x-pack/plugins/canvas/public/components/var_config/index.tsx @@ -7,27 +7,19 @@ import React, { FC } from 'react'; import copy from 'copy-to-clipboard'; import { VarConfig as ChildComponent } from './var_config'; -import { - withKibana, - KibanaReactContextValue, - KibanaServices, -} from '../../../../../../src/plugins/kibana_react/public'; -import { CanvasServices } from '../../services'; - +import { useNotifyService } from '../../services'; import { ComponentStrings } from '../../../i18n'; - import { CanvasVariable } from '../../../types'; const { VarConfig: strings } = ComponentStrings; interface Props { - kibana: KibanaReactContextValue<{ canvas: CanvasServices } & KibanaServices>; - variables: CanvasVariable[]; setVariables: (variables: CanvasVariable[]) => void; } -const WrappedComponent: FC = ({ kibana, variables, setVariables }) => { +export const VarConfig: FC = ({ variables, setVariables }) => { + const { success } = useNotifyService(); const onDeleteVar = (v: CanvasVariable) => { const index = variables.findIndex((targetVar: CanvasVariable) => { return targetVar.name === v.name; @@ -36,15 +28,14 @@ const WrappedComponent: FC = ({ kibana, variables, setVariables }) => { const newVars = [...variables]; newVars.splice(index, 1); setVariables(newVars); - - kibana.services.canvas.notify.success(strings.getDeleteNotificationDescription()); + success(strings.getDeleteNotificationDescription()); } }; const onCopyVar = (v: CanvasVariable) => { const snippetStr = `{var "${v.name}"}`; copy(snippetStr, { debug: true }); - kibana.services.canvas.notify.success(strings.getCopyNotificationDescription()); + success(strings.getCopyNotificationDescription()); }; const onAddVar = (v: CanvasVariable) => { @@ -62,5 +53,3 @@ const WrappedComponent: FC = ({ kibana, variables, setVariables }) => { return ; }; - -export const VarConfig = withKibana(WrappedComponent); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.tsx index 13b2cace13a40..264873fc994dd 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { connect } from 'react-redux'; import { compose, withProps } from 'recompose'; import { Dispatch } from 'redux'; -import { withKibana } from '../../../../../../../src/plugins/kibana_react/public/'; import { State, ElementSpec } from '../../../../types'; // @ts-expect-error untyped local import { elementsRegistry } from '../../../lib/elements_registry'; @@ -44,6 +43,5 @@ const mergeProps = (stateProps: StateProps, dispatchProps: DispatchProps) => ({ export const ElementMenu = compose( connect(mapStateToProps, mapDispatchToProps, mergeProps), - withKibana, withProps(() => ({ elements: elementsRegistry.toJS() })) )(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/index.ts index 17fcc50334a8f..01bcfebc0dba9 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/index.ts @@ -13,8 +13,7 @@ import { downloadWorkpad } from '../../../lib/download_workpad'; import { ShareMenu as Component, Props as ComponentProps } from './share_menu'; import { getPdfUrl, createPdf } from './utils'; import { State, CanvasWorkpad } from '../../../../types'; -import { withKibana } from '../../../../../../../src/plugins/kibana_react/public/'; -import { WithKibanaProps } from '../../../index'; +import { withServices, WithServicesProps } from '../../../services'; import { ComponentStrings } from '../../../../i18n'; @@ -43,12 +42,16 @@ interface Props { export const ShareMenu = compose( connect(mapStateToProps), - withKibana, + withServices, withProps( - ({ workpad, pageCount, kibana }: Props & WithKibanaProps): ComponentProps => ({ + ({ workpad, pageCount, services }: Props & WithServicesProps): ComponentProps => ({ getExportUrl: (type) => { if (type === 'pdf') { - const pdfUrl = getPdfUrl(workpad, { pageCount }, kibana.services.http.basePath); + const pdfUrl = getPdfUrl( + workpad, + { pageCount }, + services.platform.getBasePathInterface() + ); return getAbsoluteUrl(pdfUrl); } @@ -57,10 +60,10 @@ export const ShareMenu = compose( onCopy: (type) => { switch (type) { case 'pdf': - kibana.services.canvas.notify.info(strings.getCopyPDFMessage()); + services.notify.info(strings.getCopyPDFMessage()); break; case 'reportingConfig': - kibana.services.canvas.notify.info(strings.getCopyReportingConfigMessage()); + services.notify.info(strings.getCopyReportingConfigMessage()); break; default: throw new Error(strings.getUnknownExportErrorMessage(type)); @@ -69,9 +72,9 @@ export const ShareMenu = compose( onExport: (type) => { switch (type) { case 'pdf': - return createPdf(workpad, { pageCount }, kibana.services.http.basePath) + return createPdf(workpad, { pageCount }, services.platform.getBasePathInterface()) .then(({ data }: { data: { job: { id: string } } }) => { - kibana.services.canvas.notify.info(strings.getExportPDFMessage(), { + services.notify.info(strings.getExportPDFMessage(), { title: strings.getExportPDFTitle(workpad.name), }); @@ -79,7 +82,7 @@ export const ShareMenu = compose( jobCompletionNotifications.add(data.job.id); }) .catch((err: Error) => { - kibana.services.canvas.notify.error(err, { + services.notify.error(err, { title: strings.getExportPDFErrorTitle(workpad.name), }); }); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts index ddf1a12775cae..e2a05d13b017e 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts @@ -7,7 +7,6 @@ import { connect } from 'react-redux'; import { compose, withHandlers } from 'recompose'; import { Dispatch } from 'redux'; -import { withKibana } from '../../../../../../../src/plugins/kibana_react/public/'; import { zoomHandlerCreators } from '../../../lib/app_handler_creators'; import { State, CanvasWorkpadBoundingBox } from '../../../../types'; // @ts-expect-error untyped local @@ -97,6 +96,5 @@ const mergeProps = ( export const ViewMenu = compose( connect(mapStateToProps, mapDispatchToProps, mergeProps), - withKibana, withHandlers(zoomHandlerCreators) )(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/index.js b/x-pack/plugins/canvas/public/components/workpad_loader/index.js index ab07d5d722405..f747cb677a576 100644 --- a/x-pack/plugins/canvas/public/components/workpad_loader/index.js +++ b/x-pack/plugins/canvas/public/components/workpad_loader/index.js @@ -14,7 +14,7 @@ import { getWorkpad } from '../../state/selectors/workpad'; import { getId } from '../../lib/get_id'; import { downloadWorkpad } from '../../lib/download_workpad'; import { ComponentStrings, ErrorStrings } from '../../../i18n'; -import { withKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { withServices } from '../../services'; import { WorkpadLoader as Component } from './workpad_loader'; const { WorkpadLoader: strings } = ComponentStrings; @@ -31,11 +31,11 @@ export const WorkpadLoader = compose( }), connect(mapStateToProps), withState('workpads', 'setWorkpads', null), - withKibana, - withProps(({ kibana }) => ({ - notify: kibana.services.canvas.notify, + withServices, + withProps(({ services }) => ({ + notify: services.notify, })), - withHandlers(({ kibana }) => ({ + withHandlers(({ services }) => ({ // Workpad creation via navigation createWorkpad: (props) => async (workpad) => { // workpad data uploaded, create and load it @@ -44,7 +44,7 @@ export const WorkpadLoader = compose( await workpadService.create(workpad); props.router.navigateTo('loadWorkpad', { id: workpad.id, page: 1 }); } catch (err) { - kibana.services.canvas.notify.error(err, { + services.notify.error(err, { title: errors.getUploadFailureErrorMessage(), }); } @@ -60,7 +60,7 @@ export const WorkpadLoader = compose( const workpads = await workpadService.find(text); setWorkpads(workpads); } catch (err) { - kibana.services.canvas.notify.error(err, { title: errors.getFindFailureErrorMessage() }); + services.notify.error(err, { title: errors.getFindFailureErrorMessage() }); } }, @@ -76,7 +76,7 @@ export const WorkpadLoader = compose( await workpadService.create(workpad); props.router.navigateTo('loadWorkpad', { id: workpad.id, page: 1 }); } catch (err) { - kibana.services.canvas.notify.error(err, { title: errors.getCloneFailureErrorMessage() }); + services.notify.error(err, { title: errors.getCloneFailureErrorMessage() }); } }, @@ -122,7 +122,7 @@ export const WorkpadLoader = compose( }; if (errored.length > 0) { - kibana.services.canvas.notify.error(errors.getDeleteFailureErrorMessage()); + services.notify.error(errors.getDeleteFailureErrorMessage()); } setWorkpads(workpadState); @@ -137,7 +137,7 @@ export const WorkpadLoader = compose( })), withProps((props) => ({ formatDate: (date) => { - const dateFormat = props.kibana.services.uiSettings.get('dateFormat'); + const dateFormat = props.services.platform.getUISetting('dateFormat'); return date && moment(date).format(dateFormat); }, })) diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/index.tsx b/x-pack/plugins/canvas/public/components/workpad_templates/index.tsx index f35bba3fd598d..35b0e2bb19e3e 100644 --- a/x-pack/plugins/canvas/public/components/workpad_templates/index.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_templates/index.tsx @@ -10,12 +10,11 @@ import { RouterContext } from '../router'; import { ComponentStrings } from '../../../i18n/components'; // @ts-expect-error import * as workpadService from '../../lib/workpad_service'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { WorkpadTemplates as Component } from './workpad_templates'; import { CanvasTemplate } from '../../../types'; -import { UseKibanaProps } from '../../'; import { list } from '../../lib/template_service'; import { applyTemplateStrings } from '../../../i18n/templates/apply_strings'; +import { useNotifyService } from '../../services'; interface WorkpadTemplatesProps { onClose: () => void; @@ -33,7 +32,7 @@ export const WorkpadTemplates: FunctionComponent = ({ onC const [creatingFromTemplateName, setCreatingFromTemplateName] = useState( undefined ); - const kibana = useKibana(); + const { error } = useNotifyService(); useEffect(() => { if (!templates) { @@ -60,9 +59,9 @@ export const WorkpadTemplates: FunctionComponent = ({ onC if (router) { router.navigateTo('loadWorkpad', { id: result.data.id, page: 1 }); } - } catch (error) { + } catch (e) { setCreatingFromTemplateName(undefined); - kibana.services.canvas.notify.error(error, { + error(e, { title: `Couldn't create workpad from template`, }); } diff --git a/x-pack/plugins/canvas/public/lib/breadcrumbs.ts b/x-pack/plugins/canvas/public/lib/breadcrumbs.ts index 96412ef50c79d..b613bb7fcdaf1 100644 --- a/x-pack/plugins/canvas/public/lib/breadcrumbs.ts +++ b/x-pack/plugins/canvas/public/lib/breadcrumbs.ts @@ -24,6 +24,5 @@ export const getWorkpadBreadcrumb = ({ }; export const setBreadcrumb = (paths: ChromeBreadcrumb | ChromeBreadcrumb[]) => { - const setBreadCrumbs = platformService.getService().coreStart.chrome.setBreadcrumbs; - setBreadCrumbs(Array.isArray(paths) ? paths : [paths]); + platformService.getService().setBreadcrumbs(Array.isArray(paths) ? paths : [paths]); }; diff --git a/x-pack/plugins/canvas/public/lib/custom_element_service.ts b/x-pack/plugins/canvas/public/lib/custom_element_service.ts index 25c3b78a2746e..f240df93d0387 100644 --- a/x-pack/plugins/canvas/public/lib/custom_element_service.ts +++ b/x-pack/plugins/canvas/public/lib/custom_element_service.ts @@ -11,7 +11,7 @@ import { CustomElement } from '../../types'; import { platformService } from '../services'; const getApiPath = function () { - const basePath = platformService.getService().coreStart.http.basePath.get(); + const basePath = platformService.getService().getBasePath(); return `${basePath}${API_ROUTE_CUSTOM_ELEMENT}`; }; diff --git a/x-pack/plugins/canvas/public/lib/documentation_links.ts b/x-pack/plugins/canvas/public/lib/documentation_links.ts index 6430f7d87d4f7..cb19389291028 100644 --- a/x-pack/plugins/canvas/public/lib/documentation_links.ts +++ b/x-pack/plugins/canvas/public/lib/documentation_links.ts @@ -7,10 +7,14 @@ import { platformService } from '../services'; export const getDocumentationLinks = () => ({ - canvas: `${platformService.getService().coreStart.docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${ - platformService.getService().coreStart.docLinks.DOC_LINK_VERSION - }/canvas.html`, - numeral: `${platformService.getService().coreStart.docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${ - platformService.getService().coreStart.docLinks.DOC_LINK_VERSION - }/guide/numeral.html`, + canvas: `${platformService + .getService() + .getElasticWebsiteUrl()}guide/en/kibana/${platformService + .getService() + .getDocLinkVersion()}/canvas.html`, + numeral: `${platformService + .getService() + .getElasticWebsiteUrl()}guide/en/kibana/${platformService + .getService() + .getDocLinkVersion()}/guide/numeral.html`, }); diff --git a/x-pack/plugins/canvas/public/lib/es_service.ts b/x-pack/plugins/canvas/public/lib/es_service.ts index 5c1131d5fbe35..fee66c71636c8 100644 --- a/x-pack/plugins/canvas/public/lib/es_service.ts +++ b/x-pack/plugins/canvas/public/lib/es_service.ts @@ -15,16 +15,16 @@ import { platformService } from '../services'; const { esService: strings } = ErrorStrings; const getApiPath = function () { - const basePath = platformService.getService().coreStart.http.basePath.get(); + const basePath = platformService.getService().getBasePath(); return basePath + API_ROUTE; }; const getSavedObjectsClient = function () { - return platformService.getService().coreStart.savedObjects.client; + return platformService.getService().getSavedObjectsClient(); }; const getAdvancedSettings = function () { - return platformService.getService().coreStart.uiSettings; + return platformService.getService().getUISettings(); }; export const getFields = (index = '_all') => { diff --git a/x-pack/plugins/canvas/public/lib/template_service.ts b/x-pack/plugins/canvas/public/lib/template_service.ts index 98d582c854e36..185b2ec37ba95 100644 --- a/x-pack/plugins/canvas/public/lib/template_service.ts +++ b/x-pack/plugins/canvas/public/lib/template_service.ts @@ -10,7 +10,7 @@ import { platformService } from '../services'; import { CanvasTemplate } from '../../types'; const getApiPath = function () { - const basePath = platformService.getService().coreStart.http.basePath.get(); + const basePath = platformService.getService().getBasePath(); return `${basePath}${API_ROUTE_TEMPLATES}`; }; diff --git a/x-pack/plugins/canvas/public/lib/workpad_service.js b/x-pack/plugins/canvas/public/lib/workpad_service.js index 2047e20424acc..27efe25405fd7 100644 --- a/x-pack/plugins/canvas/public/lib/workpad_service.js +++ b/x-pack/plugins/canvas/public/lib/workpad_service.js @@ -12,6 +12,7 @@ import { } from '../../common/lib/constants'; import { fetch } from '../../common/lib/fetch'; import { platformService } from '../services'; + /* Remove any top level keys from the workpad which will be rejected by validation */ @@ -44,17 +45,17 @@ const sanitizeWorkpad = function (workpad) { }; const getApiPath = function () { - const basePath = platformService.getService().coreStart.http.basePath.get(); + const basePath = platformService.getService().getBasePath(); return `${basePath}${API_ROUTE_WORKPAD}`; }; const getApiPathStructures = function () { - const basePath = platformService.getService().coreStart.http.basePath.get(); + const basePath = platformService.getService().getBasePath(); return `${basePath}${API_ROUTE_WORKPAD_STRUCTURES}`; }; const getApiPathAssets = function () { - const basePath = platformService.getService().coreStart.http.basePath.get(); + const basePath = platformService.getService().getBasePath(); return `${basePath}${API_ROUTE_WORKPAD_ASSETS}`; }; diff --git a/x-pack/plugins/canvas/public/services/context.tsx b/x-pack/plugins/canvas/public/services/context.tsx new file mode 100644 index 0000000000000..9bd86ef98f1e3 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/context.tsx @@ -0,0 +1,58 @@ +/* + * 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, { + useContext, + createElement, + createContext, + ComponentType, + FC, + ReactElement, +} from 'react'; +import { CanvasServices, CanvasServiceProviders } from '.'; + +export interface WithServicesProps { + services: CanvasServices; +} + +const defaultContextValue = { + embeddables: {}, + expressions: {}, + notify: {}, + platform: {}, + navLink: {}, +}; + +const context = createContext(defaultContextValue as CanvasServices); + +export const useServices = () => useContext(context); +export const usePlatformService = () => useServices().platform; +export const useEmbeddablesService = () => useServices().embeddables; +export const useExpressionsService = () => useServices().expressions; +export const useNotifyService = () => useServices().notify; +export const useNavLinkService = () => useServices().navLink; + +export const withServices = (type: ComponentType) => { + const EnhancedType: FC = (props) => { + const services = useServices(); + return createElement(type, { ...props, services }); + }; + return EnhancedType; +}; + +export const ServicesProvider: FC<{ + providers: CanvasServiceProviders; + children: ReactElement; +}> = ({ providers, children }) => { + const value = { + embeddables: providers.embeddables.getService(), + expressions: providers.expressions.getService(), + notify: providers.notify.getService(), + platform: providers.platform.getService(), + navLink: providers.navLink.getService(), + }; + return {children}; +}; diff --git a/x-pack/plugins/canvas/public/services/embeddables.ts b/x-pack/plugins/canvas/public/services/embeddables.ts new file mode 100644 index 0000000000000..13e308effcdba --- /dev/null +++ b/x-pack/plugins/canvas/public/services/embeddables.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EmbeddableFactory } from '../../../../../src/plugins/embeddable/public'; +import { CanvasServiceFactory } from '.'; + +export interface EmbeddablesService { + getEmbeddableFactories: () => IterableIterator; +} + +export const embeddablesServiceFactory: CanvasServiceFactory = async ( + _coreSetup, + _coreStart, + _setupPlugins, + startPlugins +) => ({ + getEmbeddableFactories: startPlugins.embeddable.getEmbeddableFactories, +}); diff --git a/x-pack/plugins/canvas/public/services/expressions.ts b/x-pack/plugins/canvas/public/services/expressions.ts index 16f939a9c97fc..1376aab0ca8b9 100644 --- a/x-pack/plugins/canvas/public/services/expressions.ts +++ b/x-pack/plugins/canvas/public/services/expressions.ts @@ -14,6 +14,5 @@ export const expressionsServiceFactory: CanvasServiceFactory startPlugins ) => { await setupPlugins.expressions.__LEGACY.loadLegacyServerFunctionWrappers(); - return setupPlugins.expressions.fork(); }; diff --git a/x-pack/plugins/canvas/public/services/index.ts b/x-pack/plugins/canvas/public/services/index.ts index a929b4639d3e4..700d874d4507d 100644 --- a/x-pack/plugins/canvas/public/services/index.ts +++ b/x-pack/plugins/canvas/public/services/index.ts @@ -10,8 +10,16 @@ import { CanvasSetupDeps, CanvasStartDeps } from '../plugin'; import { notifyServiceFactory } from './notify'; import { platformServiceFactory } from './platform'; import { navLinkServiceFactory } from './nav_link'; +import { embeddablesServiceFactory } from './embeddables'; import { expressionsServiceFactory } from './expressions'; +export { NotifyService } from './notify'; +export { PlatformService } from './platform'; +export { NavLinkService } from './nav_link'; +export { EmbeddablesService } from './embeddables'; +export { ExpressionsService } from '../../../../../src/plugins/expressions/common'; +export * from './context'; + export type CanvasServiceFactory = ( coreSetup: CoreSetup, coreStart: CoreStart, @@ -28,6 +36,10 @@ class CanvasServiceProvider { this.factory = factory; } + setService(service: Service) { + this.service = service; + } + async start( coreSetup: CoreSetup, coreStart: CoreStart, @@ -60,13 +72,17 @@ class CanvasServiceProvider { export type ServiceFromProvider

= P extends CanvasServiceProvider ? T : never; export const services = { + embeddables: new CanvasServiceProvider(embeddablesServiceFactory), expressions: new CanvasServiceProvider(expressionsServiceFactory), notify: new CanvasServiceProvider(notifyServiceFactory), platform: new CanvasServiceProvider(platformServiceFactory), navLink: new CanvasServiceProvider(navLinkServiceFactory), }; +export type CanvasServiceProviders = typeof services; + export interface CanvasServices { + embeddables: ServiceFromProvider; expressions: ServiceFromProvider; notify: ServiceFromProvider; platform: ServiceFromProvider; @@ -88,10 +104,11 @@ export const startServices = async ( }; export const stopServices = () => { - Object.entries(services).forEach(([key, provider]) => provider.stop()); + Object.values(services).forEach((provider) => provider.stop()); }; export const { + embeddables: embeddableService, notify: notifyService, platform: platformService, navLink: navLinkService, diff --git a/x-pack/plugins/canvas/public/services/nav_link.ts b/x-pack/plugins/canvas/public/services/nav_link.ts index 68d685242351b..532b5264ee9ed 100644 --- a/x-pack/plugins/canvas/public/services/nav_link.ts +++ b/x-pack/plugins/canvas/public/services/nav_link.ts @@ -8,15 +8,15 @@ import { CanvasServiceFactory } from '.'; import { SESSIONSTORAGE_LASTPATH } from '../../common/lib/constants'; import { getSessionStorage } from '../lib/storage'; -interface NavLinkService { +export interface NavLinkService { updatePath: (path: string) => void; } export const navLinkServiceFactory: CanvasServiceFactory = ( coreSetup, - coreStart, - setupPlugins, - startPlugins, + _coreStart, + _setupPlugins, + _startPlugins, appUpdater ) => { return { diff --git a/x-pack/plugins/canvas/public/services/notify.ts b/x-pack/plugins/canvas/public/services/notify.ts index 5454a0f87c3f0..819525c8fa922 100644 --- a/x-pack/plugins/canvas/public/services/notify.ts +++ b/x-pack/plugins/canvas/public/services/notify.ts @@ -26,7 +26,7 @@ const getToast = (err: Error | string, opts: ToastInputFields = {}) => { }; }; -interface NotifyService { +export interface NotifyService { error: (err: string | Error, opts?: ToastInputFields) => void; warning: (err: string | Error, opts?: ToastInputFields) => void; info: (err: string | Error, opts?: ToastInputFields) => void; diff --git a/x-pack/plugins/canvas/public/services/platform.ts b/x-pack/plugins/canvas/public/services/platform.ts index 440e9523044c1..92c378e9aa597 100644 --- a/x-pack/plugins/canvas/public/services/platform.ts +++ b/x-pack/plugins/canvas/public/services/platform.ts @@ -4,21 +4,52 @@ * you may not use this file except in compliance with the Elastic License. */ +import { + SavedObjectsStart, + SavedObjectsClientContract, + IUiSettingsClient, + ChromeBreadcrumb, + IBasePath, +} from '../../../../../src/core/public'; import { CanvasServiceFactory } from '.'; -import { CoreStart, CoreSetup, CanvasSetupDeps, CanvasStartDeps } from '../plugin'; -interface PlatformService { - coreSetup: CoreSetup; - coreStart: CoreStart; - setupPlugins: CanvasSetupDeps; - startPlugins: CanvasStartDeps; +export interface PlatformService { + getBasePath: () => string; + getBasePathInterface: () => IBasePath; + getDocLinkVersion: () => string; + getElasticWebsiteUrl: () => string; + getHasWriteAccess: () => boolean; + getUISetting: (key: string, defaultValue?: any) => any; + setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => void; + setRecentlyAccessed: (link: string, label: string, id: string) => void; + + // 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. + getSavedObjects: () => SavedObjectsStart; + getSavedObjectsClient: () => SavedObjectsClientContract; + getUISettings: () => IUiSettingsClient; } export const platformServiceFactory: CanvasServiceFactory = ( - coreSetup, - coreStart, - setupPlugins, - startPlugins + _coreSetup, + coreStart ) => { - return { coreSetup, coreStart, setupPlugins, startPlugins }; + return { + getBasePath: coreStart.http.basePath.get, + getBasePathInterface: () => coreStart.http.basePath, + getElasticWebsiteUrl: () => coreStart.docLinks.ELASTIC_WEBSITE_URL, + getDocLinkVersion: () => coreStart.docLinks.DOC_LINK_VERSION, + // TODO: is there a better type for this? The capabilities type allows for a Record, + // though we don't do this. So this cast may be the best option. + getHasWriteAccess: () => coreStart.application.capabilities.canvas.save as boolean, + getUISetting: coreStart.uiSettings.get.bind(coreStart.uiSettings), + setBreadcrumbs: coreStart.chrome.setBreadcrumbs, + setRecentlyAccessed: coreStart.chrome.recentlyAccessed.add, + + // 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. + getSavedObjects: () => coreStart.savedObjects, + getSavedObjectsClient: () => coreStart.savedObjects.client, + getUISettings: () => coreStart.uiSettings, + }; }; diff --git a/x-pack/plugins/canvas/public/services/stubs/embeddables.ts b/x-pack/plugins/canvas/public/services/stubs/embeddables.ts new file mode 100644 index 0000000000000..48100da462dd5 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/stubs/embeddables.ts @@ -0,0 +1,12 @@ +/* + * 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 { EmbeddablesService } from '../embeddables'; + +const noop = (..._args: any[]): any => {}; + +export const embeddablesService: EmbeddablesService = { + getEmbeddableFactories: noop, +}; diff --git a/x-pack/plugins/canvas/public/services/stubs/expressions.ts b/x-pack/plugins/canvas/public/services/stubs/expressions.ts new file mode 100644 index 0000000000000..26a90670106d0 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/stubs/expressions.ts @@ -0,0 +1,27 @@ +/* + * 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 { ExpressionsService } from '../'; +import { + plugin, + ExpressionRenderDefinition, +} from '../../../../../../src/plugins/expressions/public'; +import { functions as functionDefinitions } from '../../../canvas_plugin_src/functions/common'; +// @ts-expect-error untyped local +import { renderFunctions } from '../../../canvas_plugin_src/renderers/core'; + +const placeholder = {} as any; +const expressionsPlugin = plugin(placeholder); +const setup = expressionsPlugin.setup(placeholder, { + inspector: {}, +} as any); + +export const expressionsService: ExpressionsService = setup.fork(); + +functionDefinitions.forEach((fn) => expressionsService.registerFunction(fn)); +renderFunctions.forEach((fn: ExpressionRenderDefinition) => + expressionsService.registerRenderer(fn) +); diff --git a/x-pack/plugins/canvas/public/services/stubs/index.ts b/x-pack/plugins/canvas/public/services/stubs/index.ts new file mode 100644 index 0000000000000..b4e440f204cc7 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/stubs/index.ts @@ -0,0 +1,28 @@ +/* + * 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 { CanvasServices, services } from '../'; +import { embeddablesService } from './embeddables'; +import { expressionsService } from './expressions'; +import { navLinkService } from './nav_link'; +import { notifyService } from './notify'; +import { platformService } from './platform'; + +export const stubs: CanvasServices = { + embeddables: embeddablesService, + expressions: expressionsService, + navLink: navLinkService, + notify: notifyService, + platform: platformService, +}; + +export const startServices = async (providedServices: Partial = {}) => { + Object.entries(services).forEach(([key, provider]) => { + // @ts-expect-error Object.entries isn't strongly typed + const stub = providedServices[key] || stubs[key]; + provider.setService(stub); + }); +}; diff --git a/x-pack/plugins/canvas/public/services/stubs/nav_link.ts b/x-pack/plugins/canvas/public/services/stubs/nav_link.ts new file mode 100644 index 0000000000000..3b40eeb3e84f2 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/stubs/nav_link.ts @@ -0,0 +1,13 @@ +/* + * 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 { NavLinkService } from '../nav_link'; + +const noop = (..._args: any[]): any => {}; + +export const navLinkService: NavLinkService = { + updatePath: noop, +}; diff --git a/x-pack/plugins/canvas/public/services/stubs/notify.ts b/x-pack/plugins/canvas/public/services/stubs/notify.ts new file mode 100644 index 0000000000000..38eac2a5813eb --- /dev/null +++ b/x-pack/plugins/canvas/public/services/stubs/notify.ts @@ -0,0 +1,16 @@ +/* + * 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 { NotifyService } from '../notify'; + +const noop = (..._args: any[]): any => {}; + +export const notifyService: NotifyService = { + error: noop, + info: noop, + success: noop, + warning: noop, +}; diff --git a/x-pack/plugins/canvas/public/services/stubs/platform.ts b/x-pack/plugins/canvas/public/services/stubs/platform.ts new file mode 100644 index 0000000000000..9ada579573502 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/stubs/platform.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PlatformService } from '../platform'; + +const noop = (..._args: any[]): any => {}; + +export const platformService: PlatformService = { + getBasePath: () => '/base/path', + getBasePathInterface: noop, + getDocLinkVersion: () => 'dockLinkVersion', + getElasticWebsiteUrl: () => 'https://elastic.co', + getHasWriteAccess: () => true, + getUISetting: noop, + setBreadcrumbs: noop, + setRecentlyAccessed: noop, + getSavedObjects: noop, + getSavedObjectsClient: noop, + getUISettings: noop, +}; diff --git a/x-pack/plugins/canvas/public/state/initial_state.js b/x-pack/plugins/canvas/public/state/initial_state.js index 13021893e72e8..f9b02d33d6112 100644 --- a/x-pack/plugins/canvas/public/state/initial_state.js +++ b/x-pack/plugins/canvas/public/state/initial_state.js @@ -9,11 +9,13 @@ import { platformService } from '../services'; import { getDefaultWorkpad } from './defaults'; export const getInitialState = (path) => { + const { getHasWriteAccess } = platformService.getService(); + const state = { app: {}, // Kibana stuff in here assets: {}, // assets end up here transient: { - canUserWrite: platformService.getService().coreStart.application.capabilities.canvas.save, + canUserWrite: getHasWriteAccess(), zoomScale: 1, elementStats: { total: 0, diff --git a/x-pack/plugins/canvas/public/state/reducers/workpad.js b/x-pack/plugins/canvas/public/state/reducers/workpad.js index 9a0c30bdf1337..fffcb69c451ed 100644 --- a/x-pack/plugins/canvas/public/state/reducers/workpad.js +++ b/x-pack/plugins/canvas/public/state/reducers/workpad.js @@ -25,11 +25,7 @@ export const workpadReducer = handleActions( [setWorkpad]: (workpadState, { payload }) => { platformService .getService() - .coreStart.chrome.recentlyAccessed.add( - `${APP_ROUTE_WORKPAD}/${payload.id}`, - payload.name, - payload.id - ); + .setRecentlyAccessed(`${APP_ROUTE_WORKPAD}/${payload.id}`, payload.name, payload.id); return payload; }, diff --git a/x-pack/plugins/canvas/storybook/config.js b/x-pack/plugins/canvas/storybook/config.js index f349f9b7ccf98..dc16d6c46084d 100644 --- a/x-pack/plugins/canvas/storybook/config.js +++ b/x-pack/plugins/canvas/storybook/config.js @@ -8,6 +8,7 @@ import { configure, addDecorator, addParameters } from '@storybook/react'; import { withInfo } from '@storybook/addon-info'; import { create } from '@storybook/theming'; +import { startServices } from '../public/services/stubs'; import { addDecorators } from './decorators'; // If we're running Storyshots, be sure to register the require context hook. @@ -32,6 +33,7 @@ if (process.env.NODE_ENV === 'test') { } addDecorators(); +startServices(); function loadStories() { require('./dll_contexts'); From e359c9ae38a0a074c52c4b806bef9c0eef9296cf Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Thu, 23 Jul 2020 14:16:56 -0700 Subject: [PATCH 03/96] changes for upgrade assistant functional test to incorporate test user (#70071) * changes for upgrade assistant functional test to incorporate test user * changes to toggle on/off * upgrade_assistant role * upgrade assistant * more debug statements to check on cloud * commented the sleeps to check toggle button * reduced the sleep to 2 seconds to test on cloud Co-authored-by: Elastic Machine --- .../upgrade_assistant/upgrade_assistant.ts | 20 +++++++++++++++++-- x-pack/test/functional/config.js | 14 +++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts b/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts index 85ad98727cea5..57b8fb23613be 100644 --- a/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts +++ b/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts @@ -11,14 +11,22 @@ export default function upgradeAssistantFunctionalTests({ getPageObjects, }: FtrProviderContext) { const esArchiver = getService('esArchiver'); - const PageObjects = getPageObjects(['upgradeAssistant']); + const PageObjects = getPageObjects(['upgradeAssistant', 'common']); + const security = getService('security'); + const log = getService('log'); describe('Upgrade Checkup', function () { this.tags('includeFirefox'); - before(async () => await esArchiver.load('empty_kibana')); + + before(async () => { + await esArchiver.load('empty_kibana'); + await security.testUser.setRoles(['global_upgrade_assistant_role']); + }); + after(async () => { await PageObjects.upgradeAssistant.expectTelemetryHasFinish(); await esArchiver.unload('empty_kibana'); + await security.testUser.restoreDefaults(); }); it('allows user to navigate to upgrade checkup', async () => { @@ -28,9 +36,17 @@ export default function upgradeAssistantFunctionalTests({ it('allows user to toggle deprecation logging', async () => { await PageObjects.upgradeAssistant.navigateToPage(); + log.debug('expect initial state to be ON'); await PageObjects.upgradeAssistant.expectDeprecationLoggingLabel('On'); + log.debug('Now toggle to off'); await PageObjects.upgradeAssistant.toggleDeprecationLogging(); + await PageObjects.common.sleep(2000); + log.debug('expect state to be OFF after toggle'); await PageObjects.upgradeAssistant.expectDeprecationLoggingLabel('Off'); + await PageObjects.upgradeAssistant.toggleDeprecationLogging(); + await PageObjects.common.sleep(2000); + log.debug('expect state to be ON after toggle'); + await PageObjects.upgradeAssistant.expectDeprecationLoggingLabel('On'); }); it('allows user to open cluster tab', async () => { diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 5c13e430ae2ca..fdd694e73394e 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -284,6 +284,20 @@ export default async function ({ readConfigFile }) { ], }, + global_upgrade_assistant_role: { + elasticsearch: { + cluster: ['manage'], + }, + kibana: [ + { + feature: { + discover: ['read'], + }, + spaces: ['*'], + }, + ], + }, + global_ccr_role: { elasticsearch: { cluster: ['manage', 'manage_ccr'], From 849bbfdcd51ccb1134277f7515ab7dfb7ff541d4 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 23 Jul 2020 15:22:54 -0600 Subject: [PATCH 04/96] [maps][docs] add trouble shooting for index not listed (#73066) * [maps][docs] add troubeshooting for index not listed * Update docs/maps/trouble-shooting.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/maps/trouble-shooting.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/maps/trouble-shooting.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/maps/trouble-shooting.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/maps/trouble-shooting.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- docs/maps/trouble-shooting.asciidoc | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/maps/trouble-shooting.asciidoc b/docs/maps/trouble-shooting.asciidoc index cfc47cf6f0e4f..1c53fbd55ea4b 100644 --- a/docs/maps/trouble-shooting.asciidoc +++ b/docs/maps/trouble-shooting.asciidoc @@ -20,6 +20,20 @@ image::maps/images/inspector.png[] [float] === Solutions to common problems +[float] +==== Index not listed when adding layer + +* Verify your geospatial data is correctly mapped as {ref}/geo-point.html[geo_point] or {ref}/geo-shape.html[geo_shape]. + ** Run `GET myIndexPatternTitle/_field_caps?fields=myGeoFieldName` in <>, replacing `myIndexPatternTitle` and `myGeoFieldName` with your index pattern title and geospatial field name. + ** Ensure response specifies `type` as `geo_point` or `geo_shape`. +* Verify your geospatial data is correctly mapped in your <>. + ** Open your index pattern in <>. + ** Ensure your geospatial field type is `geo_point` or `geo_shape`. + ** Ensure your geospatial field is searchable and aggregatable. + ** If your geospatial field type does not match your Elasticsearch mapping, click the *Refresh* button to refresh the field list from Elasticsearch. +* Index patterns with thousands of fields can exceed the default maximum payload size. +Increase <> for large index patterns. + [float] ==== Features are not displayed From bb646660512b51c0570491150ed05b649b74ecb6 Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Thu, 23 Jul 2020 17:45:07 -0400 Subject: [PATCH 05/96] [Resolver] Handle duplicate process events (#73123) In the case that the process creation (or already running) events for a node have been duplicated in ES, this uses the last one (in response order.) --- .../resolver/store/data/selectors.test.ts | 26 +++++++ .../public/resolver/store/data/selectors.ts | 10 ++- .../resolver/store/mocks/resolver_tree.ts | 78 +++++++++++++++++++ 3 files changed, 113 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts index 683f8f1a5f84a..9e1c396723a27 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts @@ -12,6 +12,7 @@ import { createStore } from 'redux'; import { mockTreeWithNoAncestorsAnd2Children, mockTreeWith2AncestorsAndNoChildren, + mockTreeWith1AncestorAnd2ChildrenAndAllNodesHave2GraphableEvents, } from '../mocks/resolver_tree'; import { uniquePidForProcess } from '../../models/process_event'; import { EndpointEvent } from '../../../../common/endpoint/types'; @@ -353,4 +354,29 @@ describe('data state', () => { } }); }); + describe('with a tree with 1 ancestor and 2 children, where all nodes have 2 graphable events', () => { + const ancestorID = 'b'; + const originID = 'c'; + const firstChildID = 'd'; + const secondChildID = 'e'; + beforeEach(() => { + const tree = mockTreeWith1AncestorAnd2ChildrenAndAllNodesHave2GraphableEvents({ + ancestorID, + originID, + firstChildID, + secondChildID, + }); + actions.push({ + type: 'serverReturnedResolverData', + payload: { + result: tree, + // this value doesn't matter + databaseDocumentID: '', + }, + }); + }); + it('should have 4 graphable processes', () => { + expect(selectors.graphableProcesses(state()).length).toBe(4); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index 40138d3f2fd3c..1d65b406306a3 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -109,8 +109,16 @@ export const terminatedProcesses = createSelector(resolverTreeResponse, function * Process events that will be graphed. */ export const graphableProcesses = createSelector(resolverTreeResponse, function (tree?) { + // Keep track of the last process event (in array order) for each entity ID + const events: Map = new Map(); if (tree) { - return resolverTreeModel.lifecycleEvents(tree).filter(isGraphableProcess); + for (const event of resolverTreeModel.lifecycleEvents(tree)) { + if (isGraphableProcess(event)) { + const entityID = uniquePidForProcess(event); + events.set(entityID, event); + } + } + return [...events.values()]; } else { return []; } diff --git a/x-pack/plugins/security_solution/public/resolver/store/mocks/resolver_tree.ts b/x-pack/plugins/security_solution/public/resolver/store/mocks/resolver_tree.ts index 862cf47f73947..2860eec5a6ab6 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/mocks/resolver_tree.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/mocks/resolver_tree.ts @@ -85,3 +85,81 @@ export function mockTreeWithNoAncestorsAnd2Children({ lifecycle: [origin], } as unknown) as ResolverTree; } + +/** + * Creates a mock tree w/ 2 'graphable' events per node. This simulates the scenario where data has been duplicated in the response from the server. + */ +export function mockTreeWith1AncestorAnd2ChildrenAndAllNodesHave2GraphableEvents({ + ancestorID, + originID, + firstChildID, + secondChildID, +}: { + ancestorID: string; + originID: string; + firstChildID: string; + secondChildID: string; +}): ResolverTree { + const ancestor: ResolverEvent = mockEndpointEvent({ + entityID: ancestorID, + name: ancestorID, + timestamp: 1, + parentEntityId: undefined, + }); + const ancestorClone: ResolverEvent = mockEndpointEvent({ + entityID: ancestorID, + name: ancestorID, + timestamp: 1, + parentEntityId: undefined, + }); + const origin: ResolverEvent = mockEndpointEvent({ + entityID: originID, + name: originID, + parentEntityId: ancestorID, + timestamp: 0, + }); + const originClone: ResolverEvent = mockEndpointEvent({ + entityID: originID, + name: originID, + parentEntityId: ancestorID, + timestamp: 0, + }); + const firstChild: ResolverEvent = mockEndpointEvent({ + entityID: firstChildID, + name: firstChildID, + parentEntityId: originID, + timestamp: 1, + }); + const firstChildClone: ResolverEvent = mockEndpointEvent({ + entityID: firstChildID, + name: firstChildID, + parentEntityId: originID, + timestamp: 1, + }); + const secondChild: ResolverEvent = mockEndpointEvent({ + entityID: secondChildID, + name: secondChildID, + parentEntityId: originID, + timestamp: 2, + }); + const secondChildClone: ResolverEvent = mockEndpointEvent({ + entityID: secondChildID, + name: secondChildID, + parentEntityId: originID, + timestamp: 2, + }); + + return ({ + entityID: originID, + children: { + childNodes: [ + { lifecycle: [firstChild, firstChildClone] }, + { lifecycle: [secondChild, secondChildClone] }, + ], + }, + ancestry: { + ancestors: [{ lifecycle: [ancestor, ancestorClone] }], + }, + lifecycle: [origin, originClone], + } as unknown) as ResolverTree; +} From 19127c287efd44718c9fe48459c5db7489c00506 Mon Sep 17 00:00:00 2001 From: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com> Date: Thu, 23 Jul 2020 18:15:38 -0400 Subject: [PATCH 06/96] [Security Solution][Endpoint] Clean up resolver query params on component dismount (#72902) --- .../security_solution/public/resolver/view/map.tsx | 6 ++++++ .../public/resolver/view/use_resolver_query_params.ts | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/x-pack/plugins/security_solution/public/resolver/view/map.tsx b/x-pack/plugins/security_solution/public/resolver/view/map.tsx index 69ff9c8e2351b..30aa4b63a138d 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/map.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/map.tsx @@ -10,6 +10,7 @@ import React, { useContext } from 'react'; import { useSelector } from 'react-redux'; +import { useEffectOnce } from 'react-use'; import { EuiLoadingSpinner } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import * as selectors from '../store/selectors'; @@ -19,6 +20,7 @@ import { ProcessEventDot } from './process_event_dot'; import { useCamera } from './use_camera'; import { SymbolDefinitions, useResolverTheme } from './assets'; import { useStateSyncingActions } from './use_state_syncing_actions'; +import { useResolverQueryParams } from './use_resolver_query_params'; import { StyledMapContainer, StyledPanel, GraphContainer } from './styles'; import { entityId } from '../../../common/endpoint/models/event'; import { SideEffectContext } from './side_effect_context'; @@ -66,6 +68,10 @@ export const ResolverMap = React.memo(function ({ const hasError = useSelector(selectors.hasError); const activeDescendantId = useSelector(selectors.ariaActiveDescendant); const { colorMap } = useResolverTheme(); + const { cleanUpQueryParams } = useResolverQueryParams(); + useEffectOnce(() => { + return () => cleanUpQueryParams(); + }); return ( diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_resolver_query_params.ts b/x-pack/plugins/security_solution/public/resolver/view/use_resolver_query_params.ts index 3c342ae575aa0..84d954de6ef27 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_resolver_query_params.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_resolver_query_params.ts @@ -63,8 +63,19 @@ export function useResolverQueryParams() { }; }, [urlSearch, uniqueCrumbIdKey, uniqueCrumbEventKey]); + const cleanUpQueryParams = () => { + const crumbsToPass = { + ...querystring.parse(urlSearch.slice(1)), + }; + delete crumbsToPass[uniqueCrumbIdKey]; + delete crumbsToPass[uniqueCrumbEventKey]; + const relativeURL = { search: querystring.stringify(crumbsToPass) }; + history.replace(relativeURL); + }; + return { pushToQueryParams, queryParams, + cleanUpQueryParams, }; } From 5a1972b8341c65f50a1b07109e90867248d4475d Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Thu, 23 Jul 2020 17:46:26 -0500 Subject: [PATCH 07/96] Index patterns on alias - reenable functional tests (#71802) * reenable test * nav to management --- test/functional/apps/management/_handle_alias.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/test/functional/apps/management/_handle_alias.js b/test/functional/apps/management/_handle_alias.js index 902b49eacdc00..67a4445d17aa0 100644 --- a/test/functional/apps/management/_handle_alias.js +++ b/test/functional/apps/management/_handle_alias.js @@ -26,8 +26,7 @@ export default function ({ getService, getPageObjects }) { const security = getService('security'); const PageObjects = getPageObjects(['common', 'home', 'settings', 'discover', 'timePicker']); - // FLAKY: https://github.com/elastic/kibana/issues/59717 - describe.skip('Index patterns on aliases', function () { + describe('Index patterns on aliases', function () { before(async function () { await security.testUser.setRoles(['kibana_admin', 'test_alias_reader']); await esArchiver.loadIfNeeded('alias'); @@ -50,9 +49,8 @@ export default function ({ getService, getPageObjects }) { }); it('should be able to create index pattern without time field', async function () { - await PageObjects.settings.createIndexPattern('alias1', null); - const patternName = await PageObjects.settings.getIndexPageHeading(); - expect(patternName).to.be('alias1*'); + await PageObjects.settings.navigateTo(); + await PageObjects.settings.createIndexPattern('alias1*', null); }); it('should be able to discover and verify no of hits for alias1', async function () { @@ -64,9 +62,8 @@ export default function ({ getService, getPageObjects }) { }); it('should be able to create index pattern with timefield', async function () { - await PageObjects.settings.createIndexPattern('alias2', 'date'); - const patternName = await PageObjects.settings.getIndexPageHeading(); - expect(patternName).to.be('alias2*'); + await PageObjects.settings.navigateTo(); + await PageObjects.settings.createIndexPattern('alias2*', 'date'); }); it('should be able to discover and verify no of hits for alias2', async function () { From f5a81deadbe4b2431cf423be8c8af85a3b3caf2d Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Thu, 23 Jul 2020 19:04:23 -0400 Subject: [PATCH 08/96] Exclude variables from rendered workpad (#72970) --- x-pack/plugins/canvas/public/state/selectors/workpad.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/canvas/public/state/selectors/workpad.ts b/x-pack/plugins/canvas/public/state/selectors/workpad.ts index 1d7ea05daaa61..a677bcaf29e61 100644 --- a/x-pack/plugins/canvas/public/state/selectors/workpad.ts +++ b/x-pack/plugins/canvas/public/state/selectors/workpad.ts @@ -497,7 +497,7 @@ export function getRenderedWorkpad(state: State) { const workpad = getWorkpad(state); // eslint-disable-next-line no-unused-vars - const { pages, ...rest } = workpad; + const { pages, variables, ...rest } = workpad; return { pages: renderedPages, From aec18923daf41e32168430a1fb788a058dbb1683 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 23 Jul 2020 17:11:04 -0600 Subject: [PATCH 09/96] update discuss link from siem to security (#72886) Co-authored-by: Elastic Machine --- .../public/common/components/help_menu/index.tsx | 2 +- .../public/common/components/news_feed/helpers.test.ts | 4 ++-- x-pack/plugins/security_solution/public/common/mock/news.ts | 2 +- .../plugins/security_solution/public/common/mock/raw_news.ts | 2 +- .../security_solution/public/overview/pages/summary.tsx | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/help_menu/index.tsx b/x-pack/plugins/security_solution/public/common/components/help_menu/index.tsx index f4477740f7b58..1eaa16fd058a5 100644 --- a/x-pack/plugins/security_solution/public/common/components/help_menu/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/help_menu/index.tsx @@ -39,7 +39,7 @@ export const HelpMenu = React.memo(() => { }, { linkType: 'discuss', - href: 'https://discuss.elastic.co/c/siem', + href: 'https://discuss.elastic.co/c/security', target: '_blank', rel: 'noopener', }, diff --git a/x-pack/plugins/security_solution/public/common/components/news_feed/helpers.test.ts b/x-pack/plugins/security_solution/public/common/components/news_feed/helpers.test.ts index cdd04b50a6d50..35a59f4d18e8b 100644 --- a/x-pack/plugins/security_solution/public/common/components/news_feed/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/news_feed/helpers.test.ts @@ -144,7 +144,7 @@ describe('helpers', () => { hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', imageUrl: 'https://aws1.discourse-cdn.com/elastic/original/3X/f/8/f8c3d0b9971cfcd0be349d973aa5799f71d280cc.png?blade=securitysolutionfeed', - linkUrl: 'https://discuss.elastic.co/c/siem?blade=securitysolutionfeed', + linkUrl: 'https://discuss.elastic.co/c/security?blade=securitysolutionfeed', publishOn: expect.any(Date), title: 'Got SIEM Questions?', }, @@ -284,7 +284,7 @@ describe('helpers', () => { }, link_text: null, link_url: { - en: 'https://discuss.elastic.co/c/siem?blade=securitysolutionfeed', + en: 'https://discuss.elastic.co/c/security?blade=securitysolutionfeed', ja: translatedLinkUrl, }, languages: null, diff --git a/x-pack/plugins/security_solution/public/common/mock/news.ts b/x-pack/plugins/security_solution/public/common/mock/news.ts index 3e421ce19ae9c..51449347e649a 100644 --- a/x-pack/plugins/security_solution/public/common/mock/news.ts +++ b/x-pack/plugins/security_solution/public/common/mock/news.ts @@ -16,7 +16,7 @@ export const rawNewsApiResponse: RawNewsApiResponse = { "There's an awesome community of Elastic SIEM users out there. Join the discussion about configuring, learning, and using the Elastic SIEM app, and detecting threats!", }, link_text: null, - link_url: { en: 'https://discuss.elastic.co/c/siem?blade=securitysolutionfeed' }, + link_url: { en: 'https://discuss.elastic.co/c/security?blade=securitysolutionfeed' }, languages: null, badge: { en: '7.6' }, image_url: { diff --git a/x-pack/plugins/security_solution/public/common/mock/raw_news.ts b/x-pack/plugins/security_solution/public/common/mock/raw_news.ts index 85bef15a41b23..9cd06ed107956 100644 --- a/x-pack/plugins/security_solution/public/common/mock/raw_news.ts +++ b/x-pack/plugins/security_solution/public/common/mock/raw_news.ts @@ -17,7 +17,7 @@ export const rawNewsJSON = ` }, "link_text":null, "link_url":{ - "en":"https://discuss.elastic.co/c/siem?blade=securitysolutionfeed" + "en":"https://discuss.elastic.co/c/security?blade=securitysolutionfeed" }, "languages":null, "badge":{ diff --git a/x-pack/plugins/security_solution/public/overview/pages/summary.tsx b/x-pack/plugins/security_solution/public/overview/pages/summary.tsx index 0f20e8bea9dc5..d8260858aa245 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/summary.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/summary.tsx @@ -71,7 +71,7 @@ export const Summary = React.memo(() => { defaultMessage="If you have input or suggestions regarding your experience with Elastic SIEM, please feel free to {feedback}." values={{ feedback: ( - + Date: Thu, 23 Jul 2020 17:20:24 -0600 Subject: [PATCH 10/96] [maps] fix data driven style properties not working when cloned layer contains joins (#73124) * [maps] fix data driven style properties not working when cloned layer contains joins * tslint * handle case where metrics is not provided * tslint --- .../maps/common/descriptor_types/sources.ts | 2 +- .../maps/public/classes/layers/layer.test.ts | 128 ++++++++++++++++++ .../maps/public/classes/layers/layer.tsx | 49 ++++++- 3 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/maps/public/classes/layers/layer.test.ts diff --git a/x-pack/plugins/maps/common/descriptor_types/sources.ts b/x-pack/plugins/maps/common/descriptor_types/sources.ts index 7eda37bf53351..6e8884d942e19 100644 --- a/x-pack/plugins/maps/common/descriptor_types/sources.ts +++ b/x-pack/plugins/maps/common/descriptor_types/sources.ts @@ -168,6 +168,7 @@ export type LayerDescriptor = { __trackedLayerDescriptor?: LayerDescriptor; alpha?: number; id: string; + joins?: JoinDescriptor[]; label?: string | null; areLabelsOnTop?: boolean; minZoom?: number; @@ -180,7 +181,6 @@ export type LayerDescriptor = { }; export type VectorLayerDescriptor = LayerDescriptor & { - joins?: JoinDescriptor[]; style?: VectorStyleDescriptor; }; diff --git a/x-pack/plugins/maps/public/classes/layers/layer.test.ts b/x-pack/plugins/maps/public/classes/layers/layer.test.ts new file mode 100644 index 0000000000000..f25ecd7106457 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/layer.test.ts @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable max-classes-per-file */ + +import { AbstractLayer } from './layer'; +import { ISource } from '../sources/source'; +import { IStyle } from '../styles/style'; +import { AGG_TYPE, FIELD_ORIGIN, LAYER_STYLE_TYPE, VECTOR_STYLES } from '../../../common/constants'; +import { ESTermSourceDescriptor, VectorStyleDescriptor } from '../../../common/descriptor_types'; +import { getDefaultDynamicProperties } from '../styles/vector/vector_style_defaults'; + +jest.mock('uuid/v4', () => { + return function () { + return '12345'; + }; +}); + +class MockLayer extends AbstractLayer {} + +class MockSource { + cloneDescriptor() { + return {}; + } + + getDisplayName() { + return 'mySource'; + } +} + +class MockStyle {} + +describe('cloneDescriptor', () => { + describe('with joins', () => { + const styleDescriptor = { + type: LAYER_STYLE_TYPE.VECTOR, + properties: { + ...getDefaultDynamicProperties(), + }, + } as VectorStyleDescriptor; + // @ts-expect-error + styleDescriptor.properties[VECTOR_STYLES.FILL_COLOR].options.field = { + name: '__kbnjoin__count__557d0f15', + origin: FIELD_ORIGIN.JOIN, + }; + // @ts-expect-error + styleDescriptor.properties[VECTOR_STYLES.LINE_COLOR].options.field = { + name: 'bytes', + origin: FIELD_ORIGIN.SOURCE, + }; + // @ts-expect-error + styleDescriptor.properties[VECTOR_STYLES.LABEL_BORDER_COLOR].options.field = { + name: '__kbnjoin__count__6666666666', + origin: FIELD_ORIGIN.JOIN, + }; + + test('Should update data driven styling properties using join fields', async () => { + const layerDescriptor = AbstractLayer.createDescriptor({ + style: styleDescriptor, + joins: [ + { + leftField: 'iso2', + right: { + id: '557d0f15', + indexPatternId: 'myIndexPattern', + indexPatternTitle: 'logs-*', + metrics: [{ type: AGG_TYPE.COUNT }], + term: 'myTermField', + type: 'joinSource', + }, + }, + ], + }); + const layer = new MockLayer({ + layerDescriptor, + source: (new MockSource() as unknown) as ISource, + style: (new MockStyle() as unknown) as IStyle, + }); + const clonedDescriptor = await layer.cloneDescriptor(); + const clonedStyleProps = (clonedDescriptor.style as VectorStyleDescriptor).properties; + // Should update style field belonging to join + // @ts-expect-error + expect(clonedStyleProps[VECTOR_STYLES.FILL_COLOR].options.field.name).toEqual( + '__kbnjoin__count__12345' + ); + // Should not update style field belonging to source + // @ts-expect-error + expect(clonedStyleProps[VECTOR_STYLES.LINE_COLOR].options.field.name).toEqual('bytes'); + // Should not update style feild belonging to different join + // @ts-expect-error + expect(clonedStyleProps[VECTOR_STYLES.LABEL_BORDER_COLOR].options.field.name).toEqual( + '__kbnjoin__count__6666666666' + ); + }); + + test('Should update data driven styling properties using join fields when metrics are not provided', async () => { + const layerDescriptor = AbstractLayer.createDescriptor({ + style: styleDescriptor, + joins: [ + { + leftField: 'iso2', + right: ({ + id: '557d0f15', + indexPatternId: 'myIndexPattern', + indexPatternTitle: 'logs-*', + term: 'myTermField', + type: 'joinSource', + } as unknown) as ESTermSourceDescriptor, + }, + ], + }); + const layer = new MockLayer({ + layerDescriptor, + source: (new MockSource() as unknown) as ISource, + style: (new MockStyle() as unknown) as IStyle, + }); + const clonedDescriptor = await layer.cloneDescriptor(); + const clonedStyleProps = (clonedDescriptor.style as VectorStyleDescriptor).properties; + // Should update style field belonging to join + // @ts-expect-error + expect(clonedStyleProps[VECTOR_STYLES.FILL_COLOR].options.field.name).toEqual( + '__kbnjoin__count__12345' + ); + }); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index d8def155a9185..424100c5a7e3a 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -14,16 +14,26 @@ import { i18n } from '@kbn/i18n'; import { FeatureCollection } from 'geojson'; import { DataRequest } from '../util/data_request'; import { + AGG_TYPE, + FIELD_ORIGIN, MAX_ZOOM, MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER, MIN_ZOOM, SOURCE_DATA_REQUEST_ID, + STYLE_TYPE, } from '../../../common/constants'; import { copyPersistentState } from '../../reducers/util'; -import { LayerDescriptor, MapExtent, StyleDescriptor } from '../../../common/descriptor_types'; +import { + AggDescriptor, + JoinDescriptor, + LayerDescriptor, + MapExtent, + StyleDescriptor, +} from '../../../common/descriptor_types'; import { Attribution, ImmutableSourceProperty, ISource, SourceEditorArgs } from '../sources/source'; import { DataRequestContext } from '../../actions'; import { IStyle } from '../styles/style'; +import { getJoinAggKey } from '../../../common/get_agg_key'; export interface ILayer { getBounds(dataRequestContext: DataRequestContext): Promise; @@ -157,10 +167,43 @@ export class AbstractLayer implements ILayer { clonedDescriptor.sourceDescriptor = this.getSource().cloneDescriptor(); if (clonedDescriptor.joins) { - // @ts-expect-error - clonedDescriptor.joins.forEach((joinDescriptor) => { + clonedDescriptor.joins.forEach((joinDescriptor: JoinDescriptor) => { + const originalJoinId = joinDescriptor.right.id!; + // right.id is uuid used to track requests in inspector joinDescriptor.right.id = uuid(); + + // Update all data driven styling properties using join fields + if (clonedDescriptor.style && 'properties' in clonedDescriptor.style) { + const metrics = + joinDescriptor.right.metrics && joinDescriptor.right.metrics.length + ? joinDescriptor.right.metrics + : [{ type: AGG_TYPE.COUNT }]; + metrics.forEach((metricsDescriptor: AggDescriptor) => { + const originalJoinKey = getJoinAggKey({ + aggType: metricsDescriptor.type, + aggFieldName: metricsDescriptor.field ? metricsDescriptor.field : '', + rightSourceId: originalJoinId, + }); + const newJoinKey = getJoinAggKey({ + aggType: metricsDescriptor.type, + aggFieldName: metricsDescriptor.field ? metricsDescriptor.field : '', + rightSourceId: joinDescriptor.right.id!, + }); + + Object.keys(clonedDescriptor.style.properties).forEach((key) => { + const styleProp = clonedDescriptor.style.properties[key]; + if ( + styleProp.type === STYLE_TYPE.DYNAMIC && + styleProp.options.field && + styleProp.options.field.origin === FIELD_ORIGIN.JOIN && + styleProp.options.field.name === originalJoinKey + ) { + styleProp.options.field.name = newJoinKey; + } + }); + }); + } }); } return clonedDescriptor; From 8021616e4160cbcc8c5d605fd731c3138deefa3f Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 23 Jul 2020 18:41:32 -0500 Subject: [PATCH 11/96] skip ingest pipeline api tests JSON formatting appears to have changed modestly. Tracking at #73170. --- .../apis/management/ingest_pipelines/ingest_pipelines.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts index a48460d7a3b23..6a827298521dd 100644 --- a/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts @@ -16,7 +16,7 @@ export default function ({ getService }: FtrProviderContext) { const { createPipeline, deletePipeline } = registerEsHelpers(getService); - describe('Pipelines', function () { + describe.skip('Pipelines', function () { describe('Create', () => { const PIPELINE_ID = 'test_create_pipeline'; const REQUIRED_FIELDS_PIPELINE_ID = 'test_create_required_fields_pipeline'; From 5f4b1a36896d6cf8fad2dd0e4487ffcf6aea7a43 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 23 Jul 2020 18:30:42 -0600 Subject: [PATCH 12/96] [Maps] fix tile layer attibution text and attribution link validation errors (#73160) * [Maps] fix tile layer attibution text and attribution link validation errors * clean up jest test * tslint * one more tslint --- .../xyz_tms_editor.test.tsx.snap | 237 ++++++++++++++++++ .../sources/xyz_tms_source/layer_wizard.tsx | 7 +- .../xyz_tms_source/xyz_tms_editor.test.tsx | 37 +++ .../sources/xyz_tms_source/xyz_tms_editor.tsx | 96 +++---- 4 files changed, 318 insertions(+), 59 deletions(-) create mode 100644 x-pack/plugins/maps/public/classes/sources/xyz_tms_source/__snapshots__/xyz_tms_editor.test.tsx.snap create mode 100644 x-pack/plugins/maps/public/classes/sources/xyz_tms_source/xyz_tms_editor.test.tsx diff --git a/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/__snapshots__/xyz_tms_editor.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/__snapshots__/xyz_tms_editor.test.tsx.snap new file mode 100644 index 0000000000000..b8ed4a727fad0 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/__snapshots__/xyz_tms_editor.test.tsx.snap @@ -0,0 +1,237 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`attribution validation should provide no validation errors when attribution text and attribution url are provided 1`] = ` + + + + + + + + + + + +`; + +exports[`attribution validation should provide validation error when attribution text is provided without attribution url 1`] = ` + + + + + + + + + + + +`; + +exports[`attribution validation should provide validation error when attribution url is provided without attribution text 1`] = ` + + + + + + + + + + + +`; + +exports[`should render 1`] = ` + + + + + + + + + + + +`; diff --git a/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx index 48c526855d3a4..b0344a3e0e318 100644 --- a/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx @@ -19,7 +19,12 @@ export const tmsLayerWizardConfig: LayerWizard = { }), icon: 'grid', renderWizard: ({ previewLayers }: RenderWizardArguments) => { - const onSourceConfigChange = (sourceConfig: XYZTMSSourceConfig) => { + const onSourceConfigChange = (sourceConfig: XYZTMSSourceConfig | null) => { + if (!sourceConfig) { + previewLayers([]); + return; + } + const layerDescriptor = TileLayer.createDescriptor({ sourceDescriptor: XYZTMSSource.createDescriptor(sourceConfig), }); diff --git a/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/xyz_tms_editor.test.tsx b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/xyz_tms_editor.test.tsx new file mode 100644 index 0000000000000..71f78c3e15152 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/xyz_tms_editor.test.tsx @@ -0,0 +1,37 @@ +/* + * 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 { shallow } from 'enzyme'; +import { XYZTMSEditor } from './xyz_tms_editor'; + +const onSourceConfigChange = () => {}; + +test('should render', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); +}); + +describe('attribution validation', () => { + test('should provide validation error when attribution text is provided without attribution url', () => { + const component = shallow(); + component.setState({ attributionText: 'myAttribtionLabel' }); + expect(component).toMatchSnapshot(); + }); + + test('should provide validation error when attribution url is provided without attribution text', () => { + const component = shallow(); + component.setState({ attributionUrl: 'http://mySource' }); + expect(component).toMatchSnapshot(); + }); + + test('should provide no validation errors when attribution text and attribution url are provided', () => { + const component = shallow(); + component.setState({ attributionText: 'myAttribtionLabel' }); + component.setState({ attributionUrl: 'http://mySource' }); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/xyz_tms_editor.tsx b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/xyz_tms_editor.tsx index bf5f2c3dfe04d..5583f637b4471 100644 --- a/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/xyz_tms_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/xyz_tms_editor.tsx @@ -9,70 +9,56 @@ import React, { Component, ChangeEvent } from 'react'; import _ from 'lodash'; import { EuiFormRow, EuiFieldText, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { AttributionDescriptor } from '../../../../common/descriptor_types'; -export type XYZTMSSourceConfig = AttributionDescriptor & { +export type XYZTMSSourceConfig = { urlTemplate: string; + attributionText: string; + attributionUrl: string; }; -export interface Props { - onSourceConfigChange: (sourceConfig: XYZTMSSourceConfig) => void; +interface Props { + onSourceConfigChange: (sourceConfig: XYZTMSSourceConfig | null) => void; } interface State { - tmsInput: string; - tmsCanPreview: boolean; + url: string; attributionText: string; attributionUrl: string; } export class XYZTMSEditor extends Component { state = { - tmsInput: '', - tmsCanPreview: false, + url: '', attributionText: '', attributionUrl: '', }; - _sourceConfigChange = _.debounce((updatedSourceConfig: XYZTMSSourceConfig) => { - if (this.state.tmsCanPreview) { - this.props.onSourceConfigChange(updatedSourceConfig); - } - }, 2000); - - _handleTMSInputChange(e: ChangeEvent) { - const url = e.target.value; + _previewLayer = _.debounce(() => { + const { url, attributionText, attributionUrl } = this.state; - const canPreview = + const isUrlValid = url.indexOf('{x}') >= 0 && url.indexOf('{y}') >= 0 && url.indexOf('{z}') >= 0; - this.setState( - { - tmsInput: url, - tmsCanPreview: canPreview, - }, - () => this._sourceConfigChange({ urlTemplate: url }) - ); - } + const sourceConfig = isUrlValid + ? { + urlTemplate: url, + attributionText, + attributionUrl, + } + : null; + this.props.onSourceConfigChange(sourceConfig); + }, 500); - _handleTMSAttributionChange(attributionUpdate: AttributionDescriptor) { - this.setState( - { - attributionUrl: attributionUpdate.attributionUrl || '', - attributionText: attributionUpdate.attributionText || '', - }, - () => { - const { attributionText, attributionUrl, tmsInput } = this.state; + _onUrlChange = (event: ChangeEvent) => { + this.setState({ url: event.target.value }, this._previewLayer); + }; - if (tmsInput && attributionText && attributionUrl) { - this._sourceConfigChange({ - urlTemplate: tmsInput, - attributionText, - attributionUrl, - }); - } - } - ); - } + _onAttributionTextChange = (event: ChangeEvent) => { + this.setState({ attributionText: event.target.value }, this._previewLayer); + }; + + _onAttributionUrlChange = (event: ChangeEvent) => { + this.setState({ attributionUrl: event.target.value }, this._previewLayer); + }; render() { const { attributionText, attributionUrl } = this.state; @@ -81,11 +67,13 @@ export class XYZTMSEditor extends Component { this._handleTMSInputChange(e)} + onChange={this._onUrlChange} /> { }), ]} > - ) => - this._handleTMSAttributionChange({ attributionText: target.value }) - } - /> + { }), ]} > - ) => - this._handleTMSAttributionChange({ attributionUrl: target.value }) - } - /> + ); From 47b3a947985c6bb1b26fa04f797fe2d454bd2a55 Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Thu, 23 Jul 2020 20:57:54 -0400 Subject: [PATCH 13/96] [Security Solution][Endpoint] Task/policy save modal text change, remove duplicate policy details text (#73130) [Security Solution][Endpoint] updates policy details text --- .../pages/policy/view/policy_details.test.tsx | 2 +- .../pages/policy/view/policy_details.tsx | 2 +- .../policy/view/policy_forms/config_form.tsx | 16 ++-------------- .../policy/view/policy_forms/events/linux.tsx | 6 ------ .../policy/view/policy_forms/events/mac.tsx | 6 ------ .../policy/view/policy_forms/events/windows.tsx | 3 --- .../view/policy_forms/protections/malware.tsx | 3 --- 7 files changed, 4 insertions(+), 34 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx index 8612b15f89857..4f7c14735fe21 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx @@ -232,7 +232,7 @@ describe('Policy Details', () => { ); expect(warningCallout).toHaveLength(1); expect(warningCallout.text()).toEqual( - 'This action will update 5 hostsSaving these changes will apply the updates to all active endpoints assigned to this policy' + 'This action will update 5 hostsSaving these changes will apply updates to all endpoints assigned to this policy' ); }); it('should close dialog if cancel button is clicked', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx index 9576e1aedcaf1..288bc484c23b5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx @@ -306,7 +306,7 @@ const ConfirmUpdate = React.memo<{ > diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/config_form.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/config_form.tsx index 763931bc2d3d7..8e3c4138efb36 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/config_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/config_form.tsx @@ -34,17 +34,10 @@ export const ConfigForm: React.FC<{ */ supportedOss: React.ReactNode; children: React.ReactNode; - /** - * A description for the component. - */ - description: string; - /** - * The `data-test-subj` attribute to append to a certain child element. - */ dataTestSubj: string; /** React Node to be put on the right corner of the card */ rightCorner: React.ReactNode; -}> = React.memo(({ type, supportedOss, children, dataTestSubj, rightCorner, description }) => { +}> = React.memo(({ type, supportedOss, children, dataTestSubj, rightCorner }) => { const typeTitle = useMemo(() => { return ( @@ -85,12 +78,7 @@ export const ConfigForm: React.FC<{ return ( - + {children} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx index d7bae0d2e6bad..66126adb7a4e1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx @@ -102,12 +102,6 @@ export const LinuxEvents = React.memo(() => { type={i18n.translate('xpack.securitySolution.endpoint.policy.details.eventCollection', { defaultMessage: 'Event Collection', })} - description={i18n.translate( - 'xpack.securitySolution.endpoint.policy.details.eventCollectionLabel', - { - defaultMessage: 'Event Collection', - } - )} supportedOss={i18n.translate('xpack.securitySolution.endpoint.policy.details.linux', { defaultMessage: 'Linux', })} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx index 37709ff608857..dc70fc0ba0f4f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx @@ -102,12 +102,6 @@ export const MacEvents = React.memo(() => { type={i18n.translate('xpack.securitySolution.endpoint.policy.details.eventCollection', { defaultMessage: 'Event Collection', })} - description={i18n.translate( - 'xpack.securitySolution.endpoint.policy.details.eventCollectionLabel', - { - defaultMessage: 'Event Collection', - } - )} supportedOss={i18n.translate('xpack.securitySolution.endpoint.policy.details.mac', { defaultMessage: 'Mac', })} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx index 3c7ecae0d9b4e..5acdf67922a3a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx @@ -142,9 +142,6 @@ export const WindowsEvents = React.memo(() => { type={i18n.translate('xpack.securitySolution.endpoint.policy.details.eventCollection', { defaultMessage: 'Event Collection', })} - description={i18n.translate('xpack.securitySolution.endpoint.policy.details.windowsLabel', { - defaultMessage: 'Windows', - })} supportedOss={i18n.translate('xpack.securitySolution.endpoint.policy.details.windows', { defaultMessage: 'Windows', })} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx index 23ac6cc5b813d..dee1e27782e69 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -174,9 +174,6 @@ export const MalwareProtections = React.memo(() => { defaultMessage: 'Windows, Mac', })} dataTestSubj="malwareProtectionsForm" - description={i18n.translate('xpack.securitySolution.endpoint.policy.details.malwareLabel', { - defaultMessage: 'Malware', - })} rightCorner={protectionSwitch} > {radioButtons} From c2ad4bf048c9623aa897efbafd40745062ded338 Mon Sep 17 00:00:00 2001 From: Andrew Cholakian Date: Thu, 23 Jul 2020 20:02:12 -0500 Subject: [PATCH 14/96] [Uptime] Use manual intervals for ping histogram (#72928) * [Uptime] Use manual intervals for ping histogram Fixes https://github.com/elastic/uptime/issues/215 Prior to this we'd get too few buckets in some ranges. * Update test fixtures, remove overly-specific checks * Remove unused import --- .../server/lib/requests/get_ping_histogram.ts | 24 ++---- .../uptime/rest/fixtures/ping_histogram.json | 76 ++++++++++++++----- .../fixtures/ping_histogram_by_filter.json | 76 ++++++++++++++----- .../rest/fixtures/ping_histogram_by_id.json | 76 ++++++++++++++----- .../apis/uptime/rest/ping_histogram.ts | 11 --- 5 files changed, 175 insertions(+), 88 deletions(-) diff --git a/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts index a74b55c24e227..970d9ad166982 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts @@ -8,6 +8,7 @@ import { UMElasticsearchQueryFn } from '../adapters'; import { getFilterClause } from '../helper'; import { HistogramResult, HistogramQueryResult } from '../../../common/runtime_types'; import { QUERY } from '../../../common/constants'; +import { getHistogramInterval } from '../helper/get_histogram_interval'; export interface GetPingHistogramParams { /** @member dateRangeStart timestamp bounds */ @@ -36,22 +37,6 @@ export const getPingHistogram: UMElasticsearchQueryFn< } const filter = getFilterClause(from, to, additionalFilters); - const seriesHistogram: any = {}; - - if (bucketSize) { - seriesHistogram.date_histogram = { - field: '@timestamp', - fixed_interval: bucketSize, - missing: 0, - }; - } else { - seriesHistogram.auto_date_histogram = { - field: '@timestamp', - buckets: QUERY.DEFAULT_BUCKET_COUNT, - missing: 0, - }; - } - const params = { index: dynamicSettings.heartbeatIndices, body: { @@ -63,7 +48,12 @@ export const getPingHistogram: UMElasticsearchQueryFn< size: 0, aggs: { timeseries: { - ...seriesHistogram, + date_histogram: { + field: '@timestamp', + fixed_interval: + bucketSize || getHistogramInterval(from, to, QUERY.DEFAULT_BUCKET_COUNT) + 'ms', + missing: 0, + }, aggs: { down: { filter: { diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/ping_histogram.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/ping_histogram.json index 562ba64c24b0b..85ce545ed92b0 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/ping_histogram.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/ping_histogram.json @@ -1,121 +1,157 @@ { "histogram": [ { - "x": 1568172664000, + "x": 1568172657286, "downCount": 7, "upCount": 93, "y": 1 }, { - "x": 1568172694000, + "x": 1568172680087, "downCount": 7, "upCount": 93, "y": 1 }, { - "x": 1568172724000, + "x": 1568172702888, "downCount": 7, "upCount": 93, "y": 1 }, { - "x": 1568172754000, + "x": 1568172725689, + "downCount": 0, + "upCount": 0, + "y": 1 + }, + { + "x": 1568172748490, "downCount": 7, "upCount": 93, "y": 1 }, { - "x": 1568172784000, + "x": 1568172771291, "downCount": 7, "upCount": 93, "y": 1 }, { - "x": 1568172814000, + "x": 1568172794092, "downCount": 8, "upCount": 92, "y": 1 }, { - "x": 1568172844000, + "x": 1568172816893, + "downCount": 0, + "upCount": 0, + "y": 1 + }, + { + "x": 1568172839694, "downCount": 7, "upCount": 93, "y": 1 }, { - "x": 1568172874000, + "x": 1568172862495, "downCount": 7, "upCount": 93, "y": 1 }, { - "x": 1568172904000, + "x": 1568172885296, "downCount": 7, "upCount": 93, "y": 1 }, { - "x": 1568172934000, + "x": 1568172908097, + "downCount": 0, + "upCount": 0, + "y": 1 + }, + { + "x": 1568172930898, "downCount": 7, "upCount": 93, "y": 1 }, { - "x": 1568172964000, + "x": 1568172953699, "downCount": 7, "upCount": 93, "y": 1 }, { - "x": 1568172994000, + "x": 1568172976500, "downCount": 8, "upCount": 92, "y": 1 }, { - "x": 1568173024000, + "x": 1568172999301, + "downCount": 0, + "upCount": 0, + "y": 1 + }, + { + "x": 1568173022102, "downCount": 7, "upCount": 93, "y": 1 }, { - "x": 1568173054000, + "x": 1568173044903, "downCount": 7, "upCount": 93, "y": 1 }, { - "x": 1568173084000, + "x": 1568173067704, "downCount": 7, "upCount": 93, "y": 1 }, { - "x": 1568173114000, + "x": 1568173090505, + "downCount": 0, + "upCount": 0, + "y": 1 + }, + { + "x": 1568173113306, "downCount": 7, "upCount": 93, "y": 1 }, { - "x": 1568173144000, + "x": 1568173136107, "downCount": 7, "upCount": 93, "y": 1 }, { - "x": 1568173174000, + "x": 1568173158908, "downCount": 8, "upCount": 92, "y": 1 }, { - "x": 1568173204000, + "x": 1568173181709, "downCount": 7, "upCount": 93, "y": 1 }, { - "x": 1568173234000, + "x": 1568173204510, + "downCount": 0, + "upCount": 0, + "y": 1 + }, + { + "x": 1568173227311, "downCount": 7, "upCount": 93, "y": 1 diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/ping_histogram_by_filter.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/ping_histogram_by_filter.json index 42be715c4acd4..fe5dc9dd3da3f 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/ping_histogram_by_filter.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/ping_histogram_by_filter.json @@ -1,121 +1,157 @@ { "histogram": [ { - "x": 1568172664000, + "x": 1568172657286, "downCount": 0, "upCount": 93, "y": 1 }, { - "x": 1568172694000, + "x": 1568172680087, "downCount": 0, "upCount": 93, "y": 1 }, { - "x": 1568172724000, + "x": 1568172702888, "downCount": 0, "upCount": 93, "y": 1 }, { - "x": 1568172754000, + "x": 1568172725689, + "downCount": 0, + "upCount": 0, + "y": 1 + }, + { + "x": 1568172748490, "downCount": 0, "upCount": 93, "y": 1 }, { - "x": 1568172784000, + "x": 1568172771291, "downCount": 0, "upCount": 93, "y": 1 }, { - "x": 1568172814000, + "x": 1568172794092, "downCount": 0, "upCount": 92, "y": 1 }, { - "x": 1568172844000, + "x": 1568172816893, + "downCount": 0, + "upCount": 0, + "y": 1 + }, + { + "x": 1568172839694, "downCount": 0, "upCount": 93, "y": 1 }, { - "x": 1568172874000, + "x": 1568172862495, "downCount": 0, "upCount": 93, "y": 1 }, { - "x": 1568172904000, + "x": 1568172885296, "downCount": 0, "upCount": 93, "y": 1 }, { - "x": 1568172934000, + "x": 1568172908097, + "downCount": 0, + "upCount": 0, + "y": 1 + }, + { + "x": 1568172930898, "downCount": 0, "upCount": 93, "y": 1 }, { - "x": 1568172964000, + "x": 1568172953699, "downCount": 0, "upCount": 93, "y": 1 }, { - "x": 1568172994000, + "x": 1568172976500, "downCount": 0, "upCount": 92, "y": 1 }, { - "x": 1568173024000, + "x": 1568172999301, + "downCount": 0, + "upCount": 0, + "y": 1 + }, + { + "x": 1568173022102, "downCount": 0, "upCount": 93, "y": 1 }, { - "x": 1568173054000, + "x": 1568173044903, "downCount": 0, "upCount": 93, "y": 1 }, { - "x": 1568173084000, + "x": 1568173067704, "downCount": 0, "upCount": 93, "y": 1 }, { - "x": 1568173114000, + "x": 1568173090505, + "downCount": 0, + "upCount": 0, + "y": 1 + }, + { + "x": 1568173113306, "downCount": 0, "upCount": 93, "y": 1 }, { - "x": 1568173144000, + "x": 1568173136107, "downCount": 0, "upCount": 93, "y": 1 }, { - "x": 1568173174000, + "x": 1568173158908, "downCount": 0, "upCount": 92, "y": 1 }, { - "x": 1568173204000, + "x": 1568173181709, "downCount": 0, "upCount": 93, "y": 1 }, { - "x": 1568173234000, + "x": 1568173204510, + "downCount": 0, + "upCount": 0, + "y": 1 + }, + { + "x": 1568173227311, "downCount": 0, "upCount": 93, "y": 1 diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/ping_histogram_by_id.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/ping_histogram_by_id.json index 9a726db616325..e54738cf5dbd7 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/ping_histogram_by_id.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/ping_histogram_by_id.json @@ -1,121 +1,157 @@ { "histogram": [ { - "x": 1568172664000, + "x": 1568172657286, "downCount": 0, "upCount": 1, "y": 1 }, { - "x": 1568172694000, + "x": 1568172680087, "downCount": 0, "upCount": 1, "y": 1 }, { - "x": 1568172724000, + "x": 1568172702888, "downCount": 0, "upCount": 1, "y": 1 }, { - "x": 1568172754000, + "x": 1568172725689, + "downCount": 0, + "upCount": 0, + "y": 1 + }, + { + "x": 1568172748490, "downCount": 0, "upCount": 1, "y": 1 }, { - "x": 1568172784000, + "x": 1568172771291, "downCount": 0, "upCount": 1, "y": 1 }, { - "x": 1568172814000, + "x": 1568172794092, "downCount": 0, "upCount": 1, "y": 1 }, { - "x": 1568172844000, + "x": 1568172816893, + "downCount": 0, + "upCount": 0, + "y": 1 + }, + { + "x": 1568172839694, "downCount": 0, "upCount": 1, "y": 1 }, { - "x": 1568172874000, + "x": 1568172862495, "downCount": 0, "upCount": 1, "y": 1 }, { - "x": 1568172904000, + "x": 1568172885296, "downCount": 0, "upCount": 1, "y": 1 }, { - "x": 1568172934000, + "x": 1568172908097, + "downCount": 0, + "upCount": 0, + "y": 1 + }, + { + "x": 1568172930898, "downCount": 0, "upCount": 1, "y": 1 }, { - "x": 1568172964000, + "x": 1568172953699, "downCount": 0, "upCount": 1, "y": 1 }, { - "x": 1568172994000, + "x": 1568172976500, "downCount": 0, "upCount": 1, "y": 1 }, { - "x": 1568173024000, + "x": 1568172999301, + "downCount": 0, + "upCount": 0, + "y": 1 + }, + { + "x": 1568173022102, "downCount": 0, "upCount": 1, "y": 1 }, { - "x": 1568173054000, + "x": 1568173044903, "downCount": 0, "upCount": 1, "y": 1 }, { - "x": 1568173084000, + "x": 1568173067704, "downCount": 0, "upCount": 1, "y": 1 }, { - "x": 1568173114000, + "x": 1568173090505, + "downCount": 0, + "upCount": 0, + "y": 1 + }, + { + "x": 1568173113306, "downCount": 0, "upCount": 1, "y": 1 }, { - "x": 1568173144000, + "x": 1568173136107, "downCount": 0, "upCount": 1, "y": 1 }, { - "x": 1568173174000, + "x": 1568173158908, "downCount": 0, "upCount": 1, "y": 1 }, { - "x": 1568173204000, + "x": 1568173181709, "downCount": 0, "upCount": 1, "y": 1 }, { - "x": 1568173234000, + "x": 1568173204510, + "downCount": 0, + "upCount": 0, + "y": 1 + }, + { + "x": 1568173227311, "downCount": 0, "upCount": 1, "y": 1 diff --git a/x-pack/test/api_integration/apis/uptime/rest/ping_histogram.ts b/x-pack/test/api_integration/apis/uptime/rest/ping_histogram.ts index ffcb1a829f0f8..b2504e3b921f7 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/ping_histogram.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/ping_histogram.ts @@ -6,7 +6,6 @@ import { expectFixtureEql } from './helper/expect_fixture_eql'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { assertCloseTo } from '../../../../../plugins/uptime/server/lib/helper'; export default function ({ getService }: FtrProviderContext) { describe('pingHistogram', () => { @@ -21,10 +20,6 @@ export default function ({ getService }: FtrProviderContext) { ); const data = apiResponse.body; - // manually testing this value and then removing it to avoid flakiness - const { interval } = data; - assertCloseTo(interval, 22801, 100); - delete data.interval; expectFixtureEql(data, 'ping_histogram'); }); @@ -38,9 +33,6 @@ export default function ({ getService }: FtrProviderContext) { ); const data = apiResponse.body; - const { interval } = data; - assertCloseTo(interval, 22801, 100); - delete data.interval; expectFixtureEql(data, 'ping_histogram_by_id'); }); @@ -55,9 +47,6 @@ export default function ({ getService }: FtrProviderContext) { ); const data = apiResponse.body; - const { interval } = data; - assertCloseTo(interval, 22801, 100); - delete data.interval; expectFixtureEql(data, 'ping_histogram_by_filter'); }); }); From 1329b683de4fb63382449f9115ed9f0c959b3408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Fri, 24 Jul 2020 07:50:37 +0200 Subject: [PATCH 15/96] =?UTF-8?q?[Composable=20template]=C2=A0Preview=20co?= =?UTF-8?q?mposite=20template=20(#72598)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jean-Louis Leysens Co-authored-by: Elastic Machine --- .../global_flyout/global_flyout.tsx | 166 +++++++++ .../global_flyout/index.ts | 20 ++ .../public/forms/form_wizard/form_wizard.tsx | 25 +- .../forms/form_wizard/form_wizard_nav.tsx | 6 + .../forms/multi_content/use_multi_content.ts | 4 +- .../public/global_flyout/index.ts | 23 ++ src/plugins/es_ui_shared/public/index.ts | 3 +- .../static/forms/helpers/serializers.ts | 6 +- .../helpers/http_requests.ts | 12 + .../helpers/setup_environment.tsx | 10 +- .../helpers/test_subjects.ts | 4 +- .../home/index_templates_tab.helpers.ts | 11 +- .../home/index_templates_tab.test.ts | 31 +- .../common/constants/index.ts | 2 + .../common/constants/ui_metric.ts | 2 + .../common/lib/template_serialization.ts | 2 +- .../component_template_details.test.ts | 7 +- .../component_template_details.helpers.ts | 6 +- .../helpers/setup_environment.tsx | 13 +- .../component_template_details.tsx | 20 +- .../component_template_details/index.ts | 6 +- .../component_template_list.tsx | 148 +++++--- .../component_templates_selector.tsx | 70 ++-- .../components/component_templates/index.ts | 5 +- .../component_templates/shared_imports.ts | 1 + .../public/application/components/index.ts | 1 + .../components/index_templates/index.ts | 7 + .../simulate_template/index.ts | 13 + .../simulate_template/simulate_template.tsx | 60 ++++ .../simulate_template_flyout.tsx | 119 +++++++ .../datatypes/shape_datatype.test.tsx | 2 - .../datatypes/text_datatype.test.tsx | 2 - .../client_integration/edit_field.test.tsx | 2 - .../helpers/mappings_editor.helpers.tsx | 14 +- .../configuration_form/configuration_form.tsx | 6 +- .../configuration_form_schema.tsx | 3 +- .../document_fields/document_fields.tsx | 14 +- .../editor_toggle_controls.tsx | 2 +- .../field_parameters/name_parameter.tsx | 2 +- .../field_parameters/type_parameter.tsx | 8 +- .../fields/create_field/create_field.tsx | 2 +- .../fields/delete_field_provider.tsx | 2 +- .../fields/edit_field/edit_field.tsx | 326 +++++++++--------- .../edit_field/edit_field_container.tsx | 81 ++++- .../edit_field/update_field_provider.tsx | 147 -------- .../fields/edit_field/use_update_field.ts | 146 ++++++++ .../fields/fields_list_item_container.tsx | 2 +- .../document_fields/fields_json_editor.tsx | 2 +- .../document_fields/fields_tree_editor.tsx | 2 +- .../search_fields/search_result.tsx | 5 +- .../search_fields/search_result_item.tsx | 2 +- .../components/load_mappings/index.ts | 4 +- .../templates_form/templates_form.tsx | 5 +- .../templates_form/templates_form_schema.ts | 2 +- .../components/mappings_editor/index.ts | 8 +- .../index_settings_context.tsx | 1 + .../mappings_editor/mappings_editor.tsx | 136 ++++---- .../mappings_editor_context.tsx | 12 + .../mappings_state_context.tsx | 77 +++++ .../components/mappings_editor/reducer.ts | 98 +----- .../mappings_editor/shared_imports.ts | 1 + .../{types.ts => types/document_fields.ts} | 101 +----- .../components/mappings_editor/types/index.ts | 11 + .../mappings_editor/types/mappings_editor.ts | 110 ++++++ .../components/mappings_editor/types/state.ts | 107 ++++++ ...pings_state.tsx => use_state_listener.tsx} | 136 ++------ .../template_form/steps/step_components.tsx | 2 +- .../template_form/steps/step_logistics.tsx | 27 +- .../template_form/steps/step_review.tsx | 69 +++- .../template_form/template_form.tsx | 142 ++++++-- .../public/application/index.tsx | 15 +- .../template_details/tabs/index.ts | 1 + .../template_details/tabs/tab_preview.tsx | 34 ++ .../template_details/template_details.tsx | 2 - .../template_details_content.tsx | 20 +- .../template_clone/template_clone.tsx | 28 +- .../template_create/template_create.tsx | 35 +- .../sections/template_edit/template_edit.tsx | 26 +- .../public/application/services/api.ts | 12 + .../application/services/documentation.ts | 6 +- .../public/application/services/index.ts | 1 + .../index_management/public/shared_imports.ts | 1 + .../server/client/elasticsearch.ts | 10 + .../api/templates/register_simulate_route.ts | 42 +++ .../api/templates/register_template_routes.ts | 2 + 85 files changed, 1877 insertions(+), 982 deletions(-) create mode 100644 src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/global_flyout.tsx create mode 100644 src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/index.ts create mode 100644 src/plugins/es_ui_shared/public/global_flyout/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/index_templates/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/index_templates/simulate_template/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/index_templates/simulate_template/simulate_template.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/index_templates/simulate_template/simulate_template_flyout.tsx delete mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/use_update_field.ts create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor_context.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx rename x-pack/plugins/index_management/public/application/components/mappings_editor/{types.ts => types/document_fields.ts} (65%) create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/types/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/types/mappings_editor.ts create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts rename x-pack/plugins/index_management/public/application/components/mappings_editor/{mappings_state.tsx => use_state_listener.tsx} (53%) create mode 100644 x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_preview.tsx create mode 100644 x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/global_flyout.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/global_flyout.tsx new file mode 100644 index 0000000000000..aa575cd64944c --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/global_flyout.tsx @@ -0,0 +1,166 @@ +/* + * 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, { + createContext, + useContext, + useState, + useCallback, + useMemo, + useEffect, + useRef, +} from 'react'; +import { EuiFlyout } from '@elastic/eui'; + +interface Context { + addContent:

(content: Content

) => void; + removeContent: (contentId: string) => void; + closeFlyout: () => void; +} + +interface Content

{ + id: string; + Component: React.FunctionComponent

; + props?: P; + flyoutProps?: { [key: string]: any }; + cleanUpFunc?: () => void; +} + +const FlyoutMultiContentContext = createContext(undefined); + +const DEFAULT_FLYOUT_PROPS = { + 'data-test-subj': 'flyout', + size: 'm' as 'm', + maxWidth: 500, +}; + +export const GlobalFlyoutProvider: React.FC = ({ children }) => { + const [showFlyout, setShowFlyout] = useState(false); + const [activeContent, setActiveContent] = useState | undefined>(undefined); + + const { id, Component, props, flyoutProps } = activeContent ?? {}; + + const addContent: Context['addContent'] = useCallback((content) => { + setActiveContent((prev) => { + if (prev !== undefined) { + if (prev.id !== content.id && prev.cleanUpFunc) { + // Clean up anything from the content about to be removed + prev.cleanUpFunc(); + } + } + return content; + }); + + setShowFlyout(true); + }, []); + + const closeFlyout: Context['closeFlyout'] = useCallback(() => { + setActiveContent(undefined); + setShowFlyout(false); + }, []); + + const removeContent: Context['removeContent'] = useCallback( + (contentId: string) => { + if (contentId === id) { + closeFlyout(); + } + }, + [id, closeFlyout] + ); + + const mergedFlyoutProps = useMemo(() => { + return { + ...DEFAULT_FLYOUT_PROPS, + onClose: closeFlyout, + ...flyoutProps, + }; + }, [flyoutProps, closeFlyout]); + + const context: Context = { + addContent, + removeContent, + closeFlyout, + }; + + const ContentFlyout = showFlyout && Component !== undefined ? Component : null; + + return ( + + <> + {children} + {ContentFlyout && ( + + + + )} + + + ); +}; + +export const useGlobalFlyout = () => { + const ctx = useContext(FlyoutMultiContentContext); + + if (ctx === undefined) { + throw new Error('useGlobalFlyout must be used within a '); + } + + const isMounted = useRef(false); + /** + * A component can add one or multiple content to the flyout + * during its lifecycle. When it unmounts, we will remove + * all those content added to the flyout. + */ + const contents = useRef | undefined>(undefined); + const { removeContent, addContent: addContentToContext } = ctx; + + useEffect(() => { + isMounted.current = true; + + return () => { + isMounted.current = false; + }; + }, []); + + const getContents = useCallback(() => { + if (contents.current === undefined) { + contents.current = new Set(); + } + return contents.current; + }, []); + + const addContent: Context['addContent'] = useCallback( + (content) => { + getContents().add(content.id); + return addContentToContext(content); + }, + [getContents, addContentToContext] + ); + + useEffect(() => { + return () => { + if (!isMounted.current) { + // When the component unmounts, remove all the content it has added to the flyout + Array.from(getContents()).forEach(removeContent); + } + }; + }, [removeContent]); + + return { ...ctx, addContent }; +}; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/index.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/index.ts new file mode 100644 index 0000000000000..c49692547fb25 --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/index.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export { GlobalFlyoutProvider, useGlobalFlyout } from './global_flyout'; diff --git a/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard.tsx b/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard.tsx index cdb332e9e9130..642a21eae50e9 100644 --- a/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard.tsx +++ b/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard.tsx @@ -27,13 +27,14 @@ import { } from './form_wizard_context'; import { FormWizardNav, NavTexts } from './form_wizard_nav'; -interface Props extends ProviderProps { +interface Props extends ProviderProps { isSaving?: boolean; apiError: JSX.Element | null; texts?: Partial; + rightContentNav?: JSX.Element | null | ((stepId: S) => JSX.Element | null); } -export function FormWizard({ +export function FormWizard({ texts, defaultActiveStep, defaultValue, @@ -43,7 +44,8 @@ export function FormWizard({ onSave, onChange, children, -}: Props) { + rightContentNav, +}: Props) { return ( defaultValue={defaultValue} @@ -53,7 +55,14 @@ export function FormWizard({ defaultActiveStep={defaultActiveStep} > - {({ activeStepIndex, lastStep, steps, isCurrentStepValid, navigateToStep }) => { + {({ + activeStepIndex, + lastStep, + steps, + isCurrentStepValid, + navigateToStep, + activeStepId, + }) => { const stepsRequiredArray = Object.values(steps).map( (step) => Boolean(step.isRequired) && step.isComplete === false ); @@ -95,6 +104,13 @@ export function FormWizard({ }; }); + const getRightContentNav = () => { + if (typeof rightContentNav === 'function') { + return rightContentNav(activeStepId); + } + return rightContentNav; + }; + const onBack = () => { const prevStep = activeStepIndex - 1; navigateToStep(prevStep); @@ -129,6 +145,7 @@ export function FormWizard({ onBack={onBack} onNext={onNext} texts={texts} + getRightContent={getRightContentNav} /> ); diff --git a/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_nav.tsx b/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_nav.tsx index 3e0e9cf897b5d..0af99e8bce35a 100644 --- a/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_nav.tsx +++ b/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_nav.tsx @@ -29,6 +29,7 @@ interface Props { isSaving?: boolean; isStepValid?: boolean; texts?: Partial; + getRightContent?: () => JSX.Element | null | undefined; } export interface NavTexts { @@ -53,6 +54,7 @@ export const FormWizardNav = ({ onBack, onNext, texts, + getRightContent, }: Props) => { const isLastStep = activeStepIndex === lastStep; const labels = { @@ -66,6 +68,8 @@ export const FormWizardNav = ({ : labels.save : labels.next; + const rightContent = getRightContent !== undefined ? getRightContent() : undefined; + return ( @@ -100,6 +104,8 @@ export const FormWizardNav = ({ + + {rightContent && {rightContent}} ); }; diff --git a/src/plugins/es_ui_shared/public/forms/multi_content/use_multi_content.ts b/src/plugins/es_ui_shared/public/forms/multi_content/use_multi_content.ts index 8d470f6454b0e..2e7c91a26e1fc 100644 --- a/src/plugins/es_ui_shared/public/forms/multi_content/use_multi_content.ts +++ b/src/plugins/es_ui_shared/public/forms/multi_content/use_multi_content.ts @@ -94,7 +94,7 @@ export function useMultiContent({ const activeContentData: Partial = {}; for (const [id, _content] of Object.entries(contents.current)) { - if (validation.contents[id as keyof T]) { + if (validation.contents[id as keyof T] !== false) { const contentData = (_content as Content).getData(); // Replace the getData() handler with the cached value @@ -161,7 +161,7 @@ export function useMultiContent({ ); /** - * Validate the multi-content active content(s) in the DOM + * Validate the content(s) currently in the DOM */ const validate = useCallback(async () => { if (Object.keys(contents.current).length === 0) { diff --git a/src/plugins/es_ui_shared/public/global_flyout/index.ts b/src/plugins/es_ui_shared/public/global_flyout/index.ts new file mode 100644 index 0000000000000..e876594337c1e --- /dev/null +++ b/src/plugins/es_ui_shared/public/global_flyout/index.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +export { + GlobalFlyoutProvider, + useGlobalFlyout, +} from '../../__packages_do_not_import__/global_flyout'; diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts index 98a305fe68f08..bdea5ccf5fe26 100644 --- a/src/plugins/es_ui_shared/public/index.ts +++ b/src/plugins/es_ui_shared/public/index.ts @@ -24,6 +24,7 @@ import * as Forms from './forms'; import * as Monaco from './monaco'; import * as ace from './ace'; +import * as GlobalFlyout from './global_flyout'; export { JsonEditor, OnJsonEditorUpdateHandler } from './components/json_editor'; @@ -65,7 +66,7 @@ export { useAuthorizationContext, } from './authorization'; -export { Monaco, Forms, ace }; +export { Monaco, Forms, ace, GlobalFlyout }; export { extractQueryParams } from './url'; diff --git a/src/plugins/es_ui_shared/static/forms/helpers/serializers.ts b/src/plugins/es_ui_shared/static/forms/helpers/serializers.ts index 98287f6bac35d..733a60f1f86ff 100644 --- a/src/plugins/es_ui_shared/static/forms/helpers/serializers.ts +++ b/src/plugins/es_ui_shared/static/forms/helpers/serializers.ts @@ -64,9 +64,13 @@ interface StripEmptyFieldsOptions { * @param options An optional configuration object. By default recursive it turned on. */ export const stripEmptyFields = ( - object: { [key: string]: any }, + object?: { [key: string]: any }, options?: StripEmptyFieldsOptions ): { [key: string]: any } => { + if (object === undefined) { + return {}; + } + const { types = ['string', 'object'], recursive = false } = options || {}; return Object.entries(object).reduce((acc, [key, value]) => { diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts index 907c749f8ec0b..12cf7ccac6c59 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts @@ -92,6 +92,17 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setSimulateTemplateResponse = (response?: HttpResponse, error?: any) => { + const status = error ? error.status || 400 : 200; + const body = error ? JSON.stringify(error.body) : JSON.stringify(response); + + server.respondWith('POST', `${API_BASE_PATH}/index_templates/simulate`, [ + status, + { 'Content-Type': 'application/json' }, + body, + ]); + }; + return { setLoadTemplatesResponse, setLoadIndicesResponse, @@ -102,6 +113,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { setLoadTemplateResponse, setCreateTemplateResponse, setUpdateTemplateResponse, + setSimulateTemplateResponse, }; }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx index ad445f75f047c..e40cdc026210d 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx @@ -14,6 +14,8 @@ import { notificationServiceMock, docLinksServiceMock, } from '../../../../../../src/core/public/mocks'; +import { GlobalFlyout } from '../../../../../../src/plugins/es_ui_shared/public'; + import { AppContextProvider } from '../../../public/application/app_context'; import { httpService } from '../../../public/application/services/http'; import { breadcrumbService } from '../../../public/application/services/breadcrumbs'; @@ -23,9 +25,11 @@ import { ExtensionsService } from '../../../public/services'; import { UiMetricService } from '../../../public/application/services/ui_metric'; import { setUiMetricService } from '../../../public/application/services/api'; import { setExtensionsService } from '../../../public/application/store/selectors'; +import { MappingsEditorProvider } from '../../../public/application/components'; import { init as initHttpRequests } from './http_requests'; const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); +const { GlobalFlyoutProvider } = GlobalFlyout; export const services = { extensionsService: new ExtensionsService(), @@ -62,7 +66,11 @@ export const WithAppDependencies = (Comp: any, overridingDependencies: any = {}) const mergedDependencies = merge({}, appDependencies, overridingDependencies); return ( - + + + + + ); }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts index 9889ebe16ba1e..ecedf819e6185 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts @@ -28,6 +28,7 @@ export type TestSubjects = | 'legacyTemplateTable' | 'manageTemplateButton' | 'mappingsTabContent' + | 'previewTabContent' | 'noAliasesCallout' | 'noMappingsCallout' | 'noSettingsCallout' @@ -48,4 +49,5 @@ export type TestSubjects = | 'templateList' | 'templatesTab' | 'templateTable' - | 'viewButton'; + | 'viewButton' + | 'simulateTemplatePreview'; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts index a397419053351..23b40f4cbd3d7 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts @@ -40,10 +40,15 @@ const createActions = (testBed: TestBed) => { /** * User Actions */ - const selectDetailsTab = (tab: 'summary' | 'settings' | 'mappings' | 'aliases') => { - const tabs = ['summary', 'settings', 'mappings', 'aliases']; + const selectDetailsTab = async ( + tab: 'summary' | 'settings' | 'mappings' | 'aliases' | 'preview' + ) => { + const tabs = ['summary', 'settings', 'mappings', 'aliases', 'preview']; - testBed.find('templateDetails.tab').at(tabs.indexOf(tab)).simulate('click'); + await act(async () => { + testBed.find('templateDetails.tab').at(tabs.indexOf(tab)).simulate('click'); + }); + testBed.component.update(); }; const clickReloadButton = () => { diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts index f7ebc0bcf632b..06f57896d4900 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts @@ -493,7 +493,7 @@ describe('Index Templates tab', () => { }); describe('tabs', () => { - test('should have 4 tabs', async () => { + test('should have 5 tabs', async () => { const template = fixtures.getTemplate({ name: `a${getRandomString()}`, indexPatterns: ['template1Pattern1*', 'template1Pattern2'], @@ -524,35 +524,48 @@ describe('Index Templates tab', () => { const { find, actions, exists } = testBed; httpRequestsMockHelpers.setLoadTemplateResponse(template); + httpRequestsMockHelpers.setSimulateTemplateResponse({ simulateTemplate: 'response' }); await actions.clickTemplateAt(0); - expect(find('templateDetails.tab').length).toBe(4); + expect(find('templateDetails.tab').length).toBe(5); expect(find('templateDetails.tab').map((t) => t.text())).toEqual([ 'Summary', 'Settings', 'Mappings', 'Aliases', + 'Preview', ]); // Summary tab should be initial active tab expect(exists('summaryTab')).toBe(true); // Navigate and verify all tabs - actions.selectDetailsTab('settings'); + await actions.selectDetailsTab('settings'); expect(exists('summaryTab')).toBe(false); expect(exists('settingsTabContent')).toBe(true); - actions.selectDetailsTab('aliases'); + await actions.selectDetailsTab('aliases'); expect(exists('summaryTab')).toBe(false); expect(exists('settingsTabContent')).toBe(false); expect(exists('aliasesTabContent')).toBe(true); - actions.selectDetailsTab('mappings'); + await actions.selectDetailsTab('mappings'); expect(exists('summaryTab')).toBe(false); expect(exists('settingsTabContent')).toBe(false); expect(exists('aliasesTabContent')).toBe(false); expect(exists('mappingsTabContent')).toBe(true); + + await actions.selectDetailsTab('preview'); + expect(exists('summaryTab')).toBe(false); + expect(exists('settingsTabContent')).toBe(false); + expect(exists('aliasesTabContent')).toBe(false); + expect(exists('mappingsTabContent')).toBe(false); + expect(exists('previewTabContent')).toBe(true); + + expect(find('simulateTemplatePreview').text().replace(/\s/g, '')).toEqual( + JSON.stringify({ simulateTemplate: 'response' }) + ); }); test('should show an info callout if data is not present', async () => { @@ -568,17 +581,17 @@ describe('Index Templates tab', () => { await actions.clickTemplateAt(0); - expect(find('templateDetails.tab').length).toBe(4); + expect(find('templateDetails.tab').length).toBe(5); expect(exists('summaryTab')).toBe(true); // Navigate and verify callout message per tab - actions.selectDetailsTab('settings'); + await actions.selectDetailsTab('settings'); expect(exists('noSettingsCallout')).toBe(true); - actions.selectDetailsTab('mappings'); + await actions.selectDetailsTab('mappings'); expect(exists('noMappingsCallout')).toBe(true); - actions.selectDetailsTab('aliases'); + await actions.selectDetailsTab('aliases'); expect(exists('noAliasesCallout')).toBe(true); }); }); diff --git a/x-pack/plugins/index_management/common/constants/index.ts b/x-pack/plugins/index_management/common/constants/index.ts index d1700f0e611c0..11240271503e2 100644 --- a/x-pack/plugins/index_management/common/constants/index.ts +++ b/x-pack/plugins/index_management/common/constants/index.ts @@ -47,7 +47,9 @@ export { UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, + UIM_TEMPLATE_DETAIL_PANEL_PREVIEW_TAB, UIM_TEMPLATE_CREATE, UIM_TEMPLATE_UPDATE, UIM_TEMPLATE_CLONE, + UIM_TEMPLATE_SIMULATE, } from './ui_metric'; diff --git a/x-pack/plugins/index_management/common/constants/ui_metric.ts b/x-pack/plugins/index_management/common/constants/ui_metric.ts index 5fda812c704d1..545555b92f352 100644 --- a/x-pack/plugins/index_management/common/constants/ui_metric.ts +++ b/x-pack/plugins/index_management/common/constants/ui_metric.ts @@ -41,6 +41,8 @@ export const UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB = 'template_details_summary_t export const UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB = 'template_details_settings_tab'; export const UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB = 'template_details_mappings_tab'; export const UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB = 'template_details_aliases_tab'; +export const UIM_TEMPLATE_DETAIL_PANEL_PREVIEW_TAB = 'template_details_preview_tab'; export const UIM_TEMPLATE_CREATE = 'template_create'; export const UIM_TEMPLATE_UPDATE = 'template_update'; export const UIM_TEMPLATE_CLONE = 'template_clone'; +export const UIM_TEMPLATE_SIMULATE = 'template_simulate'; diff --git a/x-pack/plugins/index_management/common/lib/template_serialization.ts b/x-pack/plugins/index_management/common/lib/template_serialization.ts index 069d6ac29fbca..1803d89a40016 100644 --- a/x-pack/plugins/index_management/common/lib/template_serialization.ts +++ b/x-pack/plugins/index_management/common/lib/template_serialization.ts @@ -109,7 +109,7 @@ export function serializeLegacyTemplate(template: TemplateDeserialized): LegacyT version, order, indexPatterns, - template: { settings, aliases, mappings }, + template: { settings, aliases, mappings } = {}, } = template; return { diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts index 3d496d68cc66e..a112d73230b82 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts @@ -61,11 +61,10 @@ describe('', () => { const { exists, find, actions, component } = testBed; // Verify flyout exists with correct title - expect(exists('componentTemplateDetails')).toBe(true); - expect(find('componentTemplateDetails.title').text()).toBe(COMPONENT_TEMPLATE.name); + expect(find('title').text()).toBe(COMPONENT_TEMPLATE.name); // Verify footer does not display since "actions" prop was not provided - expect(exists('componentTemplateDetails.footer')).toBe(false); + expect(exists('footer')).toBe(false); // Verify tabs exist expect(exists('settingsTab')).toBe(true); @@ -185,7 +184,7 @@ describe('', () => { const { exists, actions, component, find } = testBed; // Verify footer exists - expect(exists('componentTemplateDetails.footer')).toBe(true); + expect(exists('footer')).toBe(true); expect(exists('manageComponentTemplateButton')).toBe(true); // Click manage button and verify actions diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_details.helpers.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_details.helpers.ts index 25c2d654fd900..fe81e8dcfe123 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_details.helpers.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_details.helpers.ts @@ -6,7 +6,7 @@ import { registerTestBed, TestBed } from '../../../../../../../../../test_utils'; import { WithAppDependencies } from './setup_environment'; -import { ComponentTemplateDetailsFlyout } from '../../../component_template_details'; +import { ComponentTemplateDetailsFlyoutContent } from '../../../component_template_details'; export type ComponentTemplateDetailsTestBed = TestBed & { actions: ReturnType; @@ -44,7 +44,7 @@ const createActions = (testBed: TestBed) = export const setup = (props: any): ComponentTemplateDetailsTestBed => { const setupTestBed = registerTestBed( - WithAppDependencies(ComponentTemplateDetailsFlyout), + WithAppDependencies(ComponentTemplateDetailsFlyoutContent), { memoryRouter: { wrapComponent: false, @@ -65,6 +65,8 @@ export type ComponentTemplateDetailsTestSubjects = | 'componentTemplateDetails' | 'componentTemplateDetails.title' | 'componentTemplateDetails.footer' + | 'title' + | 'footer' | 'summaryTab' | 'mappingsTab' | 'settingsTab' diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx index 7e460d3855cb0..2f7317e3e656b 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx @@ -15,12 +15,15 @@ import { applicationServiceMock, } from '../../../../../../../../../../src/core/public/mocks'; +import { GlobalFlyout } from '../../../../../../../../../../src/plugins/es_ui_shared/public'; +import { MappingsEditorProvider } from '../../../../mappings_editor'; import { ComponentTemplatesProvider } from '../../../component_templates_context'; import { init as initHttpRequests } from './http_requests'; import { API_BASE_PATH } from './constants'; const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); +const { GlobalFlyoutProvider } = GlobalFlyout; const appDependencies = { httpClient: (mockHttpClient as unknown) as HttpSetup, @@ -42,7 +45,11 @@ export const setupEnvironment = () => { }; export const WithAppDependencies = (Comp: any) => (props: any) => ( - - - + + + + + + + ); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx index 60f1fff3cc9de..0f5bc64c358b9 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx @@ -8,7 +8,6 @@ import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { - EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody, @@ -28,14 +27,19 @@ import { ComponentTemplateTabs, TabType } from './tabs'; import { ManageButton, ManageAction } from './manage_button'; import { attemptToDecodeURI } from '../lib'; -interface Props { +export interface Props { componentTemplateName: string; onClose: () => void; actions?: ManageAction[]; showSummaryCallToAction?: boolean; } -export const ComponentTemplateDetailsFlyout: React.FunctionComponent = ({ +export const defaultFlyoutProps = { + 'data-test-subj': 'componentTemplateDetails', + 'aria-labelledby': 'componentTemplateDetailsFlyoutTitle', +}; + +export const ComponentTemplateDetailsFlyoutContent: React.FunctionComponent = ({ componentTemplateName, onClose, actions, @@ -109,13 +113,7 @@ export const ComponentTemplateDetailsFlyout: React.FunctionComponent = ({ } return ( - + <> @@ -172,6 +170,6 @@ export const ComponentTemplateDetailsFlyout: React.FunctionComponent = ({ )} - + ); }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/index.ts index 11aac200a2f14..8687a1f5b89c0 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/index.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/index.ts @@ -4,4 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export { ComponentTemplateDetailsFlyout } from './component_template_details'; +export { + ComponentTemplateDetailsFlyoutContent, + defaultFlyoutProps, + Props as ComponentTemplateDetailsProps, +} from './component_template_details'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx index efc8b649ef872..8ba7409a9ac57 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx @@ -4,18 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { ScopedHistory } from 'kibana/public'; import { EuiLink, EuiText, EuiSpacer } from '@elastic/eui'; -import { SectionLoading, ComponentTemplateDeserialized } from '../shared_imports'; +import { SectionLoading, ComponentTemplateDeserialized, GlobalFlyout } from '../shared_imports'; import { UIM_COMPONENT_TEMPLATE_LIST_LOAD } from '../constants'; import { attemptToDecodeURI } from '../lib'; import { useComponentTemplatesContext } from '../component_templates_context'; -import { ComponentTemplateDetailsFlyout } from '../component_template_details'; +import { + ComponentTemplateDetailsFlyoutContent, + defaultFlyoutProps, + ComponentTemplateDetailsProps, +} from '../component_template_details'; import { EmptyPrompt } from './empty_prompt'; import { ComponentTable } from './table'; import { LoadError } from './error'; @@ -26,39 +30,112 @@ interface Props { history: RouteComponentProps['history']; } +const { useGlobalFlyout } = GlobalFlyout; + export const ComponentTemplateList: React.FunctionComponent = ({ componentTemplateName, history, }) => { + const { + addContent: addContentToGlobalFlyout, + removeContent: removeContentFromGlobalFlyout, + } = useGlobalFlyout(); const { api, trackMetric, documentation } = useComponentTemplatesContext(); const { data, isLoading, error, sendRequest } = api.useLoadComponentTemplates(); const [componentTemplatesToDelete, setComponentTemplatesToDelete] = useState([]); - const goToComponentTemplateList = () => { + const goToComponentTemplateList = useCallback(() => { return history.push({ pathname: 'component_templates', }); - }; - - const goToEditComponentTemplate = (name: string) => { - return history.push({ - pathname: encodeURI(`edit_component_template/${encodeURIComponent(name)}`), - }); - }; + }, [history]); + + const goToEditComponentTemplate = useCallback( + (name: string) => { + return history.push({ + pathname: encodeURI(`edit_component_template/${encodeURIComponent(name)}`), + }); + }, + [history] + ); - const goToCloneComponentTemplate = (name: string) => { - return history.push({ - pathname: encodeURI(`create_component_template/${encodeURIComponent(name)}`), - }); - }; + const goToCloneComponentTemplate = useCallback( + (name: string) => { + return history.push({ + pathname: encodeURI(`create_component_template/${encodeURIComponent(name)}`), + }); + }, + [history] + ); // Track component loaded useEffect(() => { trackMetric('loaded', UIM_COMPONENT_TEMPLATE_LIST_LOAD); }, [trackMetric]); + useEffect(() => { + if (componentTemplateName) { + const actions = [ + { + name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.editButtonLabel', { + defaultMessage: 'Edit', + }), + icon: 'pencil', + handleActionClick: () => + goToEditComponentTemplate(attemptToDecodeURI(componentTemplateName)), + }, + { + name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.cloneActionLabel', { + defaultMessage: 'Clone', + }), + icon: 'copy', + handleActionClick: () => + goToCloneComponentTemplate(attemptToDecodeURI(componentTemplateName)), + }, + { + name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.deleteButtonLabel', { + defaultMessage: 'Delete', + }), + icon: 'trash', + getIsDisabled: (details: ComponentTemplateDeserialized) => + details._kbnMeta.usedBy.length > 0, + closePopoverOnClick: true, + handleActionClick: () => { + setComponentTemplatesToDelete([attemptToDecodeURI(componentTemplateName)]); + }, + }, + ]; + + // Open the flyout with the Component Template Details content + addContentToGlobalFlyout({ + id: 'componentTemplateDetails', + Component: ComponentTemplateDetailsFlyoutContent, + props: { + onClose: goToComponentTemplateList, + componentTemplateName, + showSummaryCallToAction: true, + actions, + }, + flyoutProps: { ...defaultFlyoutProps, onClose: goToComponentTemplateList }, + }); + } + }, [ + componentTemplateName, + goToComponentTemplateList, + goToEditComponentTemplate, + goToCloneComponentTemplate, + addContentToGlobalFlyout, + history, + ]); + + useEffect(() => { + if (!componentTemplateName) { + removeContentFromGlobalFlyout('componentTemplateDetails'); + } + }, [componentTemplateName, removeContentFromGlobalFlyout]); + let content: React.ReactNode; if (isLoading) { @@ -126,45 +203,6 @@ export const ComponentTemplateList: React.FunctionComponent = ({ componentTemplatesToDelete={componentTemplatesToDelete} /> ) : null} - - {/* details flyout */} - {componentTemplateName && ( - - goToEditComponentTemplate(attemptToDecodeURI(componentTemplateName)), - }, - { - name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.cloneActionLabel', { - defaultMessage: 'Clone', - }), - icon: 'copy', - handleActionClick: () => - goToCloneComponentTemplate(attemptToDecodeURI(componentTemplateName)), - }, - { - name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.deleteButtonLabel', { - defaultMessage: 'Delete', - }), - icon: 'trash', - getIsDisabled: (details: ComponentTemplateDeserialized) => - details._kbnMeta.usedBy.length > 0, - closePopoverOnClick: true, - handleActionClick: () => { - setComponentTemplatesToDelete([attemptToDecodeURI(componentTemplateName)]); - }, - }, - ]} - /> - )} ); }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx index 8795c08fd2bee..ed570579d4e45 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx @@ -11,8 +11,12 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { ComponentTemplateListItem } from '../../../../../common'; -import { SectionError, SectionLoading } from '../shared_imports'; -import { ComponentTemplateDetailsFlyout } from '../component_template_details'; +import { SectionError, SectionLoading, GlobalFlyout } from '../shared_imports'; +import { + ComponentTemplateDetailsFlyoutContent, + defaultFlyoutProps, + ComponentTemplateDetailsProps, +} from '../component_template_details'; import { CreateButtonPopOver } from './components'; import { ComponentTemplates } from './component_templates'; import { ComponentTemplatesSelection } from './component_templates_selection'; @@ -20,10 +24,12 @@ import { useApi } from '../component_templates_context'; import './component_templates_selector.scss'; +const { useGlobalFlyout } = GlobalFlyout; + interface Props { onChange: (components: string[]) => void; onComponentsLoaded: (components: ComponentTemplateListItem[]) => void; - defaultValue: string[]; + defaultValue?: string[]; docUri: string; emptyPrompt?: { text?: string | JSX.Element; @@ -53,6 +59,10 @@ export const ComponentTemplatesSelector = ({ emptyPrompt: { text, showCreateButton } = {}, }: Props) => { const { data: components, isLoading, error } = useApi().useLoadComponentTemplates(); + const { + addContent: addContentToGlobalFlyout, + removeContent: removeContentFromGlobalFlyout, + } = useGlobalFlyout(); const [selectedComponent, setSelectedComponent] = useState(null); const [componentsSelected, setComponentsSelected] = useState([]); const isInitialized = useRef(false); @@ -60,15 +70,20 @@ export const ComponentTemplatesSelector = ({ const hasSelection = Object.keys(componentsSelected).length > 0; const hasComponents = components && components.length > 0 ? true : false; + const closeComponentTemplateDetails = () => { + setSelectedComponent(null); + }; + useEffect(() => { if (components) { if ( + defaultValue && defaultValue.length > 0 && componentsSelected.length === 0 && isInitialized.current === false ) { - // Once the components are loaded we check the ones selected - // from the defaultValue provided + // Once the components are fetched, we check the ones previously selected + // from the prop "defaultValue" passed. const nextComponentsSelected = defaultValue .map((name) => components.find((comp) => comp.name === name)) .filter(Boolean) as ComponentTemplateListItem[]; @@ -88,6 +103,30 @@ export const ComponentTemplatesSelector = ({ } }, [isLoading, error, components, onComponentsLoaded]); + useEffect(() => { + if (selectedComponent) { + // Open the flyout with the Component Template Details content + addContentToGlobalFlyout({ + id: 'componentTemplateDetails', + Component: ComponentTemplateDetailsFlyoutContent, + props: { + onClose: closeComponentTemplateDetails, + componentTemplateName: selectedComponent, + }, + flyoutProps: { ...defaultFlyoutProps, onClose: closeComponentTemplateDetails }, + cleanUpFunc: () => { + setSelectedComponent(null); + }, + }); + } + }, [selectedComponent, addContentToGlobalFlyout]); + + useEffect(() => { + if (!selectedComponent) { + removeContentFromGlobalFlyout('componentTemplateDetails'); + } + }, [selectedComponent, removeContentFromGlobalFlyout]); + const onSelectionReorder = (reorderedComponents: ComponentTemplateListItem[]) => { setComponentsSelected(reorderedComponents); }; @@ -198,30 +237,12 @@ export const ComponentTemplatesSelector = ({ ); - const renderComponentDetails = () => { - if (!selectedComponent) { - return null; - } - - return ( - setSelectedComponent(null)} - componentTemplateName={selectedComponent} - /> - ); - }; - if (isLoading) { return renderLoading(); } else if (error) { return renderError(); } else if (hasComponents) { - return ( - <> - {renderSelector()} - {renderComponentDetails()} - - ); + return renderSelector(); } // No components: render empty prompt @@ -244,6 +265,7 @@ export const ComponentTemplatesSelector = ({

); + return ( { + const [templatePreview, setTemplatePreview] = useState('{}'); + + const updatePreview = useCallback(async () => { + if (!template || Object.keys(template).length === 0) { + return; + } + + const indexTemplate = serializeTemplate(stripEmptyFields(template) as TemplateDeserialized); + + // Until ES fixes a bug on their side we will send a random index pattern to the simulate API. + // Issue: https://github.com/elastic/elasticsearch/issues/59152 + indexTemplate.index_patterns = [uuid.v4()]; + + const { data, error } = await simulateIndexTemplate(indexTemplate); + + if (data) { + // "Overlapping" info is only useful when simulating against an index + // which we don't do here. + delete data.overlapping; + } + + setTemplatePreview(JSON.stringify(data ?? error, null, 2)); + }, [template]); + + useEffect(() => { + updatePreview(); + }, [updatePreview]); + + return templatePreview === '{}' ? null : ( + + {templatePreview} + + ); +}); diff --git a/x-pack/plugins/index_management/public/application/components/index_templates/simulate_template/simulate_template_flyout.tsx b/x-pack/plugins/index_management/public/application/components/index_templates/simulate_template/simulate_template_flyout.tsx new file mode 100644 index 0000000000000..63bfe78546041 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/index_templates/simulate_template/simulate_template_flyout.tsx @@ -0,0 +1,119 @@ +/* + * 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, { useState, useCallback, useEffect, useRef } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiButtonEmpty, + EuiTextColor, + EuiText, + EuiSpacer, +} from '@elastic/eui'; + +import { SimulateTemplate } from './simulate_template'; + +export interface Props { + onClose(): void; + getTemplate: () => { [key: string]: any }; +} + +export const defaultFlyoutProps = { + 'data-test-subj': 'simulateTemplateFlyout', + 'aria-labelledby': 'simulateTemplateFlyoutTitle', +}; + +export const SimulateTemplateFlyoutContent = ({ onClose, getTemplate }: Props) => { + const isMounted = useRef(false); + const [heightCodeBlock, setHeightCodeBlock] = useState(0); + const [template, setTemplate] = useState<{ [key: string]: any }>({}); + + useEffect(() => { + setHeightCodeBlock( + document.getElementsByClassName('euiFlyoutBody__overflow')[0].clientHeight - 96 + ); + }, []); + + const updatePreview = useCallback(async () => { + const indexTemplate = await getTemplate(); + setTemplate(indexTemplate); + }, [getTemplate]); + + useEffect(() => { + if (isMounted.current === false) { + updatePreview(); + } + isMounted.current = true; + }, [updatePreview]); + + return ( + <> + + +

+ +

+
+ + + +

+ +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/shape_datatype.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/shape_datatype.test.tsx index 311cb37d0b47a..64347d19e9b47 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/shape_datatype.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/shape_datatype.test.tsx @@ -36,8 +36,6 @@ describe('Mappings editor: shape datatype', () => { test('initial view and default parameters values', async () => { const defaultMappings = { - _meta: {}, - _source: {}, properties: { myField: { type: 'shape', diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/text_datatype.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/text_datatype.test.tsx index ed60414d198f1..c03aa4805d27f 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/text_datatype.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/text_datatype.test.tsx @@ -47,8 +47,6 @@ describe.skip('Mappings editor: text datatype', () => { test('initial view and default parameters values', async () => { const defaultMappings = { - _meta: {}, - _source: {}, properties: { myField: { type: 'text', diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/edit_field.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/edit_field.test.tsx index 4f9d8a960a1a2..c146c7704911f 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/edit_field.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/edit_field.test.tsx @@ -65,8 +65,6 @@ describe('Mappings editor: edit field', () => { test('should update form parameters when changing the field datatype', async () => { const defaultMappings = { - _meta: {}, - _source: {}, properties: { userName: { ...defaultTextParameters, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx index 638bbfd925ffb..a6558b28a1273 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx @@ -7,9 +7,11 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { ReactWrapper } from 'enzyme'; +import { GlobalFlyout } from '../../../../../../../../../../src/plugins/es_ui_shared/public'; import { registerTestBed, TestBed } from '../../../../../../../../../test_utils'; import { getChildFieldsName } from '../../../lib'; import { MappingsEditor } from '../../../mappings_editor'; +import { MappingsEditorProvider } from '../../../mappings_editor_context'; jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); @@ -51,6 +53,8 @@ jest.mock('@elastic/eui', () => { }; }); +const { GlobalFlyoutProvider } = GlobalFlyout; + export interface DomFields { [key: string]: { type: string; @@ -247,7 +251,15 @@ const createActions = (testBed: TestBed) => { }; export const setup = (props: any = { onUpdate() {} }): MappingsEditorTestBed => { - const setupTestBed = registerTestBed(MappingsEditor, { + const ComponentToTest = (propsOverride: { [key: string]: any }) => ( + + + + + + ); + + const setupTestBed = registerTestBed(ComponentToTest, { memoryRouter: { wrapComponent: false, }, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx index 86bcc796a88eb..20b2e11855029 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx @@ -7,16 +7,14 @@ import React, { useEffect, useRef } from 'react'; import { EuiSpacer } from '@elastic/eui'; import { useForm, Form, SerializerFunc } from '../../shared_imports'; -import { GenericObject } from '../../types'; -import { Types, useDispatch } from '../../mappings_state'; +import { GenericObject, MappingsConfiguration } from '../../types'; +import { useDispatch } from '../../mappings_state_context'; import { DynamicMappingSection } from './dynamic_mapping_section'; import { SourceFieldSection } from './source_field_section'; import { MetaFieldSection } from './meta_field_section'; import { RoutingSection } from './routing_section'; import { configurationFormSchema } from './configuration_form_schema'; -type MappingsConfiguration = Types['MappingsConfiguration']; - interface Props { value?: MappingsConfiguration; } diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx index 6e80f8b813ec2..8742dfc916924 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx @@ -11,8 +11,7 @@ import { EuiLink, EuiCode } from '@elastic/eui'; import { documentationService } from '../../../../services/documentation'; import { FormSchema, FIELD_TYPES, VALIDATION_TYPES, fieldValidators } from '../../shared_imports'; -import { MappingsConfiguration } from '../../reducer'; -import { ComboBoxOption } from '../../types'; +import { ComboBoxOption, MappingsConfiguration } from '../../types'; const { containsCharsField, isJsonField } = fieldValidators; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/document_fields.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/document_fields.tsx index 400de4052afa4..4b19b6f7ae5c3 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/document_fields.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/document_fields.tsx @@ -6,7 +6,7 @@ import React, { useMemo, useCallback } from 'react'; import { EuiSpacer } from '@elastic/eui'; -import { useMappingsState, useDispatch } from '../../mappings_state'; +import { useMappingsState, useDispatch } from '../../mappings_state_context'; import { deNormalize } from '../../lib'; import { EditFieldContainer } from './fields'; import { DocumentFieldsHeader } from './document_fields_header'; @@ -18,7 +18,7 @@ export const DocumentFields = React.memo(() => { const { fields, search, documentFields } = useMappingsState(); const dispatch = useDispatch(); - const { status, fieldToEdit, editor: editorType } = documentFields; + const { editor: editorType } = documentFields; const jsonEditorDefaultValue = useMemo(() => { if (editorType === 'json') { @@ -33,14 +33,6 @@ export const DocumentFields = React.memo(() => { ); - const renderEditField = () => { - if (status !== 'editingField') { - return null; - } - const field = fields.byId[fieldToEdit!]; - return ; - }; - const onSearchChange = useCallback( (value: string) => { dispatch({ type: 'search:update', value }); @@ -59,7 +51,7 @@ export const DocumentFields = React.memo(() => { ) : ( editor )} - {renderEditField()} + ); }); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/editor_toggle_controls.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/editor_toggle_controls.tsx index 51f9ca63be403..ad283a3fe47bd 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/editor_toggle_controls.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/editor_toggle_controls.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiButton, EuiText } from '@elastic/eui'; -import { useDispatch, useMappingsState } from '../../mappings_state'; +import { useDispatch, useMappingsState } from '../../mappings_state_context'; import { FieldsEditor } from '../../types'; import { canUseMappingsEditor, normalize } from '../../lib'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx index 01cca7e249a23..0320f2ff51da3 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { TextField, UseField, FieldConfig } from '../../../shared_imports'; import { validateUniqueName } from '../../../lib'; import { PARAMETERS_DEFINITION } from '../../../constants'; -import { useMappingsState } from '../../../mappings_state'; +import { useMappingsState } from '../../../mappings_state_context'; export const NameParameter = () => { const { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/type_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/type_parameter.tsx index 46e70bf8e56ba..31ae37c82a43e 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/type_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/type_parameter.tsx @@ -70,7 +70,13 @@ export const TypeParameter = ({ isMultiField, isRootLevelField, showDocLink = fa : filterTypesForNonRootFields(FIELD_TYPES_OPTIONS) } selectedOptions={typeField.value} - onChange={typeField.setValue} + onChange={(value) => { + if (value.length === 0) { + // Don't allow clearing the type. One must always be selected + return; + } + typeField.setValue(value); + }} isClearable={false} data-test-subj="fieldType" /> diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx index 57a765c38dd26..dc631b7dbf32d 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx @@ -18,7 +18,7 @@ import { import { useForm, Form, FormDataProvider } from '../../../../shared_imports'; import { EUI_SIZE } from '../../../../constants'; -import { useDispatch } from '../../../../mappings_state'; +import { useDispatch } from '../../../../mappings_state_context'; import { fieldSerializer } from '../../../../lib'; import { Field, NormalizedFields } from '../../../../types'; import { NameParameter, TypeParameter, SubTypeParameter } from '../../field_parameters'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx index 80e3e9bec605a..2a98b5948e5a9 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { useMappingsState, useDispatch } from '../../../mappings_state'; +import { useMappingsState, useDispatch } from '../../../mappings_state_context'; import { NormalizedField } from '../../../types'; import { getAllDescendantAliases } from '../../../lib'; import { ModalConfirmationDeleteFields } from './modal_confirmation_delete_fields'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx index e8e41955a5e80..e6950ccfe253e 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx @@ -6,7 +6,6 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { - EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody, EuiFlyoutFooter, @@ -25,7 +24,7 @@ import { TYPE_DEFINITION } from '../../../../constants'; import { Field, NormalizedField, NormalizedFields, MainType, SubType } from '../../../../types'; import { CodeBlock } from '../../../code_block'; import { getParametersFormForType } from '../field_types'; -import { UpdateFieldProvider, UpdateFieldFunc } from './update_field_provider'; +import { UpdateFieldFunc } from './use_update_field'; import { EditFieldHeaderForm } from './edit_field_header_form'; const limitStringLength = (text: string, limit = 18): string => { @@ -36,19 +35,28 @@ const limitStringLength = (text: string, limit = 18): string => { return `...${text.substr(limit * -1)}`; }; -interface Props { +export interface Props { form: FormHook; field: NormalizedField; allFields: NormalizedFields['byId']; exitEdit(): void; + updateField: UpdateFieldFunc; } -export const EditField = React.memo(({ form, field, allFields, exitEdit }: Props) => { - const getSubmitForm = (updateField: UpdateFieldFunc) => async (e?: React.FormEvent) => { - if (e) { - e.preventDefault(); - } +export const defaultFlyoutProps = { + 'data-test-subj': 'mappingsEditorFieldEdit', + 'aria-labelledby': 'mappingsEditorFieldEditTitle', + className: 'mappingsEditor__editField', + maxWidth: 720, +}; + +// The default FormWrapper is the , which wrapps the form with +// a
. We can't have a div as first child of the Flyout as it breaks +// the height calculaction and does not render the footer position correctly. +const FormWrapper: React.FC = ({ children }) => <>{children}; +export const EditField = React.memo(({ form, field, allFields, exitEdit, updateField }: Props) => { + const submitForm = async () => { const { isValid, data } = await form.submit(); if (isValid) { @@ -56,174 +64,152 @@ export const EditField = React.memo(({ form, field, allFields, exitEdit }: Props } }; - const cancel = () => { - exitEdit(); - }; - const { isMultiField } = field; return ( - - {(updateField) => ( -
- - - - - {/* We need an extra div to get out of flex grow */} -
- {/* Title */} - -

- {isMultiField - ? i18n.translate('xpack.idxMgmt.mappingsEditor.editMultiFieldTitle', { - defaultMessage: "Edit multi-field '{fieldName}'", - values: { - fieldName: limitStringLength(field.source.name), - }, - }) - : i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldTitle', { - defaultMessage: "Edit field '{fieldName}'", - values: { - fieldName: limitStringLength(field.source.name), - }, - })} -

-
-
-
- - {/* Documentation link */} - - {({ type, subType }) => { - const linkDocumentation = - documentationService.getTypeDocLink(subType) || - documentationService.getTypeDocLink(type); - - if (!linkDocumentation) { - return null; - } - - const typeDefinition = TYPE_DEFINITION[type as MainType]; - const subTypeDefinition = TYPE_DEFINITION[subType as SubType]; - - return ( - - - {i18n.translate( - 'xpack.idxMgmt.mappingsEditor.editField.typeDocumentation', - { - defaultMessage: '{type} documentation', - values: { - type: subTypeDefinition - ? subTypeDefinition.label - : typeDefinition.label, - }, - } - )} - - - ); - }} - -
- - {/* Field path */} - - - {field.path.join(' > ')} - - -
- - - - - - {({ type, subType }) => { - const ParametersForm = getParametersFormForType(type, subType); - - if (!ParametersForm) { - return null; - } - - return ( - - ); - }} - - - - - {form.isSubmitted && !form.isValid && ( - <> - - - - )} - - + + + + + {/* We need an extra div to get out of flex grow */} +
+ {/* Title */} + +

+ {isMultiField + ? i18n.translate('xpack.idxMgmt.mappingsEditor.editMultiFieldTitle', { + defaultMessage: "Edit multi-field '{fieldName}'", + values: { + fieldName: limitStringLength(field.source.name), + }, + }) + : i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldTitle', { + defaultMessage: "Edit field '{fieldName}'", + values: { + fieldName: limitStringLength(field.source.name), + }, + })} +

+
+
+
+ + {/* Documentation link */} + + {({ type, subType }) => { + const linkDocumentation = + documentationService.getTypeDocLink(subType) || + documentationService.getTypeDocLink(type); + + if (!linkDocumentation) { + return null; + } + + const typeDefinition = TYPE_DEFINITION[type as MainType]; + const subTypeDefinition = TYPE_DEFINITION[subType as SubType]; + + return ( - - {i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldCancelButtonLabel', { - defaultMessage: 'Cancel', - })} - - - - - {i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldUpdateButtonLabel', { - defaultMessage: 'Update', + {i18n.translate('xpack.idxMgmt.mappingsEditor.editField.typeDocumentation', { + defaultMessage: '{type} documentation', + values: { + type: subTypeDefinition ? subTypeDefinition.label : typeDefinition.label, + }, })} - + -
-
-
-
- )} -
+ ); + }} + + + + {/* Field path */} + + + {field.path.join(' > ')} + + + + + + + + + {({ type, subType }) => { + const ParametersForm = getParametersFormForType(type, subType); + + if (!ParametersForm) { + return null; + } + + return ( + + ); + }} + + + + + {form.isSubmitted && !form.isValid && ( + <> + + + + )} + + + + + {i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldCancelButtonLabel', { + defaultMessage: 'Cancel', + })} + + + + + {i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldUpdateButtonLabel', { + defaultMessage: 'Update', + })} + + + + + ); }); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_container.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_container.tsx index 5105a2a157a6d..4996f59105c04 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_container.tsx @@ -3,24 +3,38 @@ * 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, useCallback } from 'react'; +import React, { useEffect, useCallback, useMemo } from 'react'; -import { useForm } from '../../../../shared_imports'; -import { useDispatch } from '../../../../mappings_state'; -import { Field, NormalizedField, NormalizedFields } from '../../../../types'; +import { useForm, GlobalFlyout } from '../../../../shared_imports'; +import { useDispatch, useMappingsState } from '../../../../mappings_state_context'; +import { Field } from '../../../../types'; import { fieldSerializer, fieldDeserializer } from '../../../../lib'; -import { EditField } from './edit_field'; +import { ModalConfirmationDeleteFields } from '../modal_confirmation_delete_fields'; +import { EditField, defaultFlyoutProps, Props as EditFieldProps } from './edit_field'; +import { useUpdateField } from './use_update_field'; -interface Props { - field: NormalizedField; - allFields: NormalizedFields['byId']; -} +const { useGlobalFlyout } = GlobalFlyout; -export const EditFieldContainer = React.memo(({ field, allFields }: Props) => { +export const EditFieldContainer = React.memo(() => { + const { fields, documentFields } = useMappingsState(); const dispatch = useDispatch(); + const { + addContent: addContentToGlobalFlyout, + removeContent: removeContentFromGlobalFlyout, + } = useGlobalFlyout(); + const { updateField, modal } = useUpdateField(); + + const { status, fieldToEdit } = documentFields; + const isEditing = status === 'editingField'; + + const field = isEditing ? fields.byId[fieldToEdit!] : undefined; + + const formDefaultValue = useMemo(() => { + return { ...field?.source }; + }, [field?.source]); const { form } = useForm({ - defaultValue: { ...field.source }, + defaultValue: formDefaultValue, serializer: fieldSerializer, deserializer: fieldDeserializer, options: { stripEmptyFields: false }, @@ -40,5 +54,48 @@ export const EditFieldContainer = React.memo(({ field, allFields }: Props) => { dispatch({ type: 'documentField.changeStatus', value: 'idle' }); }, [dispatch]); - return ; + useEffect(() => { + if (isEditing) { + // Open the flyout with the content + addContentToGlobalFlyout({ + id: 'mappingsEditField', + Component: EditField, + props: { + form, + field: field!, + exitEdit, + allFields: fields.byId, + updateField, + }, + flyoutProps: { ...defaultFlyoutProps, onClose: exitEdit }, + cleanUpFunc: exitEdit, + }); + } + }, [ + isEditing, + field, + form, + addContentToGlobalFlyout, + fields.byId, + fieldToEdit, + exitEdit, + updateField, + ]); + + useEffect(() => { + if (!isEditing) { + removeContentFromGlobalFlyout('mappingsEditField'); + } + }, [isEditing, removeContentFromGlobalFlyout]); + + useEffect(() => { + return () => { + if (isEditing) { + // When the component unmounts, exit edit mode. + exitEdit(); + } + }; + }, [isEditing, exitEdit]); + + return modal.isOpen ? : null; }); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx deleted file mode 100644 index e31d12689e7e0..0000000000000 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useState } from 'react'; -import { i18n } from '@kbn/i18n'; - -import { useMappingsState, useDispatch } from '../../../../mappings_state'; -import { shouldDeleteChildFieldsAfterTypeChange, getAllDescendantAliases } from '../../../../lib'; -import { NormalizedField, DataType } from '../../../../types'; -import { PARAMETERS_DEFINITION } from '../../../../constants'; -import { ModalConfirmationDeleteFields } from '../modal_confirmation_delete_fields'; - -export type UpdateFieldFunc = (field: NormalizedField) => void; - -interface Props { - children: (saveProperty: UpdateFieldFunc) => React.ReactNode; -} - -interface State { - isModalOpen: boolean; - field?: NormalizedField; - aliases?: string[]; -} - -export const UpdateFieldProvider = ({ children }: Props) => { - const [state, setState] = useState({ - isModalOpen: false, - }); - const dispatch = useDispatch(); - - const { fields } = useMappingsState(); - const { byId, aliases } = fields; - - const confirmButtonText = i18n.translate( - 'xpack.idxMgmt.mappingsEditor.updateField.confirmationModal.confirmDescription', - { - defaultMessage: 'Confirm type change', - } - ); - - let modalTitle: string | undefined; - - if (state.field) { - const { source } = state.field; - - modalTitle = i18n.translate( - 'xpack.idxMgmt.mappingsEditor.updateField.confirmationModal.title', - { - defaultMessage: "Confirm change '{fieldName}' type to '{fieldType}'.", - values: { - fieldName: source.name, - fieldType: source.type, - }, - } - ); - } - - const closeModal = () => { - setState({ isModalOpen: false }); - }; - - const updateField: UpdateFieldFunc = (field) => { - const previousField = byId[field.id]; - - const willDeleteChildFields = (oldType: DataType, newType: DataType): boolean => { - const { hasChildFields, hasMultiFields } = field; - - if (!hasChildFields && !hasMultiFields) { - // No child or multi-fields will be deleted, no confirmation needed. - return false; - } - - return shouldDeleteChildFieldsAfterTypeChange(oldType, newType); - }; - - if (field.source.type !== previousField.source.type) { - // Array of all the aliases pointing to the current field beeing updated - const aliasesOnField = aliases[field.id] || []; - - // Array of all the aliases pointing to the current field + all its possible children - const aliasesOnFieldAndDescendants = getAllDescendantAliases(field, fields); - - const isReferencedByAlias = aliasesOnField && Boolean(aliasesOnField.length); - const nextTypeCanHaveAlias = !PARAMETERS_DEFINITION.path.targetTypesNotAllowed.includes( - field.source.type - ); - - // We need to check if, by changing the type, we will also - // delete possible child properties ("fields" or "properties"). - // If we will, we need to warn the user about it. - let requiresConfirmation: boolean; - let aliasesToDelete: string[] = []; - - if (isReferencedByAlias && !nextTypeCanHaveAlias) { - aliasesToDelete = aliasesOnFieldAndDescendants; - requiresConfirmation = true; - } else { - requiresConfirmation = willDeleteChildFields(previousField.source.type, field.source.type); - if (requiresConfirmation) { - aliasesToDelete = aliasesOnFieldAndDescendants.filter( - // We will only delete aliases that points to possible children, *NOT* the field itself - (id) => aliasesOnField.includes(id) === false - ); - } - } - - if (requiresConfirmation) { - setState({ - isModalOpen: true, - field, - aliases: Boolean(aliasesToDelete.length) - ? aliasesToDelete.map((id) => byId[id].path.join(' > ')).sort() - : undefined, - }); - return; - } - } - - dispatch({ type: 'field.edit', value: field.source }); - }; - - const confirmTypeUpdate = () => { - dispatch({ type: 'field.edit', value: state.field!.source }); - closeModal(); - }; - - return ( - <> - {children(updateField)} - - {state.isModalOpen && ( - - )} - - ); -}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/use_update_field.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/use_update_field.ts new file mode 100644 index 0000000000000..ed659cd05b060 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/use_update_field.ts @@ -0,0 +1,146 @@ +/* + * 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 { useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { useMappingsState, useDispatch } from '../../../../mappings_state_context'; +import { shouldDeleteChildFieldsAfterTypeChange, getAllDescendantAliases } from '../../../../lib'; +import { NormalizedField, DataType } from '../../../../types'; +import { PARAMETERS_DEFINITION } from '../../../../constants'; + +export type UpdateFieldFunc = (field: NormalizedField) => void; + +interface State { + isModalOpen: boolean; + field?: NormalizedField; + aliases?: string[]; +} + +export const useUpdateField = () => { + const [state, setState] = useState({ + isModalOpen: false, + }); + const dispatch = useDispatch(); + + const { fields } = useMappingsState(); + const { byId, aliases } = fields; + + const confirmButtonText = i18n.translate( + 'xpack.idxMgmt.mappingsEditor.updateField.confirmationModal.confirmDescription', + { + defaultMessage: 'Confirm type change', + } + ); + + let modalTitle = ''; + + if (state.field) { + const { source } = state.field; + + modalTitle = i18n.translate( + 'xpack.idxMgmt.mappingsEditor.updateField.confirmationModal.title', + { + defaultMessage: "Confirm change '{fieldName}' type to '{fieldType}'.", + values: { + fieldName: source.name, + fieldType: source.type, + }, + } + ); + } + + const closeModal = () => { + setState({ isModalOpen: false }); + }; + + const updateField: UpdateFieldFunc = useCallback( + (field) => { + const previousField = byId[field.id]; + + const willDeleteChildFields = (oldType: DataType, newType: DataType): boolean => { + const { hasChildFields, hasMultiFields } = field; + + if (!hasChildFields && !hasMultiFields) { + // No child or multi-fields will be deleted, no confirmation needed. + return false; + } + + return shouldDeleteChildFieldsAfterTypeChange(oldType, newType); + }; + + if (field.source.type !== previousField.source.type) { + // Array of all the aliases pointing to the current field beeing updated + const aliasesOnField = aliases[field.id] || []; + + // Array of all the aliases pointing to the current field + all its possible children + const aliasesOnFieldAndDescendants = getAllDescendantAliases(field, fields); + + const isReferencedByAlias = aliasesOnField && Boolean(aliasesOnField.length); + const nextTypeCanHaveAlias = !PARAMETERS_DEFINITION.path.targetTypesNotAllowed.includes( + field.source.type + ); + + // We need to check if, by changing the type, we will also + // delete possible child properties ("fields" or "properties"). + // If we will, we need to warn the user about it. + let requiresConfirmation: boolean; + let aliasesToDelete: string[] = []; + + if (isReferencedByAlias && !nextTypeCanHaveAlias) { + aliasesToDelete = aliasesOnFieldAndDescendants; + requiresConfirmation = true; + } else { + requiresConfirmation = willDeleteChildFields( + previousField.source.type, + field.source.type + ); + if (requiresConfirmation) { + aliasesToDelete = aliasesOnFieldAndDescendants.filter( + // We will only delete aliases that points to possible children, *NOT* the field itself + (id) => aliasesOnField.includes(id) === false + ); + } + } + + if (requiresConfirmation) { + setState({ + isModalOpen: true, + field, + aliases: Boolean(aliasesToDelete.length) + ? aliasesToDelete.map((id) => byId[id].path.join(' > ')).sort() + : undefined, + }); + return; + } + } + + dispatch({ type: 'field.edit', value: field.source }); + }, + [dispatch, aliases, fields, byId] + ); + + const confirmTypeUpdate = () => { + dispatch({ type: 'field.edit', value: state.field!.source }); + closeModal(); + }; + + return { + updateField, + modal: { + isOpen: state.isModalOpen, + props: { + childFields: state.field && state.field.childFields, + title: modalTitle, + aliases: state.aliases, + byId, + confirmButtonText, + onConfirm: confirmTypeUpdate, + onCancel: closeModal, + }, + }, + }; +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx index 55093e606cfa1..7d9ad3bc6aaec 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx @@ -5,7 +5,7 @@ */ import React, { useMemo, useCallback, useRef } from 'react'; -import { useMappingsState, useDispatch } from '../../../mappings_state'; +import { useMappingsState, useDispatch } from '../../../mappings_state_context'; import { NormalizedField } from '../../../types'; import { FieldsListItem } from './fields_list_item'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields_json_editor.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields_json_editor.tsx index 5954f6f285f10..d750c0e604c5e 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields_json_editor.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields_json_editor.tsx @@ -6,7 +6,7 @@ import React, { useRef, useCallback } from 'react'; -import { useDispatch } from '../../mappings_state'; +import { useDispatch } from '../../mappings_state_context'; import { JsonEditor } from '../../shared_imports'; export interface Props { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields_tree_editor.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields_tree_editor.tsx index 9d9df38ef4e25..7a0b72ae647d5 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields_tree_editor.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields_tree_editor.tsx @@ -8,7 +8,7 @@ import React, { useMemo, useCallback } from 'react'; import { EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useMappingsState, useDispatch } from '../../mappings_state'; +import { useMappingsState, useDispatch } from '../../mappings_state_context'; import { FieldsList, CreateField } from './fields'; export const DocumentFieldsTreeEditor = () => { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result.tsx index 9077781b7fb43..f3602a800eeeb 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result.tsx @@ -8,9 +8,8 @@ import VirtualList from 'react-tiny-virtual-list'; import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SearchResult as SearchResultType } from '../../../types'; -import { useDispatch } from '../../../mappings_state'; -import { State } from '../../../reducer'; +import { SearchResult as SearchResultType, State } from '../../../types'; +import { useDispatch } from '../../../mappings_state_context'; import { SearchResultItem } from './search_result_item'; interface Props { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx index ab8b90b6be3b5..73d3e078f6ff3 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { SearchResult } from '../../../types'; import { TYPE_DEFINITION } from '../../../constants'; -import { useDispatch } from '../../../mappings_state'; +import { useDispatch } from '../../../mappings_state_context'; import { getTypeLabelFromType } from '../../../lib'; import { DeleteFieldProvider } from '../fields/delete_field_provider'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/index.ts index 34c410f06e520..dc7f20f4d026b 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/index.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './load_from_json_button'; -export * from './load_mappings_provider'; +export { LoadMappingsFromJsonButton } from './load_from_json_button'; +export { LoadMappingsProvider } from './load_mappings_provider'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx index a95579a8a141e..44a809a7a01bf 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx @@ -9,12 +9,11 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiText, EuiLink, EuiSpacer } from '@elastic/eui'; import { useForm, Form, SerializerFunc, UseField, JsonEditorField } from '../../shared_imports'; -import { Types, useDispatch } from '../../mappings_state'; +import { MappingsTemplates } from '../../types'; +import { useDispatch } from '../../mappings_state_context'; import { templatesFormSchema } from './templates_form_schema'; import { documentationService } from '../../../../services/documentation'; -type MappingsTemplates = Types['MappingsTemplates']; - interface Props { value?: MappingsTemplates; } diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form_schema.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form_schema.ts index 667b5685723d2..daca85f95b0b9 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form_schema.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form_schema.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { FormSchema, fieldValidators } from '../../shared_imports'; -import { MappingsTemplates } from '../../reducer'; +import { MappingsTemplates } from '../../types'; const { isJsonField } = fieldValidators; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/index.ts index 29cfaf99c6559..00bb41663dd9c 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/index.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './mappings_editor'; +export { MappingsEditor } from './mappings_editor'; // We export both the button & the load mappings provider // to give flexibility to the consumer -export * from './components/load_mappings'; +export { LoadMappingsFromJsonButton, LoadMappingsProvider } from './components/load_mappings'; -export { OnUpdateHandler, Types } from './mappings_state'; +export { MappingsEditorProvider } from './mappings_editor_context'; -export { IndexSettings } from './types'; +export { IndexSettings, OnUpdateHandler } from './types'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/index_settings_context.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/index_settings_context.tsx index 9e3637f970293..411193f10b24a 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/index_settings_context.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/index_settings_context.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { createContext, useContext } from 'react'; + import { IndexSettings } from './types'; const IndexSettingsContext = createContext(undefined); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx index e8fda90737708..292882f1c5b4b 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx @@ -14,24 +14,40 @@ import { TemplatesForm, MultipleMappingsWarning, } from './components'; -import { IndexSettings } from './types'; +import { + OnUpdateHandler, + IndexSettings, + Field, + Mappings, + MappingsConfiguration, + MappingsTemplates, +} from './types'; import { extractMappingsDefinition } from './lib'; -import { State } from './reducer'; -import { MappingsState, Props as MappingsStateProps, Types } from './mappings_state'; +import { useMappingsState } from './mappings_state_context'; +import { useMappingsStateListener } from './use_state_listener'; import { IndexSettingsProvider } from './index_settings_context'; +type TabName = 'fields' | 'advanced' | 'templates'; + +interface MappingsEditorParsedMetadata { + parsedDefaultValue?: { + configuration: MappingsConfiguration; + fields: { [key: string]: Field }; + templates: MappingsTemplates; + }; + multipleMappingsDeclared: boolean; +} + interface Props { - onChange: MappingsStateProps['onChange']; + onChange: OnUpdateHandler; value?: { [key: string]: any }; indexSettings?: IndexSettings; } -type TabName = 'fields' | 'advanced' | 'templates'; - export const MappingsEditor = React.memo(({ onChange, value, indexSettings }: Props) => { - const [selectedTab, selectTab] = useState('fields'); - - const { parsedDefaultValue, multipleMappingsDeclared } = useMemo(() => { + const { parsedDefaultValue, multipleMappingsDeclared } = useMemo< + MappingsEditorParsedMetadata + >(() => { const mappingsDefinition = extractMappingsDefinition(value); if (mappingsDefinition === null) { @@ -69,18 +85,28 @@ export const MappingsEditor = React.memo(({ onChange, value, indexSettings }: Pr return { parsedDefaultValue: parsed, multipleMappingsDeclared: false }; }, [value]); + /** + * Hook that will listen to: + * 1. "value" prop changes in order to reset the mappings editor + * 2. "state" changes in order to communicate any updates to the consumer + */ + useMappingsStateListener({ onChange, value: parsedDefaultValue }); + + const state = useMappingsState(); + const [selectedTab, selectTab] = useState('fields'); + useEffect(() => { if (multipleMappingsDeclared) { // We set the data getter here as the user won't be able to make any changes onChange({ - getData: () => value! as Types['Mappings'], + getData: () => value! as Mappings, validate: () => Promise.resolve(true), isValid: true, }); } }, [multipleMappingsDeclared, onChange, value]); - const changeTab = async (tab: TabName, state: State) => { + const changeTab = async (tab: TabName) => { if (selectedTab === 'advanced') { // When we navigate away we need to submit the form to validate if there are any errors. const { isValid: isConfigurationFormValid } = await state.configuration.submitForm!(); @@ -102,59 +128,53 @@ export const MappingsEditor = React.memo(({ onChange, value, indexSettings }: Pr selectTab(tab); }; + const tabToContentMap = { + fields: , + templates: , + advanced: , + }; + return (
{multipleMappingsDeclared ? ( ) : ( - - {({ state }) => { - const tabToContentMap = { - fields: , - templates: , - advanced: , - }; - - return ( -
- - changeTab('fields', state)} - isSelected={selectedTab === 'fields'} - data-test-subj="formTab" - > - {i18n.translate('xpack.idxMgmt.mappingsEditor.fieldsTabLabel', { - defaultMessage: 'Mapped fields', - })} - - changeTab('templates', state)} - isSelected={selectedTab === 'templates'} - data-test-subj="formTab" - > - {i18n.translate('xpack.idxMgmt.mappingsEditor.templatesTabLabel', { - defaultMessage: 'Dynamic templates', - })} - - changeTab('advanced', state)} - isSelected={selectedTab === 'advanced'} - data-test-subj="formTab" - > - {i18n.translate('xpack.idxMgmt.mappingsEditor.advancedTabLabel', { - defaultMessage: 'Advanced options', - })} - - - - - - {tabToContentMap[selectedTab]} -
- ); - }} -
+
+ + changeTab('fields')} + isSelected={selectedTab === 'fields'} + data-test-subj="formTab" + > + {i18n.translate('xpack.idxMgmt.mappingsEditor.fieldsTabLabel', { + defaultMessage: 'Mapped fields', + })} + + changeTab('templates')} + isSelected={selectedTab === 'templates'} + data-test-subj="formTab" + > + {i18n.translate('xpack.idxMgmt.mappingsEditor.templatesTabLabel', { + defaultMessage: 'Dynamic templates', + })} + + changeTab('advanced')} + isSelected={selectedTab === 'advanced'} + data-test-subj="formTab" + > + {i18n.translate('xpack.idxMgmt.mappingsEditor.advancedTabLabel', { + defaultMessage: 'Advanced options', + })} + + + + + + {tabToContentMap[selectedTab]} +
)}
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor_context.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor_context.tsx new file mode 100644 index 0000000000000..596b49cc89ee8 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor_context.tsx @@ -0,0 +1,12 @@ +/* + * 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 { StateProvider } from './mappings_state_context'; + +export const MappingsEditorProvider: React.FC = ({ children }) => { + return {children}; +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx new file mode 100644 index 0000000000000..a402dec250056 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx @@ -0,0 +1,77 @@ +/* + * 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, { useReducer, createContext, useContext } from 'react'; + +import { reducer } from './reducer'; +import { State, Dispatch } from './types'; + +const StateContext = createContext(undefined); +const DispatchContext = createContext(undefined); + +export const StateProvider: React.FC = ({ children }) => { + const initialState: State = { + isValid: true, + configuration: { + defaultValue: {}, + data: { + raw: {}, + format: () => ({}), + }, + validate: () => Promise.resolve(true), + }, + templates: { + defaultValue: {}, + data: { + raw: {}, + format: () => ({}), + }, + validate: () => Promise.resolve(true), + }, + fields: { + byId: {}, + rootLevelFields: [], + aliases: {}, + maxNestedDepth: 0, + }, + documentFields: { + status: 'idle', + editor: 'default', + }, + fieldsJsonEditor: { + format: () => ({}), + isValid: true, + }, + search: { + term: '', + result: [], + }, + }; + + const [state, dispatch] = useReducer(reducer, initialState); + + return ( + + {children} + + ); +}; + +export const useMappingsState = () => { + const ctx = useContext(StateContext); + if (ctx === undefined) { + throw new Error('useMappingsState must be used within a '); + } + return ctx; +}; + +export const useDispatch = () => { + const ctx = useContext(DispatchContext); + if (ctx === undefined) { + throw new Error('useDispatch must be used within a '); + } + return ctx; +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts index 27f8b12493008..18a8270117ea4 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts @@ -3,8 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { OnFormUpdateArg, FormHook } from './shared_imports'; -import { Field, NormalizedFields, NormalizedField, FieldsEditor, SearchResult } from './types'; +import { Field, NormalizedFields, NormalizedField, State, Action } from './types'; import { getFieldMeta, getUniqueId, @@ -17,99 +16,6 @@ import { } from './lib'; import { PARAMETERS_DEFINITION } from './constants'; -export interface MappingsConfiguration { - enabled?: boolean; - throwErrorsForUnmappedFields?: boolean; - date_detection: boolean; - numeric_detection: boolean; - dynamic_date_formats: string[]; - _source: { - enabled?: boolean; - includes?: string[]; - excludes?: string[]; - }; - _meta?: string; -} - -export interface MappingsTemplates { - dynamic_templates: DynamicTemplate[]; -} - -interface DynamicTemplate { - [key: string]: { - mapping: { - [key: string]: any; - }; - match_mapping_type?: string; - match?: string; - unmatch?: string; - match_pattern?: string; - path_match?: string; - path_unmatch?: string; - }; -} - -export interface MappingsFields { - [key: string]: any; -} - -type DocumentFieldsStatus = 'idle' | 'editingField' | 'creatingField'; - -interface DocumentFieldsState { - status: DocumentFieldsStatus; - editor: FieldsEditor; - fieldToEdit?: string; - fieldToAddFieldTo?: string; -} - -interface ConfigurationFormState extends OnFormUpdateArg { - defaultValue: MappingsConfiguration; - submitForm?: FormHook['submit']; -} - -interface TemplatesFormState extends OnFormUpdateArg { - defaultValue: MappingsTemplates; - submitForm?: FormHook['submit']; -} - -export interface State { - isValid: boolean | undefined; - configuration: ConfigurationFormState; - documentFields: DocumentFieldsState; - fields: NormalizedFields; - fieldForm?: OnFormUpdateArg; - fieldsJsonEditor: { - format(): MappingsFields; - isValid: boolean; - }; - search: { - term: string; - result: SearchResult[]; - }; - templates: TemplatesFormState; -} - -export type Action = - | { type: 'editor.replaceMappings'; value: { [key: string]: any } } - | { type: 'configuration.update'; value: Partial } - | { type: 'configuration.save'; value: MappingsConfiguration } - | { type: 'templates.update'; value: Partial } - | { type: 'templates.save'; value: MappingsTemplates } - | { type: 'fieldForm.update'; value: OnFormUpdateArg } - | { type: 'field.add'; value: Field } - | { type: 'field.remove'; value: string } - | { type: 'field.edit'; value: Field } - | { type: 'field.toggleExpand'; value: { fieldId: string; isExpanded?: boolean } } - | { type: 'documentField.createField'; value?: string } - | { type: 'documentField.editField'; value: string } - | { type: 'documentField.changeStatus'; value: DocumentFieldsStatus } - | { type: 'documentField.changeEditor'; value: FieldsEditor } - | { type: 'fieldsJsonEditor.update'; value: { json: { [key: string]: any }; isValid: boolean } } - | { type: 'search:update'; value: string } - | { type: 'validity:update'; value: boolean }; - -export type Dispatch = (action: Action) => void; - export const addFieldToState = (field: Field, state: State): State => { const updatedFields = { ...state.fields }; const id = getUniqueId(); @@ -277,7 +183,7 @@ export const reducer = (state: State, action: Action): State => { }, documentFields: { ...state.documentFields, - status: 'idle', + ...action.value.documentFields, fieldToAddFieldTo: undefined, fieldToEdit: undefined, }, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts index 2979015c07455..097d039527950 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts @@ -49,4 +49,5 @@ export { export { JsonEditor, OnJsonEditorUpdateHandler, + GlobalFlyout, } from '../../../../../../../src/plugins/es_ui_shared/public'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts similarity index 65% rename from x-pack/plugins/index_management/public/application/components/mappings_editor/types.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts index 5b18af68ed55b..a9f6d2ea03bdf 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/types.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts @@ -3,10 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ReactNode, OptionHTMLAttributes } from 'react'; +import { ReactNode } from 'react'; -import { FieldConfig } from './shared_imports'; -import { PARAMETERS_DEFINITION } from './constants'; +import { GenericObject } from './mappings_editor'; + +import { FieldConfig } from '../shared_imports'; +import { PARAMETERS_DEFINITION } from '../constants'; export interface DataTypeDefinition { label: string; @@ -203,100 +205,7 @@ export interface NormalizedField extends FieldMeta { export type ChildFieldName = 'properties' | 'fields'; -export type FieldsEditor = 'default' | 'json'; - -export type SelectOption = { - value: unknown; - text: T | ReactNode; -} & OptionHTMLAttributes; - -export interface SuperSelectOption { - value: unknown; - inputDisplay?: ReactNode; - dropdownDisplay?: ReactNode; - disabled?: boolean; - 'data-test-subj'?: string; -} - export interface AliasOption { id: string; label: string; } - -export interface IndexSettingsInterface { - analysis?: { - analyzer: { - [key: string]: { - type: string; - tokenizer: string; - char_filter?: string[]; - filter?: string[]; - position_increment_gap?: number; - }; - }; - }; -} - -/** - * When we define the index settings we can skip - * the "index" property and directly add the "analysis". - * ES always returns the settings wrapped under "index". - */ -export type IndexSettings = IndexSettingsInterface | { index: IndexSettingsInterface }; - -export interface ComboBoxOption { - label: string; - value?: unknown; -} - -export interface SearchResult { - display: JSX.Element; - field: NormalizedField; -} - -export interface SearchMetadata { - /** - * Whether or not the search term match some part of the field path. - */ - matchPath: boolean; - /** - * If the search term matches the field type we will give it a higher score. - */ - matchType: boolean; - /** - * If the last word of the search terms matches the field name - */ - matchFieldName: boolean; - /** - * If the search term matches the beginning of the path we will give it a higher score - */ - matchStartOfPath: boolean; - /** - * If the last word of the search terms fully matches the field name - */ - fullyMatchFieldName: boolean; - /** - * If the search term exactly matches the field type - */ - fullyMatchType: boolean; - /** - * If the search term matches the full field path - */ - fullyMatchPath: boolean; - /** - * The score of the result that will allow us to sort the list - */ - score: number; - /** - * The JSX with tag wrapping the matched string - */ - display: JSX.Element; - /** - * The field path substring that matches the search - */ - stringMatch: string | null; -} - -export interface GenericObject { - [key: string]: any; -} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/index.ts new file mode 100644 index 0000000000000..cce2d550a68c1 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/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. + */ + +export * from './mappings_editor'; + +export * from './document_fields'; + +export * from './state'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/mappings_editor.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/mappings_editor.ts new file mode 100644 index 0000000000000..1ca944024ae2b --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/mappings_editor.ts @@ -0,0 +1,110 @@ +/* + * 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 { ReactNode, OptionHTMLAttributes } from 'react'; + +import { NormalizedField } from './document_fields'; +import { Mappings } from './state'; + +export type OnUpdateHandler = (arg: OnUpdateHandlerArg) => void; + +export interface OnUpdateHandlerArg { + isValid?: boolean; + getData: () => Mappings | undefined; + validate: () => Promise; +} + +export type FieldsEditor = 'default' | 'json'; + +export interface IndexSettingsInterface { + analysis?: { + analyzer: { + [key: string]: { + type: string; + tokenizer: string; + char_filter?: string[]; + filter?: string[]; + position_increment_gap?: number; + }; + }; + }; +} + +/** + * When we define the index settings we can skip + * the "index" property and directly add the "analysis". + * ES always returns the settings wrapped under "index". + */ +export type IndexSettings = IndexSettingsInterface | { index: IndexSettingsInterface }; + +export type SelectOption = { + value: unknown; + text: T | ReactNode; +} & OptionHTMLAttributes; + +export interface ComboBoxOption { + label: string; + value?: unknown; +} + +export interface SuperSelectOption { + value: unknown; + inputDisplay?: ReactNode; + dropdownDisplay?: ReactNode; + disabled?: boolean; + 'data-test-subj'?: string; +} + +export interface SearchResult { + display: JSX.Element; + field: NormalizedField; +} + +export interface SearchMetadata { + /** + * Whether or not the search term match some part of the field path. + */ + matchPath: boolean; + /** + * If the search term matches the field type we will give it a higher score. + */ + matchType: boolean; + /** + * If the last word of the search terms matches the field name + */ + matchFieldName: boolean; + /** + * If the search term matches the beginning of the path we will give it a higher score + */ + matchStartOfPath: boolean; + /** + * If the last word of the search terms fully matches the field name + */ + fullyMatchFieldName: boolean; + /** + * If the search term exactly matches the field type + */ + fullyMatchType: boolean; + /** + * If the search term matches the full field path + */ + fullyMatchPath: boolean; + /** + * The score of the result that will allow us to sort the list + */ + score: number; + /** + * The JSX with tag wrapping the matched string + */ + display: JSX.Element; + /** + * The field path substring that matches the search + */ + stringMatch: string | null; +} + +export interface GenericObject { + [key: string]: any; +} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts new file mode 100644 index 0000000000000..34df70374aa88 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts @@ -0,0 +1,107 @@ +/* + * 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 { FormHook, OnFormUpdateArg } from '../shared_imports'; +import { Field, NormalizedFields } from './document_fields'; +import { FieldsEditor, SearchResult } from './mappings_editor'; + +export type Mappings = MappingsTemplates & + MappingsConfiguration & { + properties?: MappingsFields; + }; + +export interface MappingsConfiguration { + enabled?: boolean; + throwErrorsForUnmappedFields?: boolean; + date_detection?: boolean; + numeric_detection?: boolean; + dynamic_date_formats?: string[]; + _source?: { + enabled?: boolean; + includes?: string[]; + excludes?: string[]; + }; + _meta?: string; +} + +export interface MappingsTemplates { + dynamic_templates?: DynamicTemplate[]; +} + +export interface DynamicTemplate { + [key: string]: { + mapping: { + [key: string]: any; + }; + match_mapping_type?: string; + match?: string; + unmatch?: string; + match_pattern?: string; + path_match?: string; + path_unmatch?: string; + }; +} + +export interface MappingsFields { + [key: string]: any; +} + +export type DocumentFieldsStatus = 'idle' | 'editingField' | 'creatingField'; + +export interface DocumentFieldsState { + status: DocumentFieldsStatus; + editor: FieldsEditor; + fieldToEdit?: string; + fieldToAddFieldTo?: string; +} + +export interface ConfigurationFormState extends OnFormUpdateArg { + defaultValue: MappingsConfiguration; + submitForm?: FormHook['submit']; +} + +interface TemplatesFormState extends OnFormUpdateArg { + defaultValue: MappingsTemplates; + submitForm?: FormHook['submit']; +} + +export interface State { + isValid: boolean | undefined; + configuration: ConfigurationFormState; + documentFields: DocumentFieldsState; + fields: NormalizedFields; + fieldForm?: OnFormUpdateArg; + fieldsJsonEditor: { + format(): MappingsFields; + isValid: boolean; + }; + search: { + term: string; + result: SearchResult[]; + }; + templates: TemplatesFormState; +} + +export type Action = + | { type: 'editor.replaceMappings'; value: { [key: string]: any } } + | { type: 'configuration.update'; value: Partial } + | { type: 'configuration.save'; value: MappingsConfiguration } + | { type: 'templates.update'; value: Partial } + | { type: 'templates.save'; value: MappingsTemplates } + | { type: 'fieldForm.update'; value: OnFormUpdateArg } + | { type: 'field.add'; value: Field } + | { type: 'field.remove'; value: string } + | { type: 'field.edit'; value: Field } + | { type: 'field.toggleExpand'; value: { fieldId: string; isExpanded?: boolean } } + | { type: 'documentField.createField'; value?: string } + | { type: 'documentField.editField'; value: string } + | { type: 'documentField.changeStatus'; value: DocumentFieldsStatus } + | { type: 'documentField.changeEditor'; value: FieldsEditor } + | { type: 'fieldsJsonEditor.update'; value: { json: { [key: string]: any }; isValid: boolean } } + | { type: 'search:update'; value: string } + | { type: 'validity:update'; value: boolean }; + +export type Dispatch = (action: Action) => void; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx similarity index 53% rename from x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx index ad5056fa73ce1..f1ffd5356c977 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx @@ -3,92 +3,32 @@ * 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, { useReducer, useEffect, createContext, useContext, useMemo, useRef } from 'react'; +import { useEffect, useMemo } from 'react'; import { - reducer, + Field, + Mappings, MappingsConfiguration, - MappingsFields, MappingsTemplates, - State, - Dispatch, -} from './reducer'; -import { Field } from './types'; + OnUpdateHandler, +} from './types'; import { normalize, deNormalize, stripUndefinedValues } from './lib'; +import { useMappingsState, useDispatch } from './mappings_state_context'; -type Mappings = MappingsTemplates & - MappingsConfiguration & { - properties?: MappingsFields; - }; - -export interface Types { - Mappings: Mappings; - MappingsConfiguration: MappingsConfiguration; - MappingsFields: MappingsFields; - MappingsTemplates: MappingsTemplates; -} - -export interface OnUpdateHandlerArg { - isValid?: boolean; - getData: () => Mappings | undefined; - validate: () => Promise; -} - -export type OnUpdateHandler = (arg: OnUpdateHandlerArg) => void; - -const StateContext = createContext(undefined); -const DispatchContext = createContext(undefined); - -export interface Props { - children: (params: { state: State }) => React.ReactNode; - value: { +interface Args { + onChange: OnUpdateHandler; + value?: { templates: MappingsTemplates; configuration: MappingsConfiguration; fields: { [key: string]: Field }; }; - onChange: OnUpdateHandler; } -export const MappingsState = React.memo(({ children, onChange, value }: Props) => { - const didMountRef = useRef(false); +export const useMappingsStateListener = ({ onChange, value }: Args) => { + const state = useMappingsState(); + const dispatch = useDispatch(); - const parsedFieldsDefaultValue = useMemo(() => normalize(value.fields), [value.fields]); - - const initialState: State = { - isValid: true, - configuration: { - defaultValue: value.configuration, - data: { - raw: value.configuration, - format: () => value.configuration, - }, - validate: () => Promise.resolve(true), - }, - templates: { - defaultValue: value.templates, - data: { - raw: value.templates, - format: () => value.templates, - }, - validate: () => Promise.resolve(true), - }, - fields: parsedFieldsDefaultValue, - documentFields: { - status: parsedFieldsDefaultValue.rootLevelFields.length === 0 ? 'creatingField' : 'idle', - editor: 'default', - }, - fieldsJsonEditor: { - format: () => ({}), - isValid: true, - }, - search: { - term: '', - result: [], - }, - }; - - const [state, dispatch] = useReducer(reducer, initialState); + const parsedFieldsDefaultValue = useMemo(() => normalize(value?.fields), [value?.fields]); useEffect(() => { // If we are creating a new field, but haven't entered any name @@ -158,46 +98,28 @@ export const MappingsState = React.memo(({ children, onChange, value }: Props) = }, isValid: state.isValid, }); - }, [state, onChange]); + }, [state, onChange, dispatch]); useEffect(() => { /** * If the value has changed that probably means that we have loaded * new data from JSON. We need to update our state with the new mappings. */ - if (didMountRef.current) { - dispatch({ - type: 'editor.replaceMappings', - value: { - configuration: value.configuration, - templates: value.templates, - fields: parsedFieldsDefaultValue, - }, - }); - } else { - didMountRef.current = true; + if (value === undefined) { + return; } - }, [value, parsedFieldsDefaultValue]); - - return ( - - {children({ state })} - - ); -}); - -export const useMappingsState = () => { - const ctx = useContext(StateContext); - if (ctx === undefined) { - throw new Error('useMappingsState must be used within a '); - } - return ctx; -}; -export const useDispatch = () => { - const ctx = useContext(DispatchContext); - if (ctx === undefined) { - throw new Error('useDispatch must be used within a '); - } - return ctx; + dispatch({ + type: 'editor.replaceMappings', + value: { + configuration: value.configuration, + templates: value.templates, + fields: parsedFieldsDefaultValue, + documentFields: { + status: parsedFieldsDefaultValue.rootLevelFields.length === 0 ? 'creatingField' : 'idle', + editor: 'default', + }, + }, + }); + }, [value, parsedFieldsDefaultValue, dispatch]); }; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx index df0cc791384fe..ae831f4acf7ee 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx @@ -39,7 +39,7 @@ const i18nTexts = { ), }; -export const StepComponents = ({ defaultValue = [], onChange, esDocsBase }: Props) => { +export const StepComponents = ({ defaultValue, onChange, esDocsBase }: Props) => { const [state, setState] = useState<{ isLoadingComponents: boolean; components: ComponentTemplateListItem[]; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx index f3d05ac38108a..fcc9795617ebb 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect } from 'react'; +import React, { useEffect, useCallback } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -153,25 +153,18 @@ export const StepLogistics: React.FunctionComponent = React.memo( serializer: formSerializer, deserializer: formDeserializer, }); + const { subscribe, submit, isSubmitted, isValid: isFormValid, getErrors: getFormErrors } = form; /** * When the consumer call validate() on this step, we submit the form so it enters the "isSubmitted" state * and we can display the form errors on top of the forms if there are any. */ - const validate = async () => { - return (await form.submit()).isValid; - }; + const validate = useCallback(async () => { + return (await submit()).isValid; + }, [submit]); useEffect(() => { - onChange({ - isValid: form.isValid, - validate, - getData: form.getFormData, - }); - }, [form.isValid, onChange]); // eslint-disable-line react-hooks/exhaustive-deps - - useEffect(() => { - const subscription = form.subscribe(({ data, isValid }) => { + const subscription = subscribe(({ data, isValid }) => { onChange({ isValid, validate, @@ -179,7 +172,7 @@ export const StepLogistics: React.FunctionComponent = React.memo( }); }); return subscription.unsubscribe; - }, [onChange]); // eslint-disable-line react-hooks/exhaustive-deps + }, [onChange, validate, subscribe]); const { name, indexPatterns, dataStream, order, priority, version } = getFieldsMeta( documentationService.getEsDocsBase() @@ -204,7 +197,7 @@ export const StepLogistics: React.FunctionComponent = React.memo( @@ -220,8 +213,8 @@ export const StepLogistics: React.FunctionComponent = React.memo(
{/* Name */} diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx index 0f4b9de4f6cfa..1b4f19dda99f7 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx @@ -24,6 +24,7 @@ import { serializers } from '../../../../shared_imports'; import { serializeLegacyTemplate, serializeTemplate } from '../../../../../common/lib'; import { TemplateDeserialized, getTemplateParameter } from '../../../../../common'; +import { SimulateTemplate } from '../../index_templates'; import { WizardSection } from '../template_form'; const { stripEmptyFields } = serializers; @@ -56,6 +57,27 @@ interface Props { navigateToStep: (stepId: WizardSection) => void; } +const PreviewTab = ({ template }: { template: { [key: string]: any } }) => { + return ( +
+ + + +

+ +

+
+ + + + +
+ ); +}; + export const StepReview: React.FunctionComponent = React.memo( ({ template, navigateToStep }) => { const { @@ -286,6 +308,33 @@ export const StepReview: React.FunctionComponent = React.memo( ); }; + const tabs = [ + { + id: 'summary', + name: i18n.translate('xpack.idxMgmt.templateForm.stepReview.summaryTabTitle', { + defaultMessage: 'Summary', + }), + content: , + }, + { + id: 'request', + name: i18n.translate('xpack.idxMgmt.templateForm.stepReview.requestTabTitle', { + defaultMessage: 'Request', + }), + content: , + }, + ]; + + if (!isLegacy) { + tabs.splice(1, 0, { + id: 'preview', + name: i18n.translate('xpack.idxMgmt.templateForm.stepReview.previewTabTitle', { + defaultMessage: 'Preview', + }), + content: , + }); + } + return (
@@ -331,25 +380,7 @@ export const StepReview: React.FunctionComponent = React.memo( ) : null} - , - }, - { - id: 'request', - name: i18n.translate('xpack.idxMgmt.templateForm.stepReview.requestTabTitle', { - defaultMessage: 'Request', - }), - content: , - }, - ]} - /> +
); } diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx index f5c9be9292cd0..fb0ba0b68fa6c 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx @@ -3,14 +3,19 @@ * 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, { useCallback } from 'react'; +import React, { useState, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiSpacer } from '@elastic/eui'; +import { EuiSpacer, EuiButton } from '@elastic/eui'; import { TemplateDeserialized } from '../../../../common'; -import { serializers, Forms } from '../../../shared_imports'; +import { serializers, Forms, GlobalFlyout } from '../../../shared_imports'; import { SectionError } from '../section_error'; +import { + SimulateTemplateFlyoutContent, + SimulateTemplateProps, + simulateTemplateFlyoutProps, +} from '../index_templates'; import { StepLogisticsContainer, StepComponentContainer, StepReviewContainer } from './steps'; import { CommonWizardSteps, @@ -22,8 +27,10 @@ import { documentationService } from '../../services/documentation'; const { stripEmptyFields } = serializers; const { FormWizard, FormWizardStep } = Forms; +const { useGlobalFlyout } = GlobalFlyout; interface Props { + title: string | JSX.Element; onSave: (template: TemplateDeserialized) => void; clearSaveError: () => void; isSaving: boolean; @@ -80,6 +87,7 @@ const wizardSections: { [id: string]: { id: WizardSection; label: string } } = { }; export const TemplateForm = ({ + title, defaultValue, isEditing, isSaving, @@ -88,6 +96,9 @@ export const TemplateForm = ({ clearSaveError, onSave, }: Props) => { + const [wizardContent, setWizardContent] = useState | null>(null); + const { addContent: addContentToGlobalFlyout, closeFlyout } = useGlobalFlyout(); + const indexTemplate = defaultValue ?? { name: '', indexPatterns: [], @@ -189,6 +200,10 @@ export const TemplateForm = ({ [] ); + const onWizardContentChange = useCallback((content: Forms.Content) => { + setWizardContent(content); + }, []); + const onSaveTemplate = useCallback( async (wizardData: WizardContent) => { const template = buildTemplateObject(indexTemplate)(wizardData); @@ -206,44 +221,101 @@ export const TemplateForm = ({ [indexTemplate, buildTemplateObject, onSave, clearSaveError] ); + const getSimulateTemplate = useCallback(async () => { + if (!wizardContent) { + return; + } + const isValid = await wizardContent.validate(); + if (!isValid) { + return; + } + const wizardData = wizardContent.getData(); + const template = buildTemplateObject(indexTemplate)(wizardData); + return template; + }, [buildTemplateObject, indexTemplate, wizardContent]); + + const showPreviewFlyout = () => { + addContentToGlobalFlyout({ + id: 'simulateTemplate', + Component: SimulateTemplateFlyoutContent, + props: { + getTemplate: getSimulateTemplate, + onClose: closeFlyout, + }, + flyoutProps: simulateTemplateFlyoutProps, + }); + }; + + const getRightContentWizardNav = (stepId: WizardSection) => { + if (isLegacy) { + return null; + } + + // Don't show "Preview template" button on logistics and review steps + if (stepId === 'logistics' || stepId === 'review') { + return null; + } + + return ( + + + + ); + }; + return ( - - defaultValue={wizardDefaultValue} - onSave={onSaveTemplate} - isEditing={isEditing} - isSaving={isSaving} - apiError={apiError} - texts={i18nTexts} - > - - - + <> + {/* Form header */} + {title} - {indexTemplate._kbnMeta.isLegacy !== true && ( - - + + + + defaultValue={wizardDefaultValue} + onSave={onSaveTemplate} + isEditing={isEditing} + isSaving={isSaving} + apiError={apiError} + texts={i18nTexts} + onChange={onWizardContentChange} + rightContentNav={getRightContentWizardNav} + > + + - )} - - - + {indexTemplate._kbnMeta.isLegacy !== true && ( + + + + )} - - - + + + + + + + - - - + + + - - - - + + + + + ); }; diff --git a/x-pack/plugins/index_management/public/application/index.tsx b/x-pack/plugins/index_management/public/application/index.tsx index ebc29ac86a17f..f881c2e01cefc 100644 --- a/x-pack/plugins/index_management/public/application/index.tsx +++ b/x-pack/plugins/index_management/public/application/index.tsx @@ -11,11 +11,14 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { CoreStart } from '../../../../../src/core/public'; import { API_BASE_PATH } from '../../common'; +import { GlobalFlyout } from '../shared_imports'; import { AppContextProvider, AppDependencies } from './app_context'; import { App } from './app'; import { indexManagementStore } from './store'; -import { ComponentTemplatesProvider } from './components'; +import { ComponentTemplatesProvider, MappingsEditorProvider } from './components'; + +const { GlobalFlyoutProvider } = GlobalFlyout; export const renderApp = ( elem: HTMLElement | null, @@ -43,9 +46,13 @@ export const renderApp = ( - - - + + + + + + + , diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/index.ts b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/index.ts index 08ebda2b5e437..11a86e78be99c 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/index.ts +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/index.ts @@ -5,3 +5,4 @@ */ export { TabSummary } from './tab_summary'; +export { TabPreview } from './tab_preview'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_preview.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_preview.tsx new file mode 100644 index 0000000000000..ec52bcbab3b0b --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_preview.tsx @@ -0,0 +1,34 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { EuiText, EuiSpacer } from '@elastic/eui'; +import { TemplateDeserialized } from '../../../../../../../common'; +import { SimulateTemplate } from '../../../../../components/index_templates'; + +interface Props { + templateDetails: TemplateDeserialized; +} + +export const TabPreview = ({ templateDetails }: Props) => { + return ( +
+ +

+ +

+
+ + + + +
+ ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx index faeca2f2487a8..c03f64880a700 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx @@ -15,8 +15,6 @@ export const TemplateDetails = (props: Props) => { onClose={props.onClose} data-test-subj="templateDetails" aria-labelledby="templateDetailsFlyoutTitle" - size="m" - maxWidth={500} > diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx index 5b726013a1d92..5bacffc4c2404 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx @@ -29,6 +29,7 @@ import { UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, + UIM_TEMPLATE_DETAIL_PANEL_PREVIEW_TAB, } from '../../../../../../common/constants'; import { SendRequestResponse } from '../../../../../shared_imports'; import { TemplateDeleteModal, SectionLoading, SectionError, Error } from '../../../../components'; @@ -37,12 +38,13 @@ import { decodePathFromReactRouter } from '../../../../services/routing'; import { useServices } from '../../../../app_context'; import { TabAliases, TabMappings, TabSettings } from '../../../../components/shared'; import { TemplateTypeIndicator } from '../components'; -import { TabSummary } from './tabs'; +import { TabSummary, TabPreview } from './tabs'; const SUMMARY_TAB_ID = 'summary'; const MAPPINGS_TAB_ID = 'mappings'; const ALIASES_TAB_ID = 'aliases'; const SETTINGS_TAB_ID = 'settings'; +const PREVIEW_TAB_ID = 'preview'; const TABS = [ { @@ -69,6 +71,12 @@ const TABS = [ defaultMessage: 'Aliases', }), }, + { + id: PREVIEW_TAB_ID, + name: i18n.translate('xpack.idxMgmt.templateDetails.previewTabTitle', { + defaultMessage: 'Preview', + }), + }, ]; const tabToUiMetricMap: { [key: string]: string } = { @@ -76,6 +84,7 @@ const tabToUiMetricMap: { [key: string]: string } = { [SETTINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, [MAPPINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, [ALIASES_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, + [PREVIEW_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_PREVIEW_TAB, }; export interface Props { @@ -161,6 +170,7 @@ export const TemplateDetailsContent = ({ [SETTINGS_TAB_ID]: , [MAPPINGS_TAB_ID]: , [ALIASES_TAB_ID]: , + [PREVIEW_TAB_ID]: , }; const tabContent = tabToComponentMap[activeTab]; @@ -191,7 +201,13 @@ export const TemplateDetailsContent = ({ {managedTemplateCallout} - {TABS.map((tab) => ( + {TABS.filter((tab) => { + // Legacy index templates don't have the "simulate" template API + if (isLegacy && tab.id === PREVIEW_TAB_ID) { + return false; + } + return true; + }).map((tab) => ( { uiMetricService.trackMetric('click', tabToUiMetricMap[tab.id]); diff --git a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx index 82835c56a3877..2aaecbd64ee28 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx @@ -6,7 +6,7 @@ import React, { useEffect, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { EuiPageBody, EuiPageContent, EuiTitle } from '@elastic/eui'; import { TemplateDeserialized } from '../../../../common'; import { TemplateForm, SectionLoading, SectionError, Error } from '../../components'; @@ -94,30 +94,30 @@ export const TemplateClone: React.FunctionComponent +

+ +

+ + } defaultValue={templateData} onSave={onSave} isSaving={isSaving} saveError={saveError} clearSaveError={clearSaveError} + isLegacy={isLegacy} /> ); } return ( - - -

- -

-
- - {content} -
+ {content}
); }; diff --git a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx index fb82f52968eb4..691d2598d56d9 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx @@ -6,7 +6,7 @@ import React, { useEffect, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { EuiPageBody, EuiPageContent, EuiTitle } from '@elastic/eui'; import { useLocation } from 'react-router-dom'; import { parse } from 'query-string'; @@ -51,23 +51,24 @@ export const TemplateCreate: React.FunctionComponent = ({ h return ( - -

- {isLegacy ? ( - - ) : ( - - )} -

-
- +

+ {isLegacy ? ( + + ) : ( + + )} +

+ + } onSave={onSave} isSaving={isSaving} saveError={saveError} diff --git a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx index 29fd2e02120fc..6bdcd03fa5ca4 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx @@ -133,12 +133,24 @@ export const TemplateEdit: React.FunctionComponent )} +

+ +

+ + } defaultValue={template} onSave={onSave} isSaving={isSaving} saveError={saveError} clearSaveError={clearSaveError} isEditing={true} + isLegacy={isLegacy} /> ); @@ -147,19 +159,7 @@ export const TemplateEdit: React.FunctionComponent - - -

- -

-
- - {content} -
+ {content}
); }; diff --git a/x-pack/plugins/index_management/public/application/services/api.ts b/x-pack/plugins/index_management/public/application/services/api.ts index d7874ec2dcf32..546a0115ee4a9 100644 --- a/x-pack/plugins/index_management/public/application/services/api.ts +++ b/x-pack/plugins/index_management/public/application/services/api.ts @@ -30,6 +30,7 @@ import { UIM_TEMPLATE_CREATE, UIM_TEMPLATE_UPDATE, UIM_TEMPLATE_CLONE, + UIM_TEMPLATE_SIMULATE, } from '../../../common/constants'; import { TemplateDeserialized, TemplateListItem, DataStream } from '../../../common'; import { IndexMgmtMetricsType } from '../../types'; @@ -286,3 +287,14 @@ export async function updateTemplate(template: TemplateDeserialized) { return result; } + +export function simulateIndexTemplate(template: { [key: string]: any }) { + return sendRequest({ + path: `${API_BASE_PATH}/index_templates/simulate`, + method: 'post', + body: JSON.stringify(template), + }).then((result) => { + uiMetricService.trackMetric('count', UIM_TEMPLATE_SIMULATE); + return result; + }); +} diff --git a/x-pack/plugins/index_management/public/application/services/documentation.ts b/x-pack/plugins/index_management/public/application/services/documentation.ts index 972b4f4b25680..afc9c76f1afbe 100644 --- a/x-pack/plugins/index_management/public/application/services/documentation.ts +++ b/x-pack/plugins/index_management/public/application/services/documentation.ts @@ -40,8 +40,10 @@ class DocumentationService { return `${this.esDocsBase}/data-streams.html`; } - public getTemplatesDocumentationLink() { - return `${this.esDocsBase}/indices-templates.html`; + public getTemplatesDocumentationLink(isLegacy = false) { + return isLegacy + ? `${this.esDocsBase}/indices-templates-v1.html` + : `${this.esDocsBase}/indices-templates.html`; } public getIdxMgmtDocumentationLink() { diff --git a/x-pack/plugins/index_management/public/application/services/index.ts b/x-pack/plugins/index_management/public/application/services/index.ts index 2334d32adf131..a78e0bac14ae1 100644 --- a/x-pack/plugins/index_management/public/application/services/index.ts +++ b/x-pack/plugins/index_management/public/application/services/index.ts @@ -22,6 +22,7 @@ export { loadIndexMapping, loadIndexData, useLoadIndexTemplates, + simulateIndexTemplate, } from './api'; export { healthToColor } from './health_to_color'; export { sortTable } from './sort_table'; diff --git a/x-pack/plugins/index_management/public/shared_imports.ts b/x-pack/plugins/index_management/public/shared_imports.ts index 3f7fcf424f1f0..16dcab18c3caf 100644 --- a/x-pack/plugins/index_management/public/shared_imports.ts +++ b/x-pack/plugins/index_management/public/shared_imports.ts @@ -12,6 +12,7 @@ export { useRequest, Forms, extractQueryParams, + GlobalFlyout, } from '../../../../src/plugins/es_ui_shared/public/'; export { diff --git a/x-pack/plugins/index_management/server/client/elasticsearch.ts b/x-pack/plugins/index_management/server/client/elasticsearch.ts index 9f8bce241ae69..ed5ede07479ca 100644 --- a/x-pack/plugins/index_management/server/client/elasticsearch.ts +++ b/x-pack/plugins/index_management/server/client/elasticsearch.ts @@ -182,4 +182,14 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) ], method: 'HEAD', }); + + dataManagement.simulateTemplate = ca({ + urls: [ + { + fmt: '/_index_template/_simulate', + }, + ], + needBody: true, + method: 'POST', + }); }; diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts new file mode 100644 index 0000000000000..9d078e135fd52 --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../index'; + +const bodySchema = schema.object({}, { unknowns: 'allow' }); + +export function registerSimulateRoute({ router, license, lib }: RouteDependencies) { + router.post( + { + path: addBasePath('/index_templates/simulate'), + validate: { body: bodySchema }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.dataManagement!.client; + const template = req.body as TypeOf; + + try { + const templatePreview = await callAsCurrentUser('dataManagement.simulateTemplate', { + body: template, + }); + + return res.ok({ body: templatePreview }); + } catch (e) { + if (lib.isEsError(e)) { + return res.customError({ + statusCode: e.statusCode, + body: e, + }); + } + // Case: default + return res.internalError({ body: e }); + } + }) + ); +} diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_template_routes.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_template_routes.ts index 2b657346a2f82..e25f2abdfee78 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_template_routes.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_template_routes.ts @@ -10,6 +10,7 @@ import { registerGetAllRoute, registerGetOneRoute } from './register_get_routes' import { registerDeleteRoute } from './register_delete_route'; import { registerCreateRoute } from './register_create_route'; import { registerUpdateRoute } from './register_update_route'; +import { registerSimulateRoute } from './register_simulate_route'; export function registerTemplateRoutes(dependencies: RouteDependencies) { registerGetAllRoute(dependencies); @@ -17,4 +18,5 @@ export function registerTemplateRoutes(dependencies: RouteDependencies) { registerDeleteRoute(dependencies); registerCreateRoute(dependencies); registerUpdateRoute(dependencies); + registerSimulateRoute(dependencies); } From 3883e3e239c9a4595a4c2c49e18ba19538dbb7ac Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Fri, 24 Jul 2020 09:14:10 +0100 Subject: [PATCH 16/96] [ML] Fixing recognizer wizard create job button (#73025) * [ML] Fixing recognizer wizard create job button * updating translations --- .../components/job_settings_form.tsx | 20 ++++++------------- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_settings_form.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_settings_form.tsx index 63dec536ea487..e31c6bc7b59e0 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_settings_form.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_settings_form.tsx @@ -258,7 +258,7 @@ export const JobSettingsForm: FC = ({ fill type="submit" isLoading={saveState === SAVE_STATE.SAVING} - disabled={!validationResult.formValid} + disabled={!validationResult.formValid || saveState === SAVE_STATE.SAVING} onClick={() => { onSubmit(formState); }} @@ -266,19 +266,11 @@ export const JobSettingsForm: FC = ({ defaultMessage: 'Create job', })} > - {saveState === SAVE_STATE.NOT_SAVED && ( - - )} - {saveState === SAVE_STATE.SAVING && ( - - )} + diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 846330146cf07..cf789d1e7c450 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10239,7 +10239,6 @@ "xpack.ml.newJob.recognize.advancedLabel": "高度な設定", "xpack.ml.newJob.recognize.advancedSettingsAriaLabel": "高度な設定", "xpack.ml.newJob.recognize.alreadyExistsLabel": "(既に存在します)", - "xpack.ml.newJob.recognize.analysisRunningLabel": "分析を実行中", "xpack.ml.newJob.recognize.cancelJobOverrideLabel": "閉じる", "xpack.ml.newJob.recognize.createJobButtonAriaLabel": "ジョブを作成", "xpack.ml.newJob.recognize.createJobButtonLabel": "{numberOfJobs, plural, zero {Job} one {Job} other {Jobs}} を作成", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 477858d2e74d1..5b81804faf715 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10244,7 +10244,6 @@ "xpack.ml.newJob.recognize.advancedLabel": "高级", "xpack.ml.newJob.recognize.advancedSettingsAriaLabel": "高级设置", "xpack.ml.newJob.recognize.alreadyExistsLabel": "(已存在)", - "xpack.ml.newJob.recognize.analysisRunningLabel": "分析正在运行", "xpack.ml.newJob.recognize.cancelJobOverrideLabel": "关闭", "xpack.ml.newJob.recognize.createJobButtonAriaLabel": "创建作业", "xpack.ml.newJob.recognize.createJobButtonLabel": "创建{numberOfJobs, plural, zero {作业} one {Job} other {Jobs}}", From c0968f5726ec19ed731f1eee005a9513d4e10dbb Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Fri, 24 Jul 2020 09:15:23 +0100 Subject: [PATCH 17/96] [ML] Fixing unnecessary deleting job polling (#73087) --- .../components/jobs_list_view/jobs_list_view.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index a3b6cb39815a3..e9f3cb0d7d70d 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -63,9 +63,14 @@ export class JobsListView extends Component { this.showDeleteJobModal = () => {}; this.showStartDatafeedModal = () => {}; this.showCreateWatchFlyout = () => {}; + // work around to keep track of whether the component is mounted + // used to block timeouts for results polling + // which can run after unmounting + this._isMounted = false; } componentDidMount() { + this._isMounted = true; this.refreshJobSummaryList(true); if (this.props.isManagementTable !== true) { @@ -87,6 +92,7 @@ export class JobsListView extends Component { if (this.props.isManagementTable === undefined) { deletingJobsRefreshTimeout = null; } + this._isMounted = false; } openAutoStartDatafeedModal() { @@ -232,7 +238,7 @@ export class JobsListView extends Component { }; async refreshJobSummaryList(forceRefresh = false) { - if (forceRefresh === true || this.props.blockRefresh !== true) { + if (this._isMounted && (forceRefresh === true || this.props.blockRefresh !== true)) { // Set loading to true for jobs_list table for initial job loading if (this.state.loading === null) { this.setState({ loading: true }); @@ -283,6 +289,10 @@ export class JobsListView extends Component { } async checkDeletingJobTasks(forceRefresh = false) { + if (this._isMounted === false) { + return; + } + const { jobIds: taskJobIds } = await ml.jobs.deletingJobTasks(); const taskListHasChanged = From 5aa121152141d95505e458e9cd7985f2c6bd0c2c Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 24 Jul 2020 11:35:10 +0300 Subject: [PATCH 18/96] [Security Solution][Detections] Change detections breadcrumb title (#73059) --- .../detections/pages/detection_engine/rules/utils.test.ts | 2 +- .../detections/pages/detection_engine/rules/utils.ts | 2 +- .../detections/pages/detection_engine/translations.ts | 7 +++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.test.ts index 32f96b519acc5..1cbd1ee0f76ae 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.test.ts @@ -23,6 +23,6 @@ describe('getBreadcrumbs', () => { [], getUrlForAppMock ) - ).toEqual([{ href: 'securitySolution:detections', text: 'Detection alerts' }]); + ).toEqual([{ href: 'securitySolution:detections', text: 'Detections' }]); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts index 75d1df9406d25..c1b4fa3e2b7d9 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts @@ -57,7 +57,7 @@ export const getBreadcrumbs = ( ): ChromeBreadcrumb[] => { let breadcrumb = [ { - text: i18nDetections.PAGE_TITLE, + text: i18nDetections.BREADCRUMB_TITLE, href: getUrlForApp(`${APP_ID}:${SecurityPageName.detections}`, { path: !isEmpty(search[0]) ? search[0] : '', }), diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts index 92dc02ac8478c..10223716ef331 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts @@ -6,6 +6,13 @@ import { i18n } from '@kbn/i18n'; +export const BREADCRUMB_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.detectionsBreadcrumbTitle', + { + defaultMessage: 'Detections', + } +); + export const PAGE_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.detectionsPageTitle', { From 680d94c82fd9f97164aa5c78fd07b2fd3e10a679 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Fri, 24 Jul 2020 08:40:41 -0400 Subject: [PATCH 19/96] [Ingest Manager] Support DEGRADED state in fleet agent event (#73104) --- .../common/openapi/spec_oas3.json | 1 + .../ingest_manager/common/types/models/agent.ts | 1 + .../components/type_labels.tsx | 8 ++++++++ .../ingest_manager/server/types/models/agent.ts | 17 ++++++++++------- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json index 4b10dab5d1ae5..e16edac5ddb7a 100644 --- a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json +++ b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json @@ -4203,6 +4203,7 @@ "FAILED", "STOPPING", "STOPPED", + "DEGRADED", "DATA_DUMP", "ACKNOWLEDGED", "UNKNOWN" diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent.ts b/x-pack/plugins/ingest_manager/common/types/models/agent.ts index d3789c58a2c22..f31d33e73c76f 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent.ts @@ -53,6 +53,7 @@ export interface NewAgentEvent { | 'FAILED' | 'STOPPING' | 'STOPPED' + | 'DEGRADED' // Action results | 'DATA_DUMP' // Actions diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/type_labels.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/type_labels.tsx index e9cb59be37892..43e4d696ded66 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/type_labels.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/type_labels.tsx @@ -95,6 +95,14 @@ export const SUBTYPE_LABEL: { [key in AgentEvent['subtype']]: JSX.Element } = { /> ), + DEGRADED: ( + + + + ), DATA_DUMP: ( Date: Fri, 24 Jul 2020 09:02:23 -0400 Subject: [PATCH 20/96] [INGEST_MANAGER] Make package config name blank for endpoint on Package Config create (#73082) * Make package config name blank for endpoint * Added functional tests on endpoint side --- .../step_define_package_config.tsx | 14 +++++++++++--- .../apps/endpoint/policy_list.ts | 5 +++++ .../ingest_manager_create_package_config_page.ts | 7 +++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_define_package_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_define_package_config.tsx index a04d023ebcc48..f487b4e5235e7 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_define_package_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_define_package_config.tsx @@ -47,9 +47,17 @@ export const StepDefinePackageConfig: React.FunctionComponent<{ .sort(); updatePackageConfig({ - name: `${packageInfo.name}-${ - dsWithMatchingNames.length ? dsWithMatchingNames[dsWithMatchingNames.length - 1] + 1 : 1 - }`, + name: + // For Endpoint packages, the user must fill in the name, thus we don't attempt to generate + // a default one here. + // FIXME: Improve package configs name uniqueness - https://github.com/elastic/kibana/issues/72948 + packageInfo.name !== 'endpoint' + ? `${packageInfo.name}-${ + dsWithMatchingNames.length + ? dsWithMatchingNames[dsWithMatchingNames.length - 1] + 1 + : 1 + }` + : '', package: { name: packageInfo.name, title: packageInfo.title, diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts index a4b3a51c49513..0c5e15ed4104c 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts @@ -131,6 +131,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(endpointConfig).not.to.be(undefined); }); + it('should have empty value for package configuration name', async () => { + await pageObjects.ingestManagerCreatePackageConfig.selectAgentConfig(); + expect(await pageObjects.ingestManagerCreatePackageConfig.getPackageConfigName()).to.be(''); + }); + it('should redirect user back to Policy List after a successful save', async () => { const newPolicyName = `endpoint policy ${Date.now()}`; await pageObjects.ingestManagerCreatePackageConfig.selectAgentConfig(); diff --git a/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_package_config_page.ts b/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_package_config_page.ts index dd3fc637a3d6c..dfdb528b7362c 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_package_config_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_package_config_page.ts @@ -62,6 +62,13 @@ export function IngestManagerCreatePackageConfig({ } }, + /** + * Returns the package config name currently populated on the input field + */ + async getPackageConfigName() { + return testSubjects.getAttribute('packageConfigNameInput', 'value'); + }, + /** * Set the name of the package config on the input field * @param name From 46bd08d6d4c068e392aed757ffdc7ddcceb45932 Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Fri, 24 Jul 2020 16:39:03 +0300 Subject: [PATCH 21/96] Removed useless karma test (#73190) --- .../timelion_expression_suggestions.js | 58 ------------------- 1 file changed, 58 deletions(-) delete mode 100644 src/plugins/timelion/public/directives/timelion_expression_suggestions/__tests__/timelion_expression_suggestions.js diff --git a/src/plugins/timelion/public/directives/timelion_expression_suggestions/__tests__/timelion_expression_suggestions.js b/src/plugins/timelion/public/directives/timelion_expression_suggestions/__tests__/timelion_expression_suggestions.js deleted file mode 100644 index 8a35a72ed19e6..0000000000000 --- a/src/plugins/timelion/public/directives/timelion_expression_suggestions/__tests__/timelion_expression_suggestions.js +++ /dev/null @@ -1,58 +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 expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import '../timelion_expression_suggestions'; - -describe('Timelion expression suggestions directive', function () { - let scope; - let $compile; - - beforeEach(ngMock.module('kibana')); - - beforeEach( - ngMock.inject(function ($injector) { - $compile = $injector.get('$compile'); - scope = $injector.get('$rootScope').$new(); - }) - ); - - describe('attributes', function () { - describe('suggestions', function () { - let element = null; - const template = ``; - - beforeEach(function () { - element = $compile(template)(scope); - scope.$apply(() => { - scope.list = [{ name: 'suggestion1' }, { name: 'suggestion2' }, { name: 'suggestion3' }]; - }); - }); - - it('are rendered', function () { - expect(element.find('[data-test-subj="timelionSuggestionListItem"]').length).to.be( - scope.list.length - ); - }); - }); - }); -}); From 120ce71171c3bb3e20cee5b61f6acbc138e13379 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Fri, 24 Jul 2020 15:39:25 -0400 Subject: [PATCH 22/96] Return EUI CSS to Shareable Runtime (#72990) * Add Canvas Styles * Tree-shaking in EUI omits CSS in the runtime Co-authored-by: Elastic Machine --- x-pack/plugins/canvas/shareable_runtime/README.md | 2 +- x-pack/plugins/canvas/shareable_runtime/webpack.config.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/canvas/shareable_runtime/README.md b/x-pack/plugins/canvas/shareable_runtime/README.md index 8fdeb6ca6258e..3839e7c4ecb3f 100644 --- a/x-pack/plugins/canvas/shareable_runtime/README.md +++ b/x-pack/plugins/canvas/shareable_runtime/README.md @@ -207,7 +207,7 @@ There are a number of options for the build script: ### Prerequisite -Before testing or running this PR locally, you **must** run `node scripts/runtime` from `/canvas` _after_ `yarn kbn bootstrap` and _before_ starting Kibana. It is only built automatically when Kibana is built to avoid slowing down other development activities. +Before testing or running this PR locally, you **must** run `node scripts/shareable_runtime` from `/canvas` _after_ `yarn kbn bootstrap` and _before_ starting Kibana. It is only built automatically when Kibana is built to avoid slowing down other development activities. ### Webpack Dev Server diff --git a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js index 1a5a21985ba72..93dc3dbccd549 100644 --- a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js +++ b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js @@ -55,7 +55,6 @@ module.exports = { options: { presets: [require.resolve('@kbn/babel-preset/webpack_preset')], }, - sideEffects: false, }, { test: /\.tsx?$/, @@ -92,6 +91,7 @@ module.exports = { }, }, ], + sideEffects: true, }, { test: /\.module\.s(a|c)ss$/, From 7f36bd7dccca86d8a812df353bfe5023df72a268 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Fri, 24 Jul 2020 15:50:16 -0600 Subject: [PATCH 23/96] [Security Solution][Exceptions] Prevents value list entries from co-existing with non value list entries (#72995) ## Summary Fixes validation issue where value list exception entries could be added alongside non value list exception entries. Once a value list operator (`is in list` or `is not in list`) is selected the `nested` button will disable, and subsequent `and`/ `or`'s will only have the value list operators available to them:

If a value list is not selected in the first exception entry, all subsequent entries will no longer have the value list operators:

Adds validation for empty case to prevent network error when submitted no entries. Add modal:

Edit modal:

### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../components/autocomplete/operators.ts | 14 ++++++ .../exceptions/add_exception_modal/index.tsx | 6 +-- .../builder/builder_entry_item.test.tsx | 4 +- .../exceptions/builder/builder_entry_item.tsx | 16 ++++-- .../builder/builder_exception_item.tsx | 3 ++ .../exceptions/builder/helpers.test.tsx | 49 ++++++++++++------- .../components/exceptions/builder/helpers.tsx | 17 +++++-- .../components/exceptions/builder/index.tsx | 21 ++++++-- .../components/exceptions/builder/reducer.ts | 7 ++- .../exceptions/edit_exception_modal/index.tsx | 14 +++++- 10 files changed, 113 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/operators.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/operators.ts index a81d8cde94e34..c54f58a3fd4b3 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/operators.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/operators.ts @@ -90,3 +90,17 @@ export const EXCEPTION_OPERATORS: OperatorOption[] = [ isInListOperator, isNotInListOperator, ]; + +export const EXCEPTION_OPERATORS_SANS_LISTS: OperatorOption[] = [ + isOperator, + isNotOperator, + isOneOfOperator, + isNotOneOfOperator, + existsOperator, + doesNotExistOperator, +]; + +export const EXCEPTION_OPERATORS_ONLY_LISTS: OperatorOption[] = [ + isInListOperator, + isNotInListOperator, +]; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index d2fec1f34755f..a4fe52eaacf4e 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -276,8 +276,8 @@ export const AddExceptionModal = memo(function AddExceptionModal({ signalIndexName, ]); - const isSubmitButtonDisabled = useCallback( - () => fetchOrCreateListError || exceptionItemsToAdd.length === 0, + const isSubmitButtonDisabled = useMemo( + () => fetchOrCreateListError || exceptionItemsToAdd.every((item) => item.entries.length === 0), [fetchOrCreateListError, exceptionItemsToAdd] ); @@ -377,7 +377,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ {i18n.ADD_EXCEPTION} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx index b845848bd14d8..3dcc3eb5a8329 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx @@ -213,7 +213,7 @@ describe('BuilderEntryItem', () => { title: 'logstash-*', fields, }} - showLabel={false} + showLabel={true} listType="detection" addNested={false} onChange={jest.fn()} @@ -245,7 +245,7 @@ describe('BuilderEntryItem', () => { title: 'logstash-*', fields, }} - showLabel={false} + showLabel={true} listType="detection" addNested={false} onChange={jest.fn()} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx index 736e88ee9fe06..dcc8a0e4fb1ba 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx @@ -27,6 +27,7 @@ import { getEntryOnMatchAnyChange, getEntryOnListChange, } from './helpers'; +import { EXCEPTION_OPERATORS_ONLY_LISTS } from '../../autocomplete/operators'; interface EntryItemProps { entry: FormattedBuilderEntry; @@ -35,6 +36,7 @@ interface EntryItemProps { listType: ExceptionListType; addNested: boolean; onChange: (arg: BuilderEntry, i: number) => void; + onlyShowListOperators?: boolean; } export const BuilderEntryItem: React.FC = ({ @@ -44,6 +46,7 @@ export const BuilderEntryItem: React.FC = ({ addNested, showLabel, onChange, + onlyShowListOperators = false, }): JSX.Element => { const handleFieldChange = useCallback( ([newField]: IFieldType[]): void => { @@ -124,11 +127,14 @@ export const BuilderEntryItem: React.FC = ({ ); const renderOperatorInput = (isFirst: boolean): JSX.Element => { - const operatorOptions = getOperatorOptions( - entry, - listType, - entry.field != null && entry.field.type === 'boolean' - ); + const operatorOptions = onlyShowListOperators + ? EXCEPTION_OPERATORS_ONLY_LISTS + : getOperatorOptions( + entry, + listType, + entry.field != null && entry.field.type === 'boolean', + isFirst + ); const comboBox = ( void; onChangeExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void; + onlyShowListOperators?: boolean; } export const ExceptionListItemComponent = React.memo( @@ -58,6 +59,7 @@ export const ExceptionListItemComponent = React.memo( andLogicIncluded, onDeleteExceptionItem, onChangeExceptionItem, + onlyShowListOperators = false, }) => { const handleEntryChange = useCallback( (entry: BuilderEntry, entryIndex: number): void => { @@ -169,6 +171,7 @@ export const ExceptionListItemComponent = React.memo( exceptionItemIndex === 0 && index === 0 && item.nested !== 'child' } onChange={handleEntryChange} + onlyShowListOperators={onlyShowListOperators} /> {getDeleteButton( diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx index 8b74d44f29a18..17c94adf42648 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx @@ -14,32 +14,33 @@ import { getEntryExistsMock } from '../../../../../../lists/common/schemas/types import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getListResponseMock } from '../../../../../../lists/common/schemas/response/list_schema.mock'; import { - isOperator, - isOneOfOperator, - isNotOperator, - isNotOneOfOperator, - existsOperator, doesNotExistOperator, - isInListOperator, EXCEPTION_OPERATORS, + EXCEPTION_OPERATORS_SANS_LISTS, + existsOperator, + isInListOperator, + isNotOneOfOperator, + isNotOperator, + isOneOfOperator, + isOperator, } from '../../autocomplete/operators'; -import { FormattedBuilderEntry, BuilderEntry, ExceptionsBuilderExceptionItem } from '../types'; -import { IIndexPattern, IFieldType } from '../../../../../../../../src/plugins/data/common'; -import { EntryNested, Entry } from '../../../../lists_plugin_deps'; +import { BuilderEntry, ExceptionsBuilderExceptionItem, FormattedBuilderEntry } from '../types'; +import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; +import { Entry, EntryNested } from '../../../../lists_plugin_deps'; import { - getFilteredIndexPatterns, - getFormattedBuilderEntry, - isEntryNested, - getFormattedBuilderEntries, - getUpdatedEntriesOnDelete, getEntryFromOperator, - getOperatorOptions, getEntryOnFieldChange, - getEntryOnOperatorChange, - getEntryOnMatchChange, - getEntryOnMatchAnyChange, getEntryOnListChange, + getEntryOnMatchAnyChange, + getEntryOnMatchChange, + getEntryOnOperatorChange, + getFilteredIndexPatterns, + getFormattedBuilderEntries, + getFormattedBuilderEntry, + getOperatorOptions, + getUpdatedEntriesOnDelete, + isEntryNested, } from './helpers'; import { OperatorOption } from '../../autocomplete/types'; @@ -672,6 +673,18 @@ describe('Exception builder helpers', () => { const expected: OperatorOption[] = [isOperator, existsOperator]; expect(output).toEqual(expected); }); + + test('it returns list operators if specified to', () => { + const payloadItem: FormattedBuilderEntry = getMockBuilderEntry(); + const output = getOperatorOptions(payloadItem, 'detection', false, true); + expect(output).toEqual(EXCEPTION_OPERATORS); + }); + + test('it does not return list operators if specified not to', () => { + const payloadItem: FormattedBuilderEntry = getMockBuilderEntry(); + const output = getOperatorOptions(payloadItem, 'detection', false, false); + expect(output).toEqual(EXCEPTION_OPERATORS_SANS_LISTS); + }); }); describe('#getEntryOnFieldChange', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx index 2fe2c68941ae6..93bae091885c1 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx @@ -22,6 +22,7 @@ import { existsOperator, isOneOfOperator, EXCEPTION_OPERATORS, + EXCEPTION_OPERATORS_SANS_LISTS, } from '../../autocomplete/operators'; import { OperatorOption } from '../../autocomplete/types'; import { @@ -40,7 +41,6 @@ import { getEntryValue, getExceptionOperatorSelect } from '../helpers'; * * @param patterns IIndexPattern containing available fields on rule index * @param item exception item entry - * @param addNested boolean noting whether or not UI is currently * set to add a nested field */ export const getFilteredIndexPatterns = ( @@ -295,12 +295,14 @@ export const getEntryFromOperator = ( * * @param item * @param listType - * + * @param isBoolean + * @param includeValueListOperators whether or not to include the 'is in list' and 'is not in list' operators */ export const getOperatorOptions = ( item: FormattedBuilderEntry, listType: ExceptionListType, - isBoolean: boolean + isBoolean: boolean, + includeValueListOperators = true ): OperatorOption[] => { if (item.nested === 'parent' || item.field == null) { return [isOperator]; @@ -309,7 +311,11 @@ export const getOperatorOptions = ( } else if (item.nested != null && listType === 'detection') { return isBoolean ? [isOperator, existsOperator] : [isOperator, isOneOfOperator, existsOperator]; } else { - return isBoolean ? [isOperator, existsOperator] : EXCEPTION_OPERATORS; + return isBoolean + ? [isOperator, existsOperator] + : includeValueListOperators + ? EXCEPTION_OPERATORS + : EXCEPTION_OPERATORS_SANS_LISTS; } }; @@ -547,3 +553,6 @@ export const getDefaultNestedEmptyEntry = (): EmptyNestedEntry => ({ type: OperatorTypeEnum.NESTED, entries: [], }); + +export const containsValueListEntry = (items: ExceptionsBuilderExceptionItem[]): boolean => + items.some((item) => item.entries.some((entry) => entry.type === OperatorTypeEnum.LIST)); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx index 141429f152790..1ec49425ce8fd 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx @@ -24,7 +24,11 @@ import { BuilderButtonOptions } from './builder_button_options'; import { getNewExceptionItem, filterExceptionItems } from '../helpers'; import { ExceptionsBuilderExceptionItem, CreateExceptionListItemBuilderSchema } from '../types'; import { State, exceptionsBuilderReducer } from './reducer'; -import { getDefaultEmptyEntry, getDefaultNestedEmptyEntry } from './helpers'; +import { + containsValueListEntry, + getDefaultEmptyEntry, + getDefaultNestedEmptyEntry, +} from './helpers'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import exceptionableFields from '../exceptionable_fields.json'; @@ -44,6 +48,7 @@ const MyButtonsContainer = styled(EuiFlexItem)` const initialState: State = { disableAnd: false, + disableNested: false, disableOr: false, andLogicIncluded: false, addNested: false, @@ -82,12 +87,21 @@ export const ExceptionBuilder = ({ onChange, }: ExceptionBuilderProps) => { const [ - { exceptions, exceptionsToDelete, andLogicIncluded, disableAnd, disableOr, addNested }, + { + exceptions, + exceptionsToDelete, + andLogicIncluded, + disableAnd, + disableNested, + disableOr, + addNested, + }, dispatch, ] = useReducer(exceptionsBuilderReducer(), { ...initialState, disableAnd: isAndDisabled, disableOr: isOrDisabled, + disableNested: isNestedDisabled, }); const setUpdateExceptions = useCallback( @@ -362,6 +376,7 @@ export const ExceptionBuilder = ({ isOnlyItem={exceptions.length === 1} onDeleteExceptionItem={handleDeleteExceptionItem} onChangeExceptionItem={handleExceptionItemChange} + onlyShowListOperators={containsValueListEntry(exceptions)} /> @@ -379,7 +394,7 @@ export const ExceptionBuilder = ({ (state: State, action: Action): St const isAndDisabled = lastEntry != null && lastEntry.type === 'nested' && lastEntry.entries.length === 0; const isOrDisabled = lastEntry != null && lastEntry.type === 'nested'; + const containsValueList = action.exceptions.some( + ({ entries }) => entries.filter(({ type }) => type === OperatorTypeEnum.LIST).length > 0 + ); return { ...state, @@ -67,6 +71,7 @@ export const exceptionsBuilderReducer = () => (state: State, action: Action): St addNested: isAddNested, disableAnd: isAndDisabled, disableOr: isOrDisabled, + disableNested: containsValueList, }; } case 'setDefault': { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 4ad077edf66ff..47c3498cb6ab4 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, useState, useCallback, useEffect } from 'react'; +import React, { memo, useState, useCallback, useEffect, useMemo } from 'react'; import styled, { css } from 'styled-components'; import { EuiModal, @@ -146,6 +146,11 @@ export const EditExceptionModal = memo(function EditExceptionModal({ } }, [shouldDisableBulkClose]); + const isSubmitButtonDisabled = useMemo( + () => exceptionItemsToAdd.every((item) => item.entries.length === 0), + [exceptionItemsToAdd] + ); + const handleBuilderOnChange = useCallback( ({ exceptionItems, @@ -261,7 +266,12 @@ export const EditExceptionModal = memo(function EditExceptionModal({ {i18n.CANCEL} - + {i18n.EDIT_EXCEPTION_SAVE_BUTTON} From 2a82ff9566423a16e1f976c9d0d2db91acf006a9 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Sat, 25 Jul 2020 12:59:56 +0300 Subject: [PATCH 24/96] [KP] use new ES client in SO service (#72289) * adapt retryCallCluster for new ES client * review comments * retry on 408 ResponseError * remove legacy retry functions * use Migrator Es client in SO migration * update migration tests * improve ES typings and mocks * migrate decorate ES errors * add repository es client * use new es client in so repository * update repository tests * fix migrator integration tests * declare _seq_no & _primary_term on get response. _source expect to be a string * make _sourceIncludes and refresh compatible with the client * add test for repository_es_client * move ApiResponse to es client mocks * TEMP: handle wait_for as true for deleteByNamespace * add tests for migration_es_client * TEMP: skip test for deleteByNamespace refresh * pass ignore as transport option in mget * log both es client and response errors * fix update method test failures * update deleteByNamespace refresh settings es doesn't support 'refresh: wait_for' for `updateByQuery` endpoint * update repository tests. we do not allow customising wait_for * do not delegate retry logic to es client * fix type errors after master merged * fix repository tests * fix security solutions code SO doesn't throw Error with status code anymore. Always use SO error helpers * switch error conditions to use SO error helpers * cleanup * address comments about mocks * use isResponseError helper * address comments * fix type errors Co-authored-by: pgayvallet --- .../client/configure_client.test.ts | 30 +- .../elasticsearch/client/configure_client.ts | 13 +- src/core/server/elasticsearch/client/index.ts | 2 +- src/core/server/elasticsearch/client/mocks.ts | 34 +- .../client/retry_call_cluster.test.ts | 35 +- .../client/retry_call_cluster.ts | 2 +- src/core/server/elasticsearch/client/types.ts | 80 + .../elasticsearch_service.test.ts | 6 +- src/core/server/elasticsearch/index.ts | 4 + src/core/server/elasticsearch/legacy/index.ts | 1 - .../legacy/retry_call_cluster.test.ts | 147 -- .../legacy/retry_call_cluster.ts | 115 -- .../version_check/ensure_es_version.test.ts | 4 +- .../integration_tests/core_services.test.ts | 4 +- src/core/server/index.ts | 1 + .../__snapshots__/elastic_index.test.ts.snap | 1 - .../migrations/core/elastic_index.test.ts | 608 +++---- .../migrations/core/elastic_index.ts | 119 +- .../saved_objects/migrations/core/index.ts | 1 + .../migrations/core/index_migrator.test.ts | 176 +- .../migrations/core/index_migrator.ts | 27 +- .../migrations/core/migration_context.ts | 27 +- .../core/migration_es_client.test.mock.ts | 22 + .../core/migration_es_client.test.ts | 65 + .../migrations/core/migration_es_client.ts | 90 + .../migrations/kibana/kibana_migrator.test.ts | 53 +- .../migrations/kibana/kibana_migrator.ts | 20 +- .../saved_objects_service.test.ts | 45 +- .../saved_objects/saved_objects_service.ts | 34 +- .../saved_objects/serialization/index.ts | 7 +- .../service/lib/decorate_es_error.test.ts | 101 +- .../service/lib/decorate_es_error.ts | 55 +- .../service/lib/repository.test.js | 1489 ++++++++++------- .../saved_objects/service/lib/repository.ts | 405 ++--- .../lib/repository_es_client.test.mock.ts | 22 + .../service/lib/repository_es_client.test.ts | 64 + .../service/lib/repository_es_client.ts | 56 + .../services/sample_data/routes/list.ts | 4 +- .../{migrations.js => migrations.ts} | 171 +- .../apm/server/lib/apm_telemetry/index.ts | 8 +- .../server/routes/agent/handlers.ts | 2 +- .../exception_lists/create_endpoint_list.ts | 2 +- .../server/endpoint/routes/metadata/index.ts | 12 +- .../endpoint/routes/metadata/metadata.test.ts | 6 +- .../manifest_manager/manifest_manager.ts | 2 +- .../lib/reindexing/reindex_actions.test.ts | 7 +- .../server/lib/reindexing/reindex_actions.ts | 2 +- 47 files changed, 2383 insertions(+), 1798 deletions(-) delete mode 100644 src/core/server/elasticsearch/legacy/retry_call_cluster.test.ts delete mode 100644 src/core/server/elasticsearch/legacy/retry_call_cluster.ts create mode 100644 src/core/server/saved_objects/migrations/core/migration_es_client.test.mock.ts create mode 100644 src/core/server/saved_objects/migrations/core/migration_es_client.test.ts create mode 100644 src/core/server/saved_objects/migrations/core/migration_es_client.ts create mode 100644 src/core/server/saved_objects/service/lib/repository_es_client.test.mock.ts create mode 100644 src/core/server/saved_objects/service/lib/repository_es_client.test.ts create mode 100644 src/core/server/saved_objects/service/lib/repository_es_client.ts rename test/api_integration/apis/saved_objects/{migrations.js => migrations.ts} (68%) diff --git a/src/core/server/elasticsearch/client/configure_client.test.ts b/src/core/server/elasticsearch/client/configure_client.test.ts index 32da142764a78..11e3199a79fd2 100644 --- a/src/core/server/elasticsearch/client/configure_client.test.ts +++ b/src/core/server/elasticsearch/client/configure_client.test.ts @@ -118,26 +118,40 @@ describe('configureClient', () => { }); describe('Client logging', () => { - it('logs error when the client emits an error', () => { + it('logs error when the client emits an @elastic/elasticsearch error', () => { + const client = configureClient(config, { logger, scoped: false }); + + const response = createApiResponse({ body: {} }); + client.emit('response', new errors.TimeoutError('message', response), response); + + expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + "[TimeoutError]: message", + ], + ] + `); + }); + + it('logs error when the client emits an ResponseError returned by elasticsearch', () => { const client = configureClient(config, { logger, scoped: false }); const response = createApiResponse({ + statusCode: 400, + headers: {}, body: { error: { - type: 'error message', + type: 'illegal_argument_exception', + reason: 'request [/_path] contains unrecognized parameter: [name]', }, }, }); - client.emit('response', new errors.ResponseError(response), null); - client.emit('response', new Error('some error'), null); + client.emit('response', new errors.ResponseError(response), response); expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` Array [ Array [ - "ResponseError: error message", - ], - Array [ - "Error: some error", + "[illegal_argument_exception]: request [/_path] contains unrecognized parameter: [name]", ], ] `); diff --git a/src/core/server/elasticsearch/client/configure_client.ts b/src/core/server/elasticsearch/client/configure_client.ts index 5377f8ca1b070..9746ecb538b75 100644 --- a/src/core/server/elasticsearch/client/configure_client.ts +++ b/src/core/server/elasticsearch/client/configure_client.ts @@ -21,6 +21,7 @@ import { stringify } from 'querystring'; import { Client } from '@elastic/elasticsearch'; import { Logger } from '../../logging'; import { parseClientOptions, ElasticsearchClientConfig } from './client_config'; +import { isResponseError } from './errors'; export const configureClient = ( config: ElasticsearchClientConfig, @@ -35,9 +36,15 @@ export const configureClient = ( }; const addLogging = (client: Client, logger: Logger, logQueries: boolean) => { - client.on('response', (err, event) => { - if (err) { - logger.error(`${err.name}: ${err.message}`); + client.on('response', (error, event) => { + if (error) { + const errorMessage = + // error details for response errors provided by elasticsearch + isResponseError(error) + ? `[${event.body.error.type}]: ${event.body.error.reason}` + : `[${error.name}]: ${error.message}`; + + logger.error(errorMessage); } if (event && logQueries) { const params = event.meta.request.params; diff --git a/src/core/server/elasticsearch/client/index.ts b/src/core/server/elasticsearch/client/index.ts index b8125de2ee498..af63dfa6c7f4e 100644 --- a/src/core/server/elasticsearch/client/index.ts +++ b/src/core/server/elasticsearch/client/index.ts @@ -17,7 +17,7 @@ * under the License. */ -export { ElasticsearchClient } from './types'; +export * from './types'; export { IScopedClusterClient, ScopedClusterClient } from './scoped_cluster_client'; export { ElasticsearchClientConfig } from './client_config'; export { IClusterClient, ICustomClusterClient, ClusterClient } from './cluster_client'; diff --git a/src/core/server/elasticsearch/client/mocks.ts b/src/core/server/elasticsearch/client/mocks.ts index ec2885dfdf922..c93294404b52f 100644 --- a/src/core/server/elasticsearch/client/mocks.ts +++ b/src/core/server/elasticsearch/client/mocks.ts @@ -45,7 +45,7 @@ const createInternalClientMock = (): DeeplyMockedKeys => { .forEach((key) => { const propType = typeof obj[key]; if (propType === 'function') { - obj[key] = jest.fn(); + obj[key] = jest.fn(() => createSuccessTransportRequestPromise({})); } else if (propType === 'object' && obj[key] != null) { mockify(obj[key]); } @@ -70,6 +70,7 @@ const createInternalClientMock = (): DeeplyMockedKeys => { return (mock as unknown) as DeeplyMockedKeys; }; +// TODO fix naming ElasticsearchClientMock export type ElasticSearchClientMock = DeeplyMockedKeys; const createClientMock = (): ElasticSearchClientMock => @@ -124,32 +125,41 @@ export type MockedTransportRequestPromise = TransportRequestPromise & { abort: jest.MockedFunction<() => undefined>; }; -const createMockedClientResponse = (body: T): MockedTransportRequestPromise> => { - const response: ApiResponse = { - body, - statusCode: 200, - warnings: [], - headers: {}, - meta: {} as any, - }; +const createSuccessTransportRequestPromise = ( + body: T, + { statusCode = 200 }: { statusCode?: number } = {} +): MockedTransportRequestPromise> => { + const response = createApiResponse({ body, statusCode }); const promise = Promise.resolve(response); (promise as MockedTransportRequestPromise>).abort = jest.fn(); return promise as MockedTransportRequestPromise>; }; -const createMockedClientError = (err: any): MockedTransportRequestPromise => { +const createErrorTransportRequestPromise = (err: any): MockedTransportRequestPromise => { const promise = Promise.reject(err); (promise as MockedTransportRequestPromise).abort = jest.fn(); return promise as MockedTransportRequestPromise; }; +function createApiResponse(opts: Partial = {}): ApiResponse { + return { + body: {}, + statusCode: 200, + headers: {}, + warnings: [], + meta: {} as any, + ...opts, + }; +} + export const elasticsearchClientMock = { createClusterClient: createClusterClientMock, createCustomClusterClient: createCustomClusterClientMock, createScopedClusterClient: createScopedClusterClientMock, createElasticSearchClient: createClientMock, createInternalClient: createInternalClientMock, - createClientResponse: createMockedClientResponse, - createClientError: createMockedClientError, + createSuccessTransportRequestPromise, + createErrorTransportRequestPromise, + createApiResponse, }; diff --git a/src/core/server/elasticsearch/client/retry_call_cluster.test.ts b/src/core/server/elasticsearch/client/retry_call_cluster.test.ts index a7177c0b29047..3aa47e8b40e24 100644 --- a/src/core/server/elasticsearch/client/retry_call_cluster.test.ts +++ b/src/core/server/elasticsearch/client/retry_call_cluster.test.ts @@ -23,7 +23,8 @@ import { loggingSystemMock } from '../../logging/logging_system.mock'; import { retryCallCluster, migrationRetryCallCluster } from './retry_call_cluster'; const dummyBody = { foo: 'bar' }; -const createErrorReturn = (err: any) => elasticsearchClientMock.createClientError(err); +const createErrorReturn = (err: any) => + elasticsearchClientMock.createErrorTransportRequestPromise(err); describe('retryCallCluster', () => { let client: ReturnType; @@ -33,7 +34,9 @@ describe('retryCallCluster', () => { }); it('returns response from ES API call in case of success', async () => { - const successReturn = elasticsearchClientMock.createClientResponse({ ...dummyBody }); + const successReturn = elasticsearchClientMock.createSuccessTransportRequestPromise({ + ...dummyBody, + }); client.asyncSearch.get.mockReturnValue(successReturn); @@ -42,7 +45,9 @@ describe('retryCallCluster', () => { }); it('retries ES API calls that rejects with `NoLivingConnectionsError`', async () => { - const successReturn = elasticsearchClientMock.createClientResponse({ ...dummyBody }); + const successReturn = elasticsearchClientMock.createSuccessTransportRequestPromise({ + ...dummyBody, + }); client.asyncSearch.get .mockImplementationOnce(() => @@ -57,7 +62,9 @@ describe('retryCallCluster', () => { it('rejects when ES API calls reject with other errors', async () => { client.ping .mockImplementationOnce(() => createErrorReturn(new Error('unknown error'))) - .mockImplementationOnce(() => elasticsearchClientMock.createClientResponse({ ...dummyBody })); + .mockImplementationOnce(() => + elasticsearchClientMock.createSuccessTransportRequestPromise({ ...dummyBody }) + ); await expect(retryCallCluster(() => client.ping())).rejects.toMatchInlineSnapshot( `[Error: unknown error]` @@ -73,7 +80,9 @@ describe('retryCallCluster', () => { createErrorReturn(new errors.NoLivingConnectionsError('no living connections', {} as any)) ) .mockImplementationOnce(() => createErrorReturn(new Error('unknown error'))) - .mockImplementationOnce(() => elasticsearchClientMock.createClientResponse({ ...dummyBody })); + .mockImplementationOnce(() => + elasticsearchClientMock.createSuccessTransportRequestPromise({ ...dummyBody }) + ); await expect(retryCallCluster(() => client.ping())).rejects.toMatchInlineSnapshot( `[Error: unknown error]` @@ -94,7 +103,9 @@ describe('migrationRetryCallCluster', () => { client.ping .mockImplementationOnce(() => createErrorReturn(error)) .mockImplementationOnce(() => createErrorReturn(error)) - .mockImplementationOnce(() => elasticsearchClientMock.createClientResponse({ ...dummyBody })); + .mockImplementationOnce(() => + elasticsearchClientMock.createSuccessTransportRequestPromise({ ...dummyBody }) + ); }; it('retries ES API calls that rejects with `NoLivingConnectionsError`', async () => { @@ -225,7 +236,9 @@ describe('migrationRetryCallCluster', () => { } as any) ) ) - .mockImplementationOnce(() => elasticsearchClientMock.createClientResponse({ ...dummyBody })); + .mockImplementationOnce(() => + elasticsearchClientMock.createSuccessTransportRequestPromise({ ...dummyBody }) + ); await migrationRetryCallCluster(() => client.ping(), logger, 1); @@ -258,7 +271,9 @@ describe('migrationRetryCallCluster', () => { } as any) ) ) - .mockImplementationOnce(() => elasticsearchClientMock.createClientResponse({ ...dummyBody })); + .mockImplementationOnce(() => + elasticsearchClientMock.createSuccessTransportRequestPromise({ ...dummyBody }) + ); await expect( migrationRetryCallCluster(() => client.ping(), logger, 1) @@ -274,7 +289,9 @@ describe('migrationRetryCallCluster', () => { createErrorReturn(new errors.TimeoutError('timeout error', {} as any)) ) .mockImplementationOnce(() => createErrorReturn(new Error('unknown error'))) - .mockImplementationOnce(() => elasticsearchClientMock.createClientResponse({ ...dummyBody })); + .mockImplementationOnce(() => + elasticsearchClientMock.createSuccessTransportRequestPromise({ ...dummyBody }) + ); await expect( migrationRetryCallCluster(() => client.ping(), logger, 1) diff --git a/src/core/server/elasticsearch/client/retry_call_cluster.ts b/src/core/server/elasticsearch/client/retry_call_cluster.ts index 1ad039e512215..792f7f0a7fac9 100644 --- a/src/core/server/elasticsearch/client/retry_call_cluster.ts +++ b/src/core/server/elasticsearch/client/retry_call_cluster.ts @@ -27,7 +27,7 @@ const retryResponseStatuses = [ 403, // AuthenticationException 408, // RequestTimeout 410, // Gone -]; +] as const; /** * Retries the provided Elasticsearch API call when a `NoLivingConnectionsError` error is diff --git a/src/core/server/elasticsearch/client/types.ts b/src/core/server/elasticsearch/client/types.ts index 7ce998aab7669..285f52e89a591 100644 --- a/src/core/server/elasticsearch/client/types.ts +++ b/src/core/server/elasticsearch/client/types.ts @@ -41,3 +41,83 @@ export type ElasticsearchClient = Omit< ): TransportRequestPromise; }; }; + +interface ShardsResponse { + total: number; + successful: number; + failed: number; + skipped: number; +} + +interface Explanation { + value: number; + description: string; + details: Explanation[]; +} + +interface ShardsInfo { + total: number; + successful: number; + skipped: number; + failed: number; +} + +export interface CountResponse { + _shards: ShardsInfo; + count: number; +} + +/** + * Maintained until elasticsearch provides response typings out of the box + * https://github.com/elastic/elasticsearch-js/pull/970 + */ +export interface SearchResponse { + took: number; + timed_out: boolean; + _scroll_id?: string; + _shards: ShardsResponse; + hits: { + total: number; + max_score: number; + hits: Array<{ + _index: string; + _type: string; + _id: string; + _score: number; + _source: T; + _version?: number; + _explanation?: Explanation; + fields?: any; + highlight?: any; + inner_hits?: any; + matched_queries?: string[]; + sort?: string[]; + }>; + }; + aggregations?: any; +} + +export interface GetResponse { + _index: string; + _type: string; + _id: string; + _version: number; + _routing?: string; + found: boolean; + _source: T; + _seq_no: number; + _primary_term: number; +} + +export interface DeleteDocumentResponse { + _shards: ShardsResponse; + found: boolean; + _index: string; + _type: string; + _id: string; + _version: number; + result: string; + error?: { + type: string; + }; +} diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index 4375f09f1ce0b..49f5c8dd98790 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -227,7 +227,7 @@ describe('#setup', () => { it('esNodeVersionCompatibility$ only starts polling when subscribed to', async (done) => { const mockedClient = mockClusterClientInstance.asInternalUser; mockedClient.nodes.info.mockImplementation(() => - elasticsearchClientMock.createClientError(new Error()) + elasticsearchClientMock.createErrorTransportRequestPromise(new Error()) ); const setupContract = await elasticsearchService.setup(setupDeps); @@ -243,7 +243,7 @@ describe('#setup', () => { it('esNodeVersionCompatibility$ stops polling when unsubscribed from', async (done) => { const mockedClient = mockClusterClientInstance.asInternalUser; mockedClient.nodes.info.mockImplementation(() => - elasticsearchClientMock.createClientError(new Error()) + elasticsearchClientMock.createErrorTransportRequestPromise(new Error()) ); const setupContract = await elasticsearchService.setup(setupDeps); @@ -359,7 +359,7 @@ describe('#stop', () => { const mockedClient = mockClusterClientInstance.asInternalUser; mockedClient.nodes.info.mockImplementation(() => - elasticsearchClientMock.createClientError(new Error()) + elasticsearchClientMock.createErrorTransportRequestPromise(new Error()) ); const setupContract = await elasticsearchService.setup(setupDeps); diff --git a/src/core/server/elasticsearch/index.ts b/src/core/server/elasticsearch/index.ts index 8bb77b5dfdee0..32be6e6bf34dd 100644 --- a/src/core/server/elasticsearch/index.ts +++ b/src/core/server/elasticsearch/index.ts @@ -36,4 +36,8 @@ export { ElasticsearchClientConfig, ElasticsearchClient, IScopedClusterClient, + SearchResponse, + GetResponse, + DeleteDocumentResponse, + CountResponse, } from './client'; diff --git a/src/core/server/elasticsearch/legacy/index.ts b/src/core/server/elasticsearch/legacy/index.ts index 165980b9f4522..a1740faac7ddf 100644 --- a/src/core/server/elasticsearch/legacy/index.ts +++ b/src/core/server/elasticsearch/legacy/index.ts @@ -23,6 +23,5 @@ export { } from './cluster_client'; export { ILegacyScopedClusterClient, LegacyScopedClusterClient } from './scoped_cluster_client'; export { LegacyElasticsearchClientConfig } from './elasticsearch_client_config'; -export { retryCallCluster, migrationsRetryCallCluster } from './retry_call_cluster'; export { LegacyElasticsearchError, LegacyElasticsearchErrorHelpers } from './errors'; export * from './api_types'; diff --git a/src/core/server/elasticsearch/legacy/retry_call_cluster.test.ts b/src/core/server/elasticsearch/legacy/retry_call_cluster.test.ts deleted file mode 100644 index 62789a4fe952d..0000000000000 --- a/src/core/server/elasticsearch/legacy/retry_call_cluster.test.ts +++ /dev/null @@ -1,147 +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 * as legacyElasticsearch from 'elasticsearch'; - -import { retryCallCluster, migrationsRetryCallCluster } from './retry_call_cluster'; -import { loggingSystemMock } from '../../logging/logging_system.mock'; - -describe('retryCallCluster', () => { - it('retries ES API calls that rejects with NoConnections', () => { - expect.assertions(1); - const callEsApi = jest.fn(); - let i = 0; - const ErrorConstructor = legacyElasticsearch.errors.NoConnections; - callEsApi.mockImplementation(() => { - return i++ <= 2 ? Promise.reject(new ErrorConstructor()) : Promise.resolve('success'); - }); - const retried = retryCallCluster(callEsApi); - return expect(retried('endpoint')).resolves.toMatchInlineSnapshot(`"success"`); - }); - - it('rejects when ES API calls reject with other errors', async () => { - expect.assertions(3); - const callEsApi = jest.fn(); - let i = 0; - callEsApi.mockImplementation(() => { - i++; - - return i === 1 - ? Promise.reject(new Error('unknown error')) - : i === 2 - ? Promise.resolve('success') - : i === 3 || i === 4 - ? Promise.reject(new legacyElasticsearch.errors.NoConnections()) - : i === 5 - ? Promise.reject(new Error('unknown error')) - : null; - }); - const retried = retryCallCluster(callEsApi); - await expect(retried('endpoint')).rejects.toMatchInlineSnapshot(`[Error: unknown error]`); - await expect(retried('endpoint')).resolves.toMatchInlineSnapshot(`"success"`); - return expect(retried('endpoint')).rejects.toMatchInlineSnapshot(`[Error: unknown error]`); - }); -}); - -describe('migrationsRetryCallCluster', () => { - const errors = [ - 'NoConnections', - 'ConnectionFault', - 'ServiceUnavailable', - 'RequestTimeout', - 'AuthenticationException', - 'AuthorizationException', - 'Gone', - ]; - - const mockLogger = loggingSystemMock.create(); - - beforeEach(() => { - loggingSystemMock.clear(mockLogger); - }); - - errors.forEach((errorName) => { - it('retries ES API calls that rejects with ' + errorName, () => { - expect.assertions(1); - const callEsApi = jest.fn(); - let i = 0; - const ErrorConstructor = (legacyElasticsearch.errors as any)[errorName]; - callEsApi.mockImplementation(() => { - return i++ <= 2 ? Promise.reject(new ErrorConstructor()) : Promise.resolve('success'); - }); - const retried = migrationsRetryCallCluster(callEsApi, mockLogger.get('mock log'), 1); - return expect(retried('endpoint')).resolves.toMatchInlineSnapshot(`"success"`); - }); - }); - - it('retries ES API calls that rejects with snapshot_in_progress_exception', () => { - expect.assertions(1); - const callEsApi = jest.fn(); - let i = 0; - callEsApi.mockImplementation(() => { - return i++ <= 2 - ? Promise.reject({ body: { error: { type: 'snapshot_in_progress_exception' } } }) - : Promise.resolve('success'); - }); - const retried = migrationsRetryCallCluster(callEsApi, mockLogger.get('mock log'), 1); - return expect(retried('endpoint')).resolves.toMatchInlineSnapshot(`"success"`); - }); - - it('rejects when ES API calls reject with other errors', async () => { - expect.assertions(3); - const callEsApi = jest.fn(); - let i = 0; - callEsApi.mockImplementation(() => { - i++; - - return i === 1 - ? Promise.reject(new Error('unknown error')) - : i === 2 - ? Promise.resolve('success') - : i === 3 || i === 4 - ? Promise.reject(new legacyElasticsearch.errors.NoConnections()) - : i === 5 - ? Promise.reject(new Error('unknown error')) - : null; - }); - const retried = migrationsRetryCallCluster(callEsApi, mockLogger.get('mock log'), 1); - await expect(retried('endpoint')).rejects.toMatchInlineSnapshot(`[Error: unknown error]`); - await expect(retried('endpoint')).resolves.toMatchInlineSnapshot(`"success"`); - return expect(retried('endpoint')).rejects.toMatchInlineSnapshot(`[Error: unknown error]`); - }); - - it('logs only once for each unique error message', async () => { - const callEsApi = jest.fn(); - callEsApi.mockRejectedValueOnce(new legacyElasticsearch.errors.NoConnections()); - callEsApi.mockRejectedValueOnce(new legacyElasticsearch.errors.NoConnections()); - callEsApi.mockRejectedValueOnce(new legacyElasticsearch.errors.AuthenticationException()); - callEsApi.mockResolvedValueOnce('done'); - const retried = migrationsRetryCallCluster(callEsApi, mockLogger.get('mock log'), 1); - await retried('endpoint'); - expect(loggingSystemMock.collect(mockLogger).warn).toMatchInlineSnapshot(` - Array [ - Array [ - "Unable to connect to Elasticsearch. Error: No Living connections", - ], - Array [ - "Unable to connect to Elasticsearch. Error: Authentication Exception", - ], - ] - `); - }); -}); diff --git a/src/core/server/elasticsearch/legacy/retry_call_cluster.ts b/src/core/server/elasticsearch/legacy/retry_call_cluster.ts deleted file mode 100644 index 1b05cb2bf13cd..0000000000000 --- a/src/core/server/elasticsearch/legacy/retry_call_cluster.ts +++ /dev/null @@ -1,115 +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 { retryWhen, concatMap } from 'rxjs/operators'; -import { defer, throwError, iif, timer } from 'rxjs'; -import * as legacyElasticsearch from 'elasticsearch'; - -import { LegacyCallAPIOptions } from '.'; -import { LegacyAPICaller } from './api_types'; -import { Logger } from '../../logging'; - -const esErrors = legacyElasticsearch.errors; - -/** - * Retries the provided Elasticsearch API call when an error such as - * `AuthenticationException` `NoConnections`, `ConnectionFault`, - * `ServiceUnavailable` or `RequestTimeout` are encountered. The API call will - * be retried once a second, indefinitely, until a successful response or a - * different error is received. - * - * @param apiCaller - * @param log - * @param delay - */ -export function migrationsRetryCallCluster( - apiCaller: LegacyAPICaller, - log: Logger, - delay: number = 2500 -) { - const previousErrors: string[] = []; - return ( - endpoint: string, - clientParams: Record = {}, - options?: LegacyCallAPIOptions - ) => { - return defer(() => apiCaller(endpoint, clientParams, options)) - .pipe( - retryWhen((error$) => - error$.pipe( - concatMap((error) => { - if (!previousErrors.includes(error.message)) { - log.warn(`Unable to connect to Elasticsearch. Error: ${error.message}`); - previousErrors.push(error.message); - } - return iif( - () => { - return ( - error instanceof esErrors.NoConnections || - error instanceof esErrors.ConnectionFault || - error instanceof esErrors.ServiceUnavailable || - error instanceof esErrors.RequestTimeout || - error instanceof esErrors.AuthenticationException || - error instanceof esErrors.AuthorizationException || - // @ts-expect-error - error instanceof esErrors.Gone || - error?.body?.error?.type === 'snapshot_in_progress_exception' - ); - }, - timer(delay), - throwError(error) - ); - }) - ) - ) - ) - .toPromise(); - }; -} - -/** - * Retries the provided Elasticsearch API call when a `NoConnections` error is - * encountered. The API call will be retried once a second, indefinitely, until - * a successful response or a different error is received. - * - * @param apiCaller - */ -export function retryCallCluster(apiCaller: LegacyAPICaller) { - return ( - endpoint: string, - clientParams: Record = {}, - options?: LegacyCallAPIOptions - ) => { - return defer(() => apiCaller(endpoint, clientParams, options)) - .pipe( - retryWhen((errors) => - errors.pipe( - concatMap((error) => - iif( - () => error instanceof legacyElasticsearch.errors.NoConnections, - timer(1000), - throwError(error) - ) - ) - ) - ) - ) - .toPromise(); - }; -} diff --git a/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts b/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts index 21adac081acf7..f6313f68abff2 100644 --- a/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts +++ b/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts @@ -28,8 +28,8 @@ const mockLogger = mockLoggerFactory.get('mock logger'); const KIBANA_VERSION = '5.1.0'; -const createEsSuccess = elasticsearchClientMock.createClientResponse; -const createEsError = elasticsearchClientMock.createClientError; +const createEsSuccess = elasticsearchClientMock.createSuccessTransportRequestPromise; +const createEsError = elasticsearchClientMock.createErrorTransportRequestPromise; function createNodes(...versions: string[]): NodesInfo { const nodes = {} as any; diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index 6338326626d54..6a00db5a6cc4a 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -479,7 +479,7 @@ describe('http service', () => { let elasticsearch: InternalElasticsearchServiceStart; esClient.ping.mockImplementation(() => - elasticsearchClientMock.createClientError( + elasticsearchClientMock.createErrorTransportRequestPromise( new ResponseError({ statusCode: 401, body: { @@ -517,7 +517,7 @@ describe('http service', () => { let elasticsearch: InternalElasticsearchServiceStart; esClient.ping.mockImplementation(() => - elasticsearchClientMock.createClientError( + elasticsearchClientMock.createErrorTransportRequestPromise( new ResponseError({ statusCode: 401, body: { diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 706ec88c6ebfd..c846e81573acb 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -109,6 +109,7 @@ export { LegacyAPICaller, FakeRequest, ScopeableRequest, + ElasticsearchClient, } from './elasticsearch'; export * from './elasticsearch/legacy/api_types'; export { diff --git a/src/core/server/saved_objects/migrations/core/__snapshots__/elastic_index.test.ts.snap b/src/core/server/saved_objects/migrations/core/__snapshots__/elastic_index.test.ts.snap index 76bcc6ee219d9..6bd567be204d0 100644 --- a/src/core/server/saved_objects/migrations/core/__snapshots__/elastic_index.test.ts.snap +++ b/src/core/server/saved_objects/migrations/core/__snapshots__/elastic_index.test.ts.snap @@ -2,7 +2,6 @@ exports[`ElasticIndex write writes documents in bulk to the index 1`] = ` Array [ - "bulk", Object { "body": Array [ Object { diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.test.ts b/src/core/server/saved_objects/migrations/core/elastic_index.test.ts index 393cbb7fbb2ae..fb8fb4ef95081 100644 --- a/src/core/server/saved_objects/migrations/core/elastic_index.test.ts +++ b/src/core/server/saved_objects/migrations/core/elastic_index.test.ts @@ -18,47 +18,52 @@ */ import _ from 'lodash'; +import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; import * as Index from './elastic_index'; describe('ElasticIndex', () => { + let client: ReturnType; + + beforeEach(() => { + client = elasticsearchClientMock.createElasticSearchClient(); + }); describe('fetchInfo', () => { test('it handles 404', async () => { - const callCluster = jest - .fn() - .mockImplementation(async (path: string, { ignore, index }: any) => { - expect(path).toEqual('indices.get'); - expect(ignore).toEqual([404]); - expect(index).toEqual('.kibana-test'); - return { status: 404 }; - }); + client.indices.get.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + ); - const info = await Index.fetchInfo(callCluster as any, '.kibana-test'); + const info = await Index.fetchInfo(client, '.kibana-test'); expect(info).toEqual({ aliases: {}, exists: false, indexName: '.kibana-test', mappings: { dynamic: 'strict', properties: {} }, }); + + expect(client.indices.get).toHaveBeenCalledWith({ index: '.kibana-test' }, { ignore: [404] }); }); test('fails if the index doc type is unsupported', async () => { - const callCluster = jest.fn(async (path: string, { index }: any) => { - return { + client.indices.get.mockImplementation((params) => { + const index = params!.index as string; + return elasticsearchClientMock.createSuccessTransportRequestPromise({ [index]: { aliases: { foo: index }, mappings: { spock: { dynamic: 'strict', properties: { a: 'b' } } }, }, - }; + }); }); - await expect(Index.fetchInfo(callCluster as any, '.baz')).rejects.toThrow( + await expect(Index.fetchInfo(client, '.baz')).rejects.toThrow( /cannot be automatically migrated/ ); }); test('fails if there are multiple root types', async () => { - const callCluster = jest.fn().mockImplementation(async (path: string, { index }: any) => { - return { + client.indices.get.mockImplementation((params) => { + const index = params!.index as string; + return elasticsearchClientMock.createSuccessTransportRequestPromise({ [index]: { aliases: { foo: index }, mappings: { @@ -66,25 +71,26 @@ describe('ElasticIndex', () => { doctor: { dynamic: 'strict', properties: { a: 'b' } }, }, }, - }; + }); }); - await expect(Index.fetchInfo(callCluster, '.baz')).rejects.toThrow( + await expect(Index.fetchInfo(client, '.baz')).rejects.toThrow( /cannot be automatically migrated/ ); }); test('decorates index info with exists and indexName', async () => { - const callCluster = jest.fn().mockImplementation(async (path: string, { index }: any) => { - return { + client.indices.get.mockImplementation((params) => { + const index = params!.index as string; + return elasticsearchClientMock.createSuccessTransportRequestPromise({ [index]: { aliases: { foo: index }, mappings: { dynamic: 'strict', properties: { a: 'b' } }, }, - }; + }); }); - const info = await Index.fetchInfo(callCluster, '.baz'); + const info = await Index.fetchInfo(client, '.baz'); expect(info).toEqual({ aliases: { foo: '.baz' }, mappings: { dynamic: 'strict', properties: { a: 'b' } }, @@ -96,171 +102,120 @@ describe('ElasticIndex', () => { describe('createIndex', () => { test('calls indices.create', async () => { - const callCluster = jest.fn(async (path: string, { body, index }: any) => { - expect(path).toEqual('indices.create'); - expect(body).toEqual({ + await Index.createIndex(client, '.abcd', { foo: 'bar' } as any); + + expect(client.indices.create).toHaveBeenCalledTimes(1); + expect(client.indices.create).toHaveBeenCalledWith({ + body: { mappings: { foo: 'bar' }, - settings: { auto_expand_replicas: '0-1', number_of_shards: 1 }, - }); - expect(index).toEqual('.abcd'); + settings: { + auto_expand_replicas: '0-1', + number_of_shards: 1, + }, + }, + index: '.abcd', }); - - await Index.createIndex(callCluster as any, '.abcd', { foo: 'bar' } as any); - expect(callCluster).toHaveBeenCalled(); }); }); describe('deleteIndex', () => { test('calls indices.delete', async () => { - const callCluster = jest.fn(async (path: string, { index }: any) => { - expect(path).toEqual('indices.delete'); - expect(index).toEqual('.lotr'); - }); + await Index.deleteIndex(client, '.lotr'); - await Index.deleteIndex(callCluster as any, '.lotr'); - expect(callCluster).toHaveBeenCalled(); + expect(client.indices.delete).toHaveBeenCalledTimes(1); + expect(client.indices.delete).toHaveBeenCalledWith({ + index: '.lotr', + }); }); }); describe('claimAlias', () => { - function assertCalled(callCluster: jest.Mock) { - expect(callCluster.mock.calls.map(([path]) => path)).toEqual([ - 'indices.getAlias', - 'indices.updateAliases', - 'indices.refresh', - ]); - } - test('handles unaliased indices', async () => { - const callCluster = jest.fn(async (path: string, arg: any) => { - switch (path) { - case 'indices.getAlias': - expect(arg.ignore).toEqual([404]); - expect(arg.name).toEqual('.hola'); - return { status: 404 }; - case 'indices.updateAliases': - expect(arg.body).toEqual({ - actions: [{ add: { index: '.hola-42', alias: '.hola' } }], - }); - return true; - case 'indices.refresh': - expect(arg.index).toEqual('.hola-42'); - return true; - default: - throw new Error(`Dunnoes what ${path} means.`); - } - }); + client.indices.getAlias.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + ); - await Index.claimAlias(callCluster as any, '.hola-42', '.hola'); + await Index.claimAlias(client, '.hola-42', '.hola'); - assertCalled(callCluster); + expect(client.indices.getAlias).toHaveBeenCalledWith( + { + name: '.hola', + }, + { ignore: [404] } + ); + expect(client.indices.updateAliases).toHaveBeenCalledWith({ + body: { + actions: [{ add: { index: '.hola-42', alias: '.hola' } }], + }, + }); + expect(client.indices.refresh).toHaveBeenCalledWith({ + index: '.hola-42', + }); }); test('removes existing alias', async () => { - const callCluster = jest.fn(async (path: string, arg: any) => { - switch (path) { - case 'indices.getAlias': - return { '.my-fanci-index': '.muchacha' }; - case 'indices.updateAliases': - expect(arg.body).toEqual({ - actions: [ - { remove: { index: '.my-fanci-index', alias: '.muchacha' } }, - { add: { index: '.ze-index', alias: '.muchacha' } }, - ], - }); - return true; - case 'indices.refresh': - expect(arg.index).toEqual('.ze-index'); - return true; - default: - throw new Error(`Dunnoes what ${path} means.`); - } - }); + client.indices.getAlias.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + '.my-fanci-index': '.muchacha', + }) + ); - await Index.claimAlias(callCluster as any, '.ze-index', '.muchacha'); + await Index.claimAlias(client, '.ze-index', '.muchacha'); - assertCalled(callCluster); + expect(client.indices.getAlias).toHaveBeenCalledTimes(1); + expect(client.indices.updateAliases).toHaveBeenCalledWith({ + body: { + actions: [ + { remove: { index: '.my-fanci-index', alias: '.muchacha' } }, + { add: { index: '.ze-index', alias: '.muchacha' } }, + ], + }, + }); + expect(client.indices.refresh).toHaveBeenCalledWith({ + index: '.ze-index', + }); }); test('allows custom alias actions', async () => { - const callCluster = jest.fn(async (path: string, arg: any) => { - switch (path) { - case 'indices.getAlias': - return { '.my-fanci-index': '.muchacha' }; - case 'indices.updateAliases': - expect(arg.body).toEqual({ - actions: [ - { remove_index: { index: 'awww-snap!' } }, - { remove: { index: '.my-fanci-index', alias: '.muchacha' } }, - { add: { index: '.ze-index', alias: '.muchacha' } }, - ], - }); - return true; - case 'indices.refresh': - expect(arg.index).toEqual('.ze-index'); - return true; - default: - throw new Error(`Dunnoes what ${path} means.`); - } - }); + client.indices.getAlias.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + '.my-fanci-index': '.muchacha', + }) + ); - await Index.claimAlias(callCluster as any, '.ze-index', '.muchacha', [ + await Index.claimAlias(client, '.ze-index', '.muchacha', [ { remove_index: { index: 'awww-snap!' } }, ]); - assertCalled(callCluster); + expect(client.indices.getAlias).toHaveBeenCalledTimes(1); + expect(client.indices.updateAliases).toHaveBeenCalledWith({ + body: { + actions: [ + { remove_index: { index: 'awww-snap!' } }, + { remove: { index: '.my-fanci-index', alias: '.muchacha' } }, + { add: { index: '.ze-index', alias: '.muchacha' } }, + ], + }, + }); + expect(client.indices.refresh).toHaveBeenCalledWith({ + index: '.ze-index', + }); }); }); describe('convertToAlias', () => { test('it creates the destination index, then reindexes to it', async () => { - const callCluster = jest.fn(async (path: string, arg: any) => { - switch (path) { - case 'indices.create': - expect(arg.body).toEqual({ - mappings: { - dynamic: 'strict', - properties: { foo: { type: 'keyword' } }, - }, - settings: { auto_expand_replicas: '0-1', number_of_shards: 1 }, - }); - expect(arg.index).toEqual('.ze-index'); - return true; - case 'reindex': - expect(arg).toMatchObject({ - body: { - dest: { index: '.ze-index' }, - source: { index: '.muchacha' }, - script: { - source: `ctx._id = ctx._source.type + ':' + ctx._id`, - lang: 'painless', - }, - }, - refresh: true, - waitForCompletion: false, - }); - return { task: 'abc' }; - case 'tasks.get': - expect(arg.taskId).toEqual('abc'); - return { completed: true }; - case 'indices.getAlias': - return { '.my-fanci-index': '.muchacha' }; - case 'indices.updateAliases': - expect(arg.body).toEqual({ - actions: [ - { remove_index: { index: '.muchacha' } }, - { remove: { alias: '.muchacha', index: '.my-fanci-index' } }, - { add: { index: '.ze-index', alias: '.muchacha' } }, - ], - }); - return true; - case 'indices.refresh': - expect(arg.index).toEqual('.ze-index'); - return true; - default: - throw new Error(`Dunnoes what ${path} means.`); - } - }); + client.indices.getAlias.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + '.my-fanci-index': '.muchacha', + }) + ); + client.reindex.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ task: 'abc' }) + ); + client.tasks.get.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ completed: true }) + ); const info = { aliases: {}, @@ -271,61 +226,77 @@ describe('ElasticIndex', () => { properties: { foo: { type: 'keyword' } }, }, }; + await Index.convertToAlias( - callCluster as any, + client, info, '.muchacha', 10, `ctx._id = ctx._source.type + ':' + ctx._id` ); - expect(callCluster.mock.calls.map(([path]) => path)).toEqual([ - 'indices.create', - 'reindex', - 'tasks.get', - 'indices.getAlias', - 'indices.updateAliases', - 'indices.refresh', - ]); + expect(client.indices.create).toHaveBeenCalledWith({ + body: { + mappings: { + dynamic: 'strict', + properties: { foo: { type: 'keyword' } }, + }, + settings: { auto_expand_replicas: '0-1', number_of_shards: 1 }, + }, + index: '.ze-index', + }); + + expect(client.reindex).toHaveBeenCalledWith({ + body: { + dest: { index: '.ze-index' }, + source: { index: '.muchacha', size: 10 }, + script: { + source: `ctx._id = ctx._source.type + ':' + ctx._id`, + lang: 'painless', + }, + }, + refresh: true, + wait_for_completion: false, + }); + + expect(client.tasks.get).toHaveBeenCalledWith({ + task_id: 'abc', + }); + + expect(client.indices.updateAliases).toHaveBeenCalledWith({ + body: { + actions: [ + { remove_index: { index: '.muchacha' } }, + { remove: { alias: '.muchacha', index: '.my-fanci-index' } }, + { add: { index: '.ze-index', alias: '.muchacha' } }, + ], + }, + }); + + expect(client.indices.refresh).toHaveBeenCalledWith({ + index: '.ze-index', + }); }); test('throws error if re-index task fails', async () => { - const callCluster = jest.fn(async (path: string, arg: any) => { - switch (path) { - case 'indices.create': - expect(arg.body).toEqual({ - mappings: { - dynamic: 'strict', - properties: { foo: { type: 'keyword' } }, - }, - settings: { auto_expand_replicas: '0-1', number_of_shards: 1 }, - }); - expect(arg.index).toEqual('.ze-index'); - return true; - case 'reindex': - expect(arg).toMatchObject({ - body: { - dest: { index: '.ze-index' }, - source: { index: '.muchacha' }, - }, - refresh: true, - waitForCompletion: false, - }); - return { task: 'abc' }; - case 'tasks.get': - expect(arg.taskId).toEqual('abc'); - return { - completed: true, - error: { - type: 'search_phase_execution_exception', - reason: 'all shards failed', - failed_shards: [], - }, - }; - default: - throw new Error(`Dunnoes what ${path} means.`); - } - }); + client.indices.getAlias.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + '.my-fanci-index': '.muchacha', + }) + ); + client.reindex.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ task: 'abc' }) + ); + client.tasks.get.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + completed: true, + error: { + type: 'search_phase_execution_exception', + reason: 'all shards failed', + failed_shards: [], + }, + }) + ); const info = { aliases: {}, @@ -336,22 +307,44 @@ describe('ElasticIndex', () => { properties: { foo: { type: 'keyword' } }, }, }; - await expect(Index.convertToAlias(callCluster as any, info, '.muchacha', 10)).rejects.toThrow( + + await expect(Index.convertToAlias(client, info, '.muchacha', 10)).rejects.toThrow( /Re-index failed \[search_phase_execution_exception\] all shards failed/ ); - expect(callCluster.mock.calls.map(([path]) => path)).toEqual([ - 'indices.create', - 'reindex', - 'tasks.get', - ]); + expect(client.indices.create).toHaveBeenCalledWith({ + body: { + mappings: { + dynamic: 'strict', + properties: { foo: { type: 'keyword' } }, + }, + settings: { auto_expand_replicas: '0-1', number_of_shards: 1 }, + }, + index: '.ze-index', + }); + + expect(client.reindex).toHaveBeenCalledWith({ + body: { + dest: { index: '.ze-index' }, + source: { index: '.muchacha', size: 10 }, + }, + refresh: true, + wait_for_completion: false, + }); + + expect(client.tasks.get).toHaveBeenCalledWith({ + task_id: 'abc', + }); }); }); describe('write', () => { test('writes documents in bulk to the index', async () => { + client.bulk.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ items: [] }) + ); + const index = '.myalias'; - const callCluster = jest.fn().mockResolvedValue({ items: [] }); const docs = [ { _id: 'niceguy:fredrogers', @@ -375,19 +368,20 @@ describe('ElasticIndex', () => { }, ]; - await Index.write(callCluster, index, docs); + await Index.write(client, index, docs); - expect(callCluster).toHaveBeenCalled(); - expect(callCluster.mock.calls[0]).toMatchSnapshot(); + expect(client.bulk).toHaveBeenCalled(); + expect(client.bulk.mock.calls[0]).toMatchSnapshot(); }); test('fails if any document fails', async () => { - const index = '.myalias'; - const callCluster = jest.fn(() => - Promise.resolve({ + client.bulk.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ items: [{ index: { error: { type: 'shazm', reason: 'dern' } } }], }) ); + + const index = '.myalias'; const docs = [ { _id: 'niceguy:fredrogers', @@ -400,23 +394,20 @@ describe('ElasticIndex', () => { }, ]; - await expect(Index.write(callCluster as any, index, docs)).rejects.toThrow(/dern/); - expect(callCluster).toHaveBeenCalled(); + await expect(Index.write(client as any, index, docs)).rejects.toThrow(/dern/); + expect(client.bulk).toHaveBeenCalledTimes(1); }); }); describe('reader', () => { test('returns docs in batches', async () => { const index = '.myalias'; - const callCluster = jest.fn(); - const batch1 = [ { _id: 'such:1', _source: { type: 'such', such: { num: 1 } }, }, ]; - const batch2 = [ { _id: 'aaa:2', @@ -432,42 +423,56 @@ describe('ElasticIndex', () => { }, ]; - callCluster - .mockResolvedValueOnce({ + client.search = jest.fn().mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ _scroll_id: 'x', _shards: { success: 1, total: 1 }, hits: { hits: _.cloneDeep(batch1) }, }) - .mockResolvedValueOnce({ - _scroll_id: 'y', - _shards: { success: 1, total: 1 }, - hits: { hits: _.cloneDeep(batch2) }, - }) - .mockResolvedValueOnce({ - _scroll_id: 'z', - _shards: { success: 1, total: 1 }, - hits: { hits: [] }, - }) - .mockResolvedValue({}); - - const read = Index.reader(callCluster, index, { batchSize: 100, scrollDuration: '5m' }); + ); + client.scroll = jest + .fn() + .mockReturnValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + _scroll_id: 'y', + _shards: { success: 1, total: 1 }, + hits: { hits: _.cloneDeep(batch2) }, + }) + ) + .mockReturnValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + _scroll_id: 'z', + _shards: { success: 1, total: 1 }, + hits: { hits: [] }, + }) + ); + + const read = Index.reader(client, index, { batchSize: 100, scrollDuration: '5m' }); expect(await read()).toEqual(batch1); expect(await read()).toEqual(batch2); expect(await read()).toEqual([]); - // Check order of calls, as well as args - expect(callCluster.mock.calls).toEqual([ - ['search', { body: { size: 100 }, index, scroll: '5m' }], - ['scroll', { scroll: '5m', scrollId: 'x' }], - ['scroll', { scroll: '5m', scrollId: 'y' }], - ['clearScroll', { scrollId: 'z' }], - ]); + expect(client.search).toHaveBeenCalledWith({ + body: { size: 100 }, + index, + scroll: '5m', + }); + expect(client.scroll).toHaveBeenCalledWith({ + scroll: '5m', + scroll_id: 'x', + }); + expect(client.scroll).toHaveBeenCalledWith({ + scroll: '5m', + scroll_id: 'y', + }); + expect(client.clearScroll).toHaveBeenCalledWith({ + scroll_id: 'z', + }); }); test('returns all root-level properties', async () => { const index = '.myalias'; - const callCluster = jest.fn(); const batch = [ { _id: 'such:1', @@ -480,19 +485,22 @@ describe('ElasticIndex', () => { }, ]; - callCluster - .mockResolvedValueOnce({ + client.search = jest.fn().mockReturnValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ _scroll_id: 'x', _shards: { success: 1, total: 1 }, hits: { hits: _.cloneDeep(batch) }, }) - .mockResolvedValue({ + ); + client.scroll = jest.fn().mockReturnValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ _scroll_id: 'z', _shards: { success: 1, total: 1 }, hits: { hits: [] }, - }); + }) + ); - const read = Index.reader(callCluster, index, { + const read = Index.reader(client, index, { batchSize: 100, scrollDuration: '5m', }); @@ -502,11 +510,14 @@ describe('ElasticIndex', () => { test('fails if not all shards were successful', async () => { const index = '.myalias'; - const callCluster = jest.fn(); - callCluster.mockResolvedValue({ _shards: { successful: 1, total: 2 } }); + client.search = jest.fn().mockReturnValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + _shards: { successful: 1, total: 2 }, + }) + ); - const read = Index.reader(callCluster, index, { + const read = Index.reader(client, index, { batchSize: 100, scrollDuration: '5m', }); @@ -516,7 +527,6 @@ describe('ElasticIndex', () => { test('handles shards not being returned', async () => { const index = '.myalias'; - const callCluster = jest.fn(); const batch = [ { _id: 'such:1', @@ -529,11 +539,20 @@ describe('ElasticIndex', () => { }, ]; - callCluster - .mockResolvedValueOnce({ _scroll_id: 'x', hits: { hits: _.cloneDeep(batch) } }) - .mockResolvedValue({ _scroll_id: 'z', hits: { hits: [] } }); + client.search = jest.fn().mockReturnValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + _scroll_id: 'x', + hits: { hits: _.cloneDeep(batch) }, + }) + ); + client.scroll = jest.fn().mockReturnValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + _scroll_id: 'z', + hits: { hits: [] }, + }) + ); - const read = Index.reader(callCluster, index, { + const read = Index.reader(client, index, { batchSize: 100, scrollDuration: '5m', }); @@ -550,23 +569,24 @@ describe('ElasticIndex', () => { count, migrations, }: any) { - const callCluster = jest.fn(async (path: string) => { - if (path === 'indices.get') { - return { - [index]: { mappings }, - }; - } - if (path === 'count') { - return { count, _shards: { success: 1, total: 1 } }; - } - throw new Error(`Unknown command ${path}.`); - }); - const hasMigrations = await Index.migrationsUpToDate(callCluster as any, index, migrations); - return { hasMigrations, callCluster }; + client.indices.get = jest.fn().mockReturnValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + [index]: { mappings }, + }) + ); + client.count = jest.fn().mockReturnValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + count, + _shards: { success: 1, total: 1 }, + }) + ); + + const hasMigrations = await Index.migrationsUpToDate(client, index, migrations); + return { hasMigrations }; } test('is false if the index mappings do not contain migrationVersion', async () => { - const { hasMigrations, callCluster } = await testMigrationsUpToDate({ + const { hasMigrations } = await testMigrationsUpToDate({ index: '.myalias', mappings: { properties: { @@ -578,17 +598,18 @@ describe('ElasticIndex', () => { }); expect(hasMigrations).toBeFalsy(); - expect(callCluster.mock.calls[0]).toEqual([ - 'indices.get', + expect(client.indices.get).toHaveBeenCalledWith( { - ignore: [404], index: '.myalias', }, - ]); + { + ignore: [404], + } + ); }); test('is true if there are no migrations defined', async () => { - const { hasMigrations, callCluster } = await testMigrationsUpToDate({ + const { hasMigrations } = await testMigrationsUpToDate({ index: '.myalias', mappings: { properties: { @@ -604,12 +625,11 @@ describe('ElasticIndex', () => { }); expect(hasMigrations).toBeTruthy(); - expect(callCluster).toHaveBeenCalled(); - expect(callCluster.mock.calls[0][0]).toEqual('indices.get'); + expect(client.indices.get).toHaveBeenCalledTimes(1); }); test('is true if there are no documents out of date', async () => { - const { hasMigrations, callCluster } = await testMigrationsUpToDate({ + const { hasMigrations } = await testMigrationsUpToDate({ index: '.myalias', mappings: { properties: { @@ -625,13 +645,12 @@ describe('ElasticIndex', () => { }); expect(hasMigrations).toBeTruthy(); - expect(callCluster).toHaveBeenCalled(); - expect(callCluster.mock.calls[0][0]).toEqual('indices.get'); - expect(callCluster.mock.calls[1][0]).toEqual('count'); + expect(client.indices.get).toHaveBeenCalledTimes(1); + expect(client.count).toHaveBeenCalledTimes(1); }); test('is false if there are documents out of date', async () => { - const { hasMigrations, callCluster } = await testMigrationsUpToDate({ + const { hasMigrations } = await testMigrationsUpToDate({ index: '.myalias', mappings: { properties: { @@ -647,12 +666,12 @@ describe('ElasticIndex', () => { }); expect(hasMigrations).toBeFalsy(); - expect(callCluster.mock.calls[0][0]).toEqual('indices.get'); - expect(callCluster.mock.calls[1][0]).toEqual('count'); + expect(client.indices.get).toHaveBeenCalledTimes(1); + expect(client.count).toHaveBeenCalledTimes(1); }); test('counts docs that are out of date', async () => { - const { callCluster } = await testMigrationsUpToDate({ + await testMigrationsUpToDate({ index: '.myalias', mappings: { properties: { @@ -686,23 +705,20 @@ describe('ElasticIndex', () => { }; } - expect(callCluster.mock.calls[1]).toEqual([ - 'count', - { - body: { - query: { - bool: { - should: [ - shouldClause('dashy', '23.2.5'), - shouldClause('bashy', '99.9.3'), - shouldClause('flashy', '3.4.5'), - ], - }, + expect(client.count).toHaveBeenCalledWith({ + body: { + query: { + bool: { + should: [ + shouldClause('dashy', '23.2.5'), + shouldClause('bashy', '99.9.3'), + shouldClause('flashy', '3.4.5'), + ], }, }, - index: '.myalias', }, - ]); + index: '.myalias', + }); }); }); }); diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.ts b/src/core/server/saved_objects/migrations/core/elastic_index.ts index e87c3e3ff0d64..d5093bfd8dc42 100644 --- a/src/core/server/saved_objects/migrations/core/elastic_index.ts +++ b/src/core/server/saved_objects/migrations/core/elastic_index.ts @@ -23,9 +23,12 @@ */ import _ from 'lodash'; +import { MigrationEsClient } from './migration_es_client'; +import { CountResponse, SearchResponse } from '../../../elasticsearch'; import { IndexMapping } from '../../mappings'; import { SavedObjectsMigrationVersion } from '../../types'; -import { AliasAction, CallCluster, NotFound, RawDoc, ShardsInfo } from './call_cluster'; +import { AliasAction, RawDoc, ShardsInfo } from './call_cluster'; +import { SavedObjectsRawDocSource } from '../../serialization'; const settings = { number_of_shards: 1, auto_expand_replicas: '0-1' }; @@ -40,13 +43,10 @@ export interface FullIndexInfo { * A slight enhancement to indices.get, that adds indexName, and validates that the * index mappings are somewhat what we expect. */ -export async function fetchInfo(callCluster: CallCluster, index: string): Promise { - const result = await callCluster('indices.get', { - ignore: [404], - index, - }); +export async function fetchInfo(client: MigrationEsClient, index: string): Promise { + const { body, statusCode } = await client.indices.get({ index }, { ignore: [404] }); - if ((result as NotFound).status === 404) { + if (statusCode === 404) { return { aliases: {}, exists: false, @@ -55,7 +55,7 @@ export async function fetchInfo(callCluster: CallCluster, index: string): Promis }; } - const [indexName, indexInfo] = Object.entries(result)[0]; + const [indexName, indexInfo] = Object.entries(body)[0]; return assertIsSupportedIndex({ ...indexInfo, exists: true, indexName }); } @@ -71,7 +71,7 @@ export async function fetchInfo(callCluster: CallCluster, index: string): Promis * @prop {string} scrollDuration - The scroll duration used for scrolling through the index */ export function reader( - callCluster: CallCluster, + client: MigrationEsClient, index: string, { batchSize = 10, scrollDuration = '15m' }: { batchSize: number; scrollDuration: string } ) { @@ -80,19 +80,24 @@ export function reader( const nextBatch = () => scrollId !== undefined - ? callCluster('scroll', { scroll, scrollId }) - : callCluster('search', { body: { size: batchSize }, index, scroll }); - - const close = async () => scrollId && (await callCluster('clearScroll', { scrollId })); + ? client.scroll>({ + scroll, + scroll_id: scrollId, + }) + : client.search>({ + body: { size: batchSize }, + index, + scroll, + }); + + const close = async () => scrollId && (await client.clearScroll({ scroll_id: scrollId })); return async function read() { const result = await nextBatch(); - assertResponseIncludeAllShards(result); - - const docs = result.hits.hits; - - scrollId = result._scroll_id; + assertResponseIncludeAllShards(result.body); + scrollId = result.body._scroll_id; + const docs = result.body.hits.hits; if (!docs.length) { await close(); } @@ -109,8 +114,8 @@ export function reader( * @param {string} index * @param {RawDoc[]} docs */ -export async function write(callCluster: CallCluster, index: string, docs: RawDoc[]) { - const result = await callCluster('bulk', { +export async function write(client: MigrationEsClient, index: string, docs: RawDoc[]) { + const { body } = await client.bulk({ body: docs.reduce((acc: object[], doc: RawDoc) => { acc.push({ index: { @@ -125,7 +130,7 @@ export async function write(callCluster: CallCluster, index: string, docs: RawDo }, []), }); - const err = _.find(result.items, 'index.error.reason'); + const err = _.find(body.items, 'index.error.reason'); if (!err) { return; @@ -150,15 +155,15 @@ export async function write(callCluster: CallCluster, index: string, docs: RawDo * @param {SavedObjectsMigrationVersion} migrationVersion - The latest versions of the migrations */ export async function migrationsUpToDate( - callCluster: CallCluster, + client: MigrationEsClient, index: string, migrationVersion: SavedObjectsMigrationVersion, retryCount: number = 10 ): Promise { try { - const indexInfo = await fetchInfo(callCluster, index); + const indexInfo = await fetchInfo(client, index); - if (!_.get(indexInfo, 'mappings.properties.migrationVersion')) { + if (!indexInfo.mappings.properties?.migrationVersion) { return false; } @@ -167,7 +172,7 @@ export async function migrationsUpToDate( return true; } - const response = await callCluster('count', { + const { body } = await client.count({ body: { query: { bool: { @@ -175,7 +180,11 @@ export async function migrationsUpToDate( bool: { must: [ { exists: { field: type } }, - { bool: { must_not: { term: { [`migrationVersion.${type}`]: latestVersion } } } }, + { + bool: { + must_not: { term: { [`migrationVersion.${type}`]: latestVersion } }, + }, + }, ], }, })), @@ -185,9 +194,9 @@ export async function migrationsUpToDate( index, }); - assertResponseIncludeAllShards(response); + assertResponseIncludeAllShards(body); - return response.count === 0; + return body.count === 0; } catch (e) { // retry for Service Unavailable if (e.status !== 503 || retryCount === 0) { @@ -196,23 +205,23 @@ export async function migrationsUpToDate( await new Promise((r) => setTimeout(r, 1000)); - return await migrationsUpToDate(callCluster, index, migrationVersion, retryCount - 1); + return await migrationsUpToDate(client, index, migrationVersion, retryCount - 1); } } export async function createIndex( - callCluster: CallCluster, + client: MigrationEsClient, index: string, mappings?: IndexMapping ) { - await callCluster('indices.create', { + await client.indices.create({ body: { mappings, settings }, index, }); } -export async function deleteIndex(callCluster: CallCluster, index: string) { - await callCluster('indices.delete', { index }); +export async function deleteIndex(client: MigrationEsClient, index: string) { + await client.indices.delete({ index }); } /** @@ -225,20 +234,20 @@ export async function deleteIndex(callCluster: CallCluster, index: string) { * @param {string} alias - The name of the index being converted to an alias */ export async function convertToAlias( - callCluster: CallCluster, + client: MigrationEsClient, info: FullIndexInfo, alias: string, batchSize: number, script?: string ) { - await callCluster('indices.create', { + await client.indices.create({ body: { mappings: info.mappings, settings }, index: info.indexName, }); - await reindex(callCluster, alias, info.indexName, batchSize, script); + await reindex(client, alias, info.indexName, batchSize, script); - await claimAlias(callCluster, info.indexName, alias, [{ remove_index: { index: alias } }]); + await claimAlias(client, info.indexName, alias, [{ remove_index: { index: alias } }]); } /** @@ -252,22 +261,22 @@ export async function convertToAlias( * @param {AliasAction[]} aliasActions - Optional actions to be added to the updateAliases call */ export async function claimAlias( - callCluster: CallCluster, + client: MigrationEsClient, index: string, alias: string, aliasActions: AliasAction[] = [] ) { - const result = await callCluster('indices.getAlias', { ignore: [404], name: alias }); - const aliasInfo = (result as NotFound).status === 404 ? {} : result; + const { body, statusCode } = await client.indices.getAlias({ name: alias }, { ignore: [404] }); + const aliasInfo = statusCode === 404 ? {} : body; const removeActions = Object.keys(aliasInfo).map((key) => ({ remove: { index: key, alias } })); - await callCluster('indices.updateAliases', { + await client.indices.updateAliases({ body: { actions: aliasActions.concat(removeActions).concat({ add: { index, alias } }), }, }); - await callCluster('indices.refresh', { index }); + await client.indices.refresh({ index }); } /** @@ -318,7 +327,7 @@ function assertResponseIncludeAllShards({ _shards }: { _shards: ShardsInfo }) { * Reindexes from source to dest, polling for the reindex completion. */ async function reindex( - callCluster: CallCluster, + client: MigrationEsClient, source: string, dest: string, batchSize: number, @@ -329,7 +338,7 @@ async function reindex( // polling interval, as the request is fairly efficent, and we don't // want to block index migrations for too long on this. const pollInterval = 250; - const { task } = await callCluster('reindex', { + const { body: reindexBody } = await client.reindex({ body: { dest: { index: dest }, source: { index: source, size: batchSize }, @@ -341,23 +350,25 @@ async function reindex( : undefined, }, refresh: true, - waitForCompletion: false, + wait_for_completion: false, }); + const task = reindexBody.task; + let completed = false; while (!completed) { await new Promise((r) => setTimeout(r, pollInterval)); - completed = await callCluster('tasks.get', { - taskId: task, - }).then((result) => { - if (result.error) { - const e = result.error; - throw new Error(`Re-index failed [${e.type}] ${e.reason} :: ${JSON.stringify(e)}`); - } - - return result.completed; + const { body } = await client.tasks.get({ + task_id: task, }); + + if (body.error) { + const e = body.error; + throw new Error(`Re-index failed [${e.type}] ${e.reason} :: ${JSON.stringify(e)}`); + } + + completed = body.completed; } } diff --git a/src/core/server/saved_objects/migrations/core/index.ts b/src/core/server/saved_objects/migrations/core/index.ts index f7274740ea5fe..c9d3d2a71c9ad 100644 --- a/src/core/server/saved_objects/migrations/core/index.ts +++ b/src/core/server/saved_objects/migrations/core/index.ts @@ -23,3 +23,4 @@ export { buildActiveMappings } from './build_active_mappings'; export { CallCluster } from './call_cluster'; export { LogFn, SavedObjectsMigrationLogger } from './migration_logger'; export { MigrationResult, MigrationStatus } from './migration_coordinator'; +export { createMigrationEsClient, MigrationEsClient } from './migration_es_client'; diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts index f8b203bf66d6a..78601d033f8d8 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts @@ -18,18 +18,22 @@ */ import _ from 'lodash'; +import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; import { SavedObjectUnsanitizedDoc, SavedObjectsSerializer } from '../../serialization'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { IndexMigrator } from './index_migrator'; +import { MigrationOpts } from './migration_context'; import { loggingSystemMock } from '../../../logging/logging_system.mock'; describe('IndexMigrator', () => { - let testOpts: any; + let testOpts: jest.Mocked & { + client: ReturnType; + }; beforeEach(() => { testOpts = { batchSize: 10, - callCluster: jest.fn(), + client: elasticsearchClientMock.createElasticSearchClient(), index: '.kibana', log: loggingSystemMock.create().get(), mappingProperties: {}, @@ -44,15 +48,15 @@ describe('IndexMigrator', () => { }); test('creates the index if it does not exist', async () => { - const { callCluster } = testOpts; + const { client } = testOpts; - testOpts.mappingProperties = { foo: { type: 'long' } }; + testOpts.mappingProperties = { foo: { type: 'long' } as any }; - withIndex(callCluster, { index: { status: 404 }, alias: { status: 404 } }); + withIndex(client, { index: { statusCode: 404 }, alias: { statusCode: 404 } }); await new IndexMigrator(testOpts).migrate(); - expect(callCluster).toHaveBeenCalledWith('indices.create', { + expect(client.indices.create).toHaveBeenCalledWith({ body: { mappings: { dynamic: 'strict', @@ -91,9 +95,9 @@ describe('IndexMigrator', () => { }); test('returns stats about the migration', async () => { - const { callCluster } = testOpts; + const { client } = testOpts; - withIndex(callCluster, { index: { status: 404 }, alias: { status: 404 } }); + withIndex(client, { index: { statusCode: 404 }, alias: { statusCode: 404 } }); const result = await new IndexMigrator(testOpts).migrate(); @@ -105,9 +109,9 @@ describe('IndexMigrator', () => { }); test('fails if there are multiple root doc types', async () => { - const { callCluster } = testOpts; + const { client } = testOpts; - withIndex(callCluster, { + withIndex(client, { index: { '.kibana_1': { aliases: {}, @@ -129,9 +133,9 @@ describe('IndexMigrator', () => { }); test('fails if root doc type is not "doc"', async () => { - const { callCluster } = testOpts; + const { client } = testOpts; - withIndex(callCluster, { + withIndex(client, { index: { '.kibana_1': { aliases: {}, @@ -152,11 +156,11 @@ describe('IndexMigrator', () => { }); test('retains unknown core field mappings from the previous index', async () => { - const { callCluster } = testOpts; + const { client } = testOpts; - testOpts.mappingProperties = { foo: { type: 'text' } }; + testOpts.mappingProperties = { foo: { type: 'text' } as any }; - withIndex(callCluster, { + withIndex(client, { index: { '.kibana_1': { aliases: {}, @@ -171,7 +175,7 @@ describe('IndexMigrator', () => { await new IndexMigrator(testOpts).migrate(); - expect(callCluster).toHaveBeenCalledWith('indices.create', { + expect(client.indices.create).toHaveBeenCalledWith({ body: { mappings: { dynamic: 'strict', @@ -211,11 +215,11 @@ describe('IndexMigrator', () => { }); test('disables complex field mappings from unknown types in the previous index', async () => { - const { callCluster } = testOpts; + const { client } = testOpts; - testOpts.mappingProperties = { foo: { type: 'text' } }; + testOpts.mappingProperties = { foo: { type: 'text' } as any }; - withIndex(callCluster, { + withIndex(client, { index: { '.kibana_1': { aliases: {}, @@ -230,7 +234,7 @@ describe('IndexMigrator', () => { await new IndexMigrator(testOpts).migrate(); - expect(callCluster).toHaveBeenCalledWith('indices.create', { + expect(client.indices.create).toHaveBeenCalledWith({ body: { mappings: { dynamic: 'strict', @@ -270,31 +274,31 @@ describe('IndexMigrator', () => { }); test('points the alias at the dest index', async () => { - const { callCluster } = testOpts; + const { client } = testOpts; - withIndex(callCluster, { index: { status: 404 }, alias: { status: 404 } }); + withIndex(client, { index: { statusCode: 404 }, alias: { statusCode: 404 } }); await new IndexMigrator(testOpts).migrate(); - expect(callCluster).toHaveBeenCalledWith('indices.create', expect.any(Object)); - expect(callCluster).toHaveBeenCalledWith('indices.updateAliases', { + expect(client.indices.create).toHaveBeenCalledWith(expect.any(Object)); + expect(client.indices.updateAliases).toHaveBeenCalledWith({ body: { actions: [{ add: { alias: '.kibana', index: '.kibana_1' } }] }, }); }); test('removes previous indices from the alias', async () => { - const { callCluster } = testOpts; + const { client } = testOpts; testOpts.documentMigrator.migrationVersion = { dashboard: '2.4.5', }; - withIndex(callCluster, { numOutOfDate: 1 }); + withIndex(client, { numOutOfDate: 1 }); await new IndexMigrator(testOpts).migrate(); - expect(callCluster).toHaveBeenCalledWith('indices.create', expect.any(Object)); - expect(callCluster).toHaveBeenCalledWith('indices.updateAliases', { + expect(client.indices.create).toHaveBeenCalledWith(expect.any(Object)); + expect(client.indices.updateAliases).toHaveBeenCalledWith({ body: { actions: [ { remove: { alias: '.kibana', index: '.kibana_1' } }, @@ -306,7 +310,7 @@ describe('IndexMigrator', () => { test('transforms all docs from the original index', async () => { let count = 0; - const { callCluster } = testOpts; + const { client } = testOpts; const migrateDoc = jest.fn((doc: SavedObjectUnsanitizedDoc) => { return { ...doc, @@ -319,7 +323,7 @@ describe('IndexMigrator', () => { migrate: migrateDoc, }; - withIndex(callCluster, { + withIndex(client, { numOutOfDate: 1, docs: [ [{ _id: 'foo:1', _source: { type: 'foo', foo: { name: 'Bar' } } }], @@ -344,30 +348,27 @@ describe('IndexMigrator', () => { migrationVersion: {}, references: [], }); - const bulkCalls = callCluster.mock.calls.filter(([action]: any) => action === 'bulk'); - expect(bulkCalls.length).toEqual(2); - expect(bulkCalls[0]).toEqual([ - 'bulk', - { - body: [ - { index: { _id: 'foo:1', _index: '.kibana_2' } }, - { foo: { name: 1 }, type: 'foo', migrationVersion: {}, references: [] }, - ], - }, - ]); - expect(bulkCalls[1]).toEqual([ - 'bulk', - { - body: [ - { index: { _id: 'foo:2', _index: '.kibana_2' } }, - { foo: { name: 2 }, type: 'foo', migrationVersion: {}, references: [] }, - ], - }, - ]); + + expect(client.bulk).toHaveBeenCalledTimes(2); + expect(client.bulk).toHaveBeenNthCalledWith(1, { + body: [ + { index: { _id: 'foo:1', _index: '.kibana_2' } }, + { foo: { name: 1 }, type: 'foo', migrationVersion: {}, references: [] }, + ], + }); + expect(client.bulk).toHaveBeenNthCalledWith(2, { + body: [ + { index: { _id: 'foo:2', _index: '.kibana_2' } }, + { foo: { name: 2 }, type: 'foo', migrationVersion: {}, references: [] }, + ], + }); }); }); -function withIndex(callCluster: jest.Mock, opts: any = {}) { +function withIndex( + client: ReturnType, + opts: any = {} +) { const defaultIndex = { '.kibana_1': { aliases: { '.kibana': {} }, @@ -386,39 +387,56 @@ function withIndex(callCluster: jest.Mock, opts: any = {}) { const { alias = defaultAlias } = opts; const { index = defaultIndex } = opts; const { docs = [] } = opts; - const searchResult = (i: number) => - Promise.resolve({ - _scroll_id: i, - _shards: { - successful: 1, - total: 1, - }, - hits: { - hits: docs[i] || [], - }, - }); + const searchResult = (i: number) => ({ + _scroll_id: i, + _shards: { + successful: 1, + total: 1, + }, + hits: { + hits: docs[i] || [], + }, + }); let scrollCallCounter = 1; - callCluster.mockImplementation((method) => { - if (method === 'indices.get') { - return Promise.resolve(index); - } else if (method === 'indices.getAlias') { - return Promise.resolve(alias); - } else if (method === 'reindex') { - return Promise.resolve({ task: 'zeid', _shards: { successful: 1, total: 1 } }); - } else if (method === 'tasks.get') { - return Promise.resolve({ completed: true }); - } else if (method === 'search') { - return searchResult(0); - } else if (method === 'bulk') { - return Promise.resolve({ items: [] }); - } else if (method === 'count') { - return Promise.resolve({ count: numOutOfDate, _shards: { successful: 1, total: 1 } }); - } else if (method === 'scroll' && scrollCallCounter <= docs.length) { + client.indices.get.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(index, { + statusCode: index.statusCode, + }) + ); + client.indices.getAlias.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(alias, { + statusCode: index.statusCode, + }) + ); + client.reindex.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + task: 'zeid', + _shards: { successful: 1, total: 1 }, + }) + ); + client.tasks.get.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ completed: true }) + ); + client.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(searchResult(0)) + ); + client.bulk.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ items: [] }) + ); + client.count.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + count: numOutOfDate, + _shards: { successful: 1, total: 1 }, + }) + ); + client.scroll.mockImplementation(() => { + if (scrollCallCounter <= docs.length) { const result = searchResult(scrollCallCounter); scrollCallCounter++; - return result; + return elasticsearchClientMock.createSuccessTransportRequestPromise(result); } + return elasticsearchClientMock.createSuccessTransportRequestPromise({}); }); } diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.ts b/src/core/server/saved_objects/migrations/core/index_migrator.ts index e588eb7877322..ceca27fa87723 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - import { diffMappings } from './build_active_mappings'; import * as Index from './elastic_index'; import { migrateRawDocs } from './migrate_raw_docs'; @@ -71,11 +70,11 @@ export class IndexMigrator { * Determines what action the migration system needs to take (none, patch, migrate). */ async function requiresMigration(context: Context): Promise { - const { callCluster, alias, documentMigrator, dest, log } = context; + const { client, alias, documentMigrator, dest, log } = context; // Have all of our known migrations been run against the index? const hasMigrations = await Index.migrationsUpToDate( - callCluster, + client, alias, documentMigrator.migrationVersion ); @@ -85,7 +84,7 @@ async function requiresMigration(context: Context): Promise { } // Is our index aliased? - const refreshedSource = await Index.fetchInfo(callCluster, alias); + const refreshedSource = await Index.fetchInfo(client, alias); if (!refreshedSource.aliases[alias]) { return true; @@ -109,19 +108,19 @@ async function requiresMigration(context: Context): Promise { */ async function migrateIndex(context: Context): Promise { const startTime = Date.now(); - const { callCluster, alias, source, dest, log } = context; + const { client, alias, source, dest, log } = context; await deleteIndexTemplates(context); log.info(`Creating index ${dest.indexName}.`); - await Index.createIndex(callCluster, dest.indexName, dest.mappings); + await Index.createIndex(client, dest.indexName, dest.mappings); await migrateSourceToDest(context); log.info(`Pointing alias ${alias} to ${dest.indexName}.`); - await Index.claimAlias(callCluster, dest.indexName, alias); + await Index.claimAlias(client, dest.indexName, alias); const result: MigrationResult = { status: 'migrated', @@ -139,12 +138,12 @@ async function migrateIndex(context: Context): Promise { * If the obsoleteIndexTemplatePattern option is specified, this will delete any index templates * that match it. */ -async function deleteIndexTemplates({ callCluster, log, obsoleteIndexTemplatePattern }: Context) { +async function deleteIndexTemplates({ client, log, obsoleteIndexTemplatePattern }: Context) { if (!obsoleteIndexTemplatePattern) { return; } - const templates = await callCluster('cat.templates', { + const { body: templates } = await client.cat.templates>({ format: 'json', name: obsoleteIndexTemplatePattern, }); @@ -157,7 +156,7 @@ async function deleteIndexTemplates({ callCluster, log, obsoleteIndexTemplatePat log.info(`Removing index templates: ${templateNames}`); - return Promise.all(templateNames.map((name) => callCluster('indices.deleteTemplate', { name }))); + return Promise.all(templateNames.map((name) => client.indices.deleteTemplate({ name }))); } /** @@ -166,7 +165,7 @@ async function deleteIndexTemplates({ callCluster, log, obsoleteIndexTemplatePat * a situation where the alias moves out from under us as we're migrating docs. */ async function migrateSourceToDest(context: Context) { - const { callCluster, alias, dest, source, batchSize } = context; + const { client, alias, dest, source, batchSize } = context; const { scrollDuration, documentMigrator, log, serializer } = context; if (!source.exists) { @@ -176,10 +175,10 @@ async function migrateSourceToDest(context: Context) { if (!source.aliases[alias]) { log.info(`Reindexing ${alias} to ${source.indexName}`); - await Index.convertToAlias(callCluster, source, alias, batchSize, context.convertToAliasScript); + await Index.convertToAlias(client, source, alias, batchSize, context.convertToAliasScript); } - const read = Index.reader(callCluster, source.indexName, { batchSize, scrollDuration }); + const read = Index.reader(client, source.indexName, { batchSize, scrollDuration }); log.info(`Migrating ${source.indexName} saved objects to ${dest.indexName}`); @@ -193,7 +192,7 @@ async function migrateSourceToDest(context: Context) { log.debug(`Migrating saved objects ${docs.map((d) => d._id).join(', ')}`); await Index.write( - callCluster, + client, dest.indexName, await migrateRawDocs(serializer, documentMigrator.migrate, docs, log) ); diff --git a/src/core/server/saved_objects/migrations/core/migration_context.ts b/src/core/server/saved_objects/migrations/core/migration_context.ts index adf1851a1aa75..0ea362d65623e 100644 --- a/src/core/server/saved_objects/migrations/core/migration_context.ts +++ b/src/core/server/saved_objects/migrations/core/migration_context.ts @@ -25,6 +25,7 @@ */ import { Logger } from 'src/core/server/logging'; +import { MigrationEsClient } from './migration_es_client'; import { SavedObjectsSerializer } from '../../serialization'; import { SavedObjectsTypeMappingDefinitions, @@ -32,16 +33,15 @@ import { IndexMapping, } from '../../mappings'; import { buildActiveMappings } from './build_active_mappings'; -import { CallCluster } from './call_cluster'; import { VersionedTransformer } from './document_migrator'; -import { fetchInfo, FullIndexInfo } from './elastic_index'; +import * as Index from './elastic_index'; import { SavedObjectsMigrationLogger, MigrationLogger } from './migration_logger'; export interface MigrationOpts { batchSize: number; pollInterval: number; scrollDuration: string; - callCluster: CallCluster; + client: MigrationEsClient; index: string; log: Logger; mappingProperties: SavedObjectsTypeMappingDefinitions; @@ -56,11 +56,14 @@ export interface MigrationOpts { obsoleteIndexTemplatePattern?: string; } +/** + * @internal + */ export interface Context { - callCluster: CallCluster; + client: MigrationEsClient; alias: string; - source: FullIndexInfo; - dest: FullIndexInfo; + source: Index.FullIndexInfo; + dest: Index.FullIndexInfo; documentMigrator: VersionedTransformer; log: SavedObjectsMigrationLogger; batchSize: number; @@ -76,13 +79,13 @@ export interface Context { * and various info needed to migrate the source index. */ export async function migrationContext(opts: MigrationOpts): Promise { - const { log, callCluster } = opts; + const { log, client } = opts; const alias = opts.index; - const source = createSourceContext(await fetchInfo(callCluster, alias), alias); + const source = createSourceContext(await Index.fetchInfo(client, alias), alias); const dest = createDestContext(source, alias, opts.mappingProperties); return { - callCluster, + client, alias, source, dest, @@ -97,7 +100,7 @@ export async function migrationContext(opts: MigrationOpts): Promise { }; } -function createSourceContext(source: FullIndexInfo, alias: string) { +function createSourceContext(source: Index.FullIndexInfo, alias: string) { if (source.exists && source.indexName === alias) { return { ...source, @@ -109,10 +112,10 @@ function createSourceContext(source: FullIndexInfo, alias: string) { } function createDestContext( - source: FullIndexInfo, + source: Index.FullIndexInfo, alias: string, typeMappingDefinitions: SavedObjectsTypeMappingDefinitions -): FullIndexInfo { +): Index.FullIndexInfo { const targetMappings = disableUnknownTypeMappingFields( buildActiveMappings(typeMappingDefinitions), source.mappings diff --git a/src/core/server/saved_objects/migrations/core/migration_es_client.test.mock.ts b/src/core/server/saved_objects/migrations/core/migration_es_client.test.mock.ts new file mode 100644 index 0000000000000..8ebed25d87cba --- /dev/null +++ b/src/core/server/saved_objects/migrations/core/migration_es_client.test.mock.ts @@ -0,0 +1,22 @@ +/* + * 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. + */ +export const migrationRetryCallClusterMock = jest.fn((fn) => fn()); +jest.doMock('../../../elasticsearch/client/retry_call_cluster', () => ({ + migrationRetryCallCluster: migrationRetryCallClusterMock, +})); diff --git a/src/core/server/saved_objects/migrations/core/migration_es_client.test.ts b/src/core/server/saved_objects/migrations/core/migration_es_client.test.ts new file mode 100644 index 0000000000000..40c06677c4a5a --- /dev/null +++ b/src/core/server/saved_objects/migrations/core/migration_es_client.test.ts @@ -0,0 +1,65 @@ +/* + * 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 { migrationRetryCallClusterMock } from './migration_es_client.test.mock'; + +import { createMigrationEsClient, MigrationEsClient } from './migration_es_client'; +import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; +import { loggerMock } from '../../../logging/logger.mock'; +import { SavedObjectsErrorHelpers } from '../../service/lib/errors'; + +describe('MigrationEsClient', () => { + let client: ReturnType; + let migrationEsClient: MigrationEsClient; + + beforeEach(() => { + client = elasticsearchClientMock.createElasticSearchClient(); + migrationEsClient = createMigrationEsClient(client, loggerMock.create()); + migrationRetryCallClusterMock.mockClear(); + }); + + it('delegates call to ES client method', async () => { + expect(migrationEsClient.bulk).toStrictEqual(expect.any(Function)); + await migrationEsClient.bulk({ body: [] }); + expect(client.bulk).toHaveBeenCalledTimes(1); + }); + + it('wraps a method call in migrationRetryCallClusterMock', async () => { + await migrationEsClient.bulk({ body: [] }); + expect(migrationRetryCallClusterMock).toHaveBeenCalledTimes(1); + }); + + it('sets maxRetries: 0 to delegate retry logic to migrationRetryCallCluster', async () => { + expect(migrationEsClient.bulk).toStrictEqual(expect.any(Function)); + await migrationEsClient.bulk({ body: [] }); + expect(client.bulk).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ maxRetries: 0 }) + ); + }); + + it('do not transform elasticsearch errors into saved objects errors', async () => { + expect.assertions(1); + client.bulk = jest.fn().mockRejectedValue(new Error('reason')); + try { + await migrationEsClient.bulk({ body: [] }); + } catch (e) { + expect(SavedObjectsErrorHelpers.isSavedObjectsClientError(e)).toBe(false); + } + }); +}); diff --git a/src/core/server/saved_objects/migrations/core/migration_es_client.ts b/src/core/server/saved_objects/migrations/core/migration_es_client.ts new file mode 100644 index 0000000000000..ff859057f8fe8 --- /dev/null +++ b/src/core/server/saved_objects/migrations/core/migration_es_client.ts @@ -0,0 +1,90 @@ +/* + * 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 type { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; +import { get } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; + +import { ElasticsearchClient } from '../../../elasticsearch'; +import { migrationRetryCallCluster } from '../../../elasticsearch/client/retry_call_cluster'; +import { Logger } from '../../../logging'; + +const methods = [ + 'bulk', + 'cat.templates', + 'clearScroll', + 'count', + 'indices.create', + 'indices.delete', + 'indices.deleteTemplate', + 'indices.get', + 'indices.getAlias', + 'indices.refresh', + 'indices.updateAliases', + 'reindex', + 'search', + 'scroll', + 'tasks.get', +] as const; + +type MethodName = typeof methods[number]; + +export interface MigrationEsClient { + bulk: ElasticsearchClient['bulk']; + cat: { + templates: ElasticsearchClient['cat']['templates']; + }; + clearScroll: ElasticsearchClient['clearScroll']; + count: ElasticsearchClient['count']; + indices: { + create: ElasticsearchClient['indices']['create']; + delete: ElasticsearchClient['indices']['delete']; + deleteTemplate: ElasticsearchClient['indices']['deleteTemplate']; + get: ElasticsearchClient['indices']['get']; + getAlias: ElasticsearchClient['indices']['getAlias']; + refresh: ElasticsearchClient['indices']['refresh']; + updateAliases: ElasticsearchClient['indices']['updateAliases']; + }; + reindex: ElasticsearchClient['reindex']; + search: ElasticsearchClient['search']; + scroll: ElasticsearchClient['scroll']; + tasks: { + get: ElasticsearchClient['tasks']['get']; + }; +} + +export function createMigrationEsClient( + client: ElasticsearchClient, + log: Logger, + delay?: number +): MigrationEsClient { + return methods.reduce((acc: MigrationEsClient, key: MethodName) => { + set(acc, key, async (params?: unknown, options?: TransportRequestOptions) => { + const fn = get(client, key); + if (!fn) { + throw new Error(`unknown ElasticsearchClient client method [${key}]`); + } + return await migrationRetryCallCluster( + () => fn(params, { maxRetries: 0, ...options }), + log, + delay + ); + }); + return acc; + }, {} as MigrationEsClient); +} diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts index 01b0d1cd0ba3a..c3ed97a89af80 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts @@ -18,6 +18,7 @@ */ import { take } from 'rxjs/operators'; +import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; import { KibanaMigratorOptions, KibanaMigrator } from './kibana_migrator'; import { loggingSystemMock } from '../../../logging/logging_system.mock'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; @@ -66,26 +67,44 @@ describe('KibanaMigrator', () => { describe('runMigrations', () => { it('only runs migrations once if called multiple times', async () => { const options = mockOptions(); - const clusterStub = jest.fn(() => ({ status: 404 })); - options.callCluster = clusterStub; + options.client.cat.templates.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise( + { templates: [] }, + { statusCode: 404 } + ) + ); + options.client.indices.get.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + ); + options.client.indices.getAlias.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + ); + const migrator = new KibanaMigrator(options); + await migrator.runMigrations(); await migrator.runMigrations(); - // callCluster with "cat.templates" is called by "deleteIndexTemplates" function - // and should only be done once - const callClusterCommands = clusterStub.mock.calls - .map(([callClusterPath]) => callClusterPath) - .filter((callClusterPath) => callClusterPath === 'cat.templates'); - expect(callClusterCommands.length).toBe(1); + expect(options.client.cat.templates).toHaveBeenCalledTimes(1); }); it('emits results on getMigratorResult$()', async () => { const options = mockOptions(); - const clusterStub = jest.fn(() => ({ status: 404 })); - options.callCluster = clusterStub; + options.client.cat.templates.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise( + { templates: [] }, + { statusCode: 404 } + ) + ); + options.client.indices.get.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + ); + options.client.indices.getAlias.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + ); + const migrator = new KibanaMigrator(options); const migratorStatus = migrator.getStatus$().pipe(take(3)).toPromise(); await migrator.runMigrations(); @@ -107,9 +126,12 @@ describe('KibanaMigrator', () => { }); }); -function mockOptions(): KibanaMigratorOptions { - const callCluster = jest.fn(); - return { +type MockedOptions = KibanaMigratorOptions & { + client: ReturnType; +}; + +const mockOptions = () => { + const options: MockedOptions = { logger: loggingSystemMock.create().get(), kibanaVersion: '8.2.3', savedObjectValidations: {}, @@ -148,6 +170,7 @@ function mockOptions(): KibanaMigratorOptions { scrollDuration: '10m', skip: false, }, - callCluster, + client: elasticsearchClientMock.createElasticSearchClient(), }; -} + return options; +}; diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts index 69b57a498936e..85b9099308807 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -24,25 +24,21 @@ import { KibanaConfigType } from 'src/core/server/kibana_config'; import { BehaviorSubject } from 'rxjs'; + import { Logger } from '../../../logging'; import { IndexMapping, SavedObjectsTypeMappingDefinitions } from '../../mappings'; import { SavedObjectUnsanitizedDoc, SavedObjectsSerializer } from '../../serialization'; import { docValidator, PropertyValidators } from '../../validation'; -import { - buildActiveMappings, - CallCluster, - IndexMigrator, - MigrationResult, - MigrationStatus, -} from '../core'; +import { buildActiveMappings, IndexMigrator, MigrationResult, MigrationStatus } from '../core'; import { DocumentMigrator, VersionedTransformer } from '../core/document_migrator'; +import { MigrationEsClient } from '../core/'; import { createIndexMap } from '../core/build_index_map'; import { SavedObjectsMigrationConfigType } from '../../saved_objects_config'; import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { SavedObjectsType } from '../../types'; export interface KibanaMigratorOptions { - callCluster: CallCluster; + client: MigrationEsClient; typeRegistry: ISavedObjectTypeRegistry; savedObjectsConfig: SavedObjectsMigrationConfigType; kibanaConfig: KibanaConfigType; @@ -62,7 +58,7 @@ export interface KibanaMigratorStatus { * Manages the shape of mappings and documents in the Kibana index. */ export class KibanaMigrator { - private readonly callCluster: CallCluster; + private readonly client: MigrationEsClient; private readonly savedObjectsConfig: SavedObjectsMigrationConfigType; private readonly documentMigrator: VersionedTransformer; private readonly kibanaConfig: KibanaConfigType; @@ -80,7 +76,7 @@ export class KibanaMigrator { * Creates an instance of KibanaMigrator. */ constructor({ - callCluster, + client, typeRegistry, kibanaConfig, savedObjectsConfig, @@ -88,7 +84,7 @@ export class KibanaMigrator { kibanaVersion, logger, }: KibanaMigratorOptions) { - this.callCluster = callCluster; + this.client = client; this.kibanaConfig = kibanaConfig; this.savedObjectsConfig = savedObjectsConfig; this.typeRegistry = typeRegistry; @@ -153,7 +149,7 @@ export class KibanaMigrator { const migrators = Object.keys(indexMap).map((index) => { return new IndexMigrator({ batchSize: this.savedObjectsConfig.batchSize, - callCluster: this.callCluster, + client: this.client, documentMigrator: this.documentMigrator, index, log: this.log, diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index e8b2cf0b583b1..8df6a07318c45 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -25,18 +25,20 @@ import { } from './saved_objects_service.test.mocks'; import { BehaviorSubject } from 'rxjs'; import { ByteSizeValue } from '@kbn/config-schema'; +import { errors as esErrors } from '@elastic/elasticsearch'; + import { SavedObjectsService } from './saved_objects_service'; import { mockCoreContext } from '../core_context.mock'; -import * as legacyElasticsearch from 'elasticsearch'; import { Env } from '../config'; import { configServiceMock } from '../mocks'; import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service.mock'; +import { elasticsearchClientMock } from '../elasticsearch/client/mocks'; import { legacyServiceMock } from '../legacy/legacy_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; +import { httpServerMock } from '../http/http_server.mocks'; import { SavedObjectsClientFactoryProvider } from './service/lib'; import { NodesVersionCompatibility } from '../elasticsearch/version_check/ensure_es_version'; import { SavedObjectsRepository } from './service/lib/repository'; -import { KibanaRequest } from '../http'; jest.mock('./service/lib/repository'); @@ -70,7 +72,7 @@ describe('SavedObjectsService', () => { const createStartDeps = (pluginsInitialized: boolean = true) => { return { pluginsInitialized, - elasticsearch: elasticsearchServiceMock.createStart(), + elasticsearch: elasticsearchServiceMock.createInternalStart(), }; }; @@ -161,26 +163,27 @@ describe('SavedObjectsService', () => { }); describe('#start()', () => { - it('creates a KibanaMigrator which retries NoConnections errors from callAsInternalUser', async () => { + it('creates a KibanaMigrator which retries NoLivingConnectionsError errors from ES client', async () => { const coreContext = createCoreContext(); const soService = new SavedObjectsService(coreContext); const coreSetup = createSetupDeps(); const coreStart = createStartDeps(); - let i = 0; - coreStart.elasticsearch.legacy.client.callAsInternalUser = jest + coreStart.elasticsearch.client.asInternalUser.indices.create = jest .fn() - .mockImplementation(() => - i++ <= 2 - ? Promise.reject(new legacyElasticsearch.errors.NoConnections()) - : Promise.resolve('success') + .mockImplementationOnce(() => + Promise.reject(new esErrors.NoLivingConnectionsError('reason', {} as any)) + ) + .mockImplementationOnce(() => + elasticsearchClientMock.createSuccessTransportRequestPromise('success') ); await soService.setup(coreSetup); await soService.start(coreStart, 1); - return expect(KibanaMigratorMock.mock.calls[0][0].callCluster()).resolves.toMatch('success'); + const response = await KibanaMigratorMock.mock.calls[0][0].client.indices.create(); + return expect(response.body).toBe('success'); }); it('skips KibanaMigrator migrations when pluginsInitialized=false', async () => { @@ -291,22 +294,15 @@ describe('SavedObjectsService', () => { const coreStart = createStartDeps(); const { createScopedRepository } = await soService.start(coreStart); - const req = {} as KibanaRequest; + const req = httpServerMock.createKibanaRequest(); createScopedRepository(req); - expect(coreStart.elasticsearch.legacy.client.asScoped).toHaveBeenCalledWith(req); - - const [ - { - value: { callAsCurrentUser }, - }, - ] = coreStart.elasticsearch.legacy.client.asScoped.mock.results; + expect(coreStart.elasticsearch.client.asScoped).toHaveBeenCalledWith(req); const [ - [, , , callCluster, includedHiddenTypes], + [, , , , includedHiddenTypes], ] = (SavedObjectsRepository.createRepository as jest.Mocked).mock.calls; - expect(callCluster).toBe(callAsCurrentUser); expect(includedHiddenTypes).toEqual([]); }); @@ -318,7 +314,7 @@ describe('SavedObjectsService', () => { const coreStart = createStartDeps(); const { createScopedRepository } = await soService.start(coreStart); - const req = {} as KibanaRequest; + const req = httpServerMock.createKibanaRequest(); createScopedRepository(req, ['someHiddenType']); const [ @@ -341,11 +337,10 @@ describe('SavedObjectsService', () => { createInternalRepository(); const [ - [, , , callCluster, includedHiddenTypes], + [, , , client, includedHiddenTypes], ] = (SavedObjectsRepository.createRepository as jest.Mocked).mock.calls; - expect(coreStart.elasticsearch.legacy.client.callAsInternalUser).toBe(callCluster); - expect(callCluster).toBe(coreStart.elasticsearch.legacy.client.callAsInternalUser); + expect(coreStart.elasticsearch.client.asInternalUser).toBe(client); expect(includedHiddenTypes).toEqual([]); }); diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index c2d4f49d7ee2a..f05e912b12ad8 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -30,13 +30,12 @@ import { KibanaMigrator, IKibanaMigrator } from './migrations'; import { CoreContext } from '../core_context'; import { LegacyServiceDiscoverPlugins } from '../legacy'; import { - LegacyAPICaller, - ElasticsearchServiceStart, - ILegacyClusterClient, + ElasticsearchClient, + IClusterClient, InternalElasticsearchServiceSetup, + InternalElasticsearchServiceStart, } from '../elasticsearch'; import { KibanaConfigType } from '../kibana_config'; -import { migrationsRetryCallCluster } from '../elasticsearch/legacy'; import { SavedObjectsConfigType, SavedObjectsMigrationConfigType, @@ -57,7 +56,7 @@ import { SavedObjectsSerializer } from './serialization'; import { registerRoutes } from './routes'; import { ServiceStatus } from '../status'; import { calculateStatus$ } from './status'; - +import { createMigrationEsClient } from './migrations/core/'; /** * Saved Objects is Kibana's data persistence mechanism allowing plugins to * use Elasticsearch for storing and querying state. The SavedObjectsServiceSetup API exposes methods @@ -284,7 +283,7 @@ interface WrappedClientFactoryWrapper { /** @internal */ export interface SavedObjectsStartDeps { - elasticsearch: ElasticsearchServiceStart; + elasticsearch: InternalElasticsearchServiceStart; pluginsInitialized?: boolean; } @@ -383,12 +382,12 @@ export class SavedObjectsService .atPath('kibana') .pipe(first()) .toPromise(); - const client = elasticsearch.legacy.client; + const client = elasticsearch.client; const migrator = this.createMigrator( kibanaConfig, this.config.migration, - client, + elasticsearch.client, migrationsRetryDelay ); @@ -434,21 +433,24 @@ export class SavedObjectsService await migrator.runMigrations(); } - const createRepository = (callCluster: LegacyAPICaller, includedHiddenTypes: string[] = []) => { + const createRepository = ( + esClient: ElasticsearchClient, + includedHiddenTypes: string[] = [] + ) => { return SavedObjectsRepository.createRepository( migrator, this.typeRegistry, kibanaConfig.index, - callCluster, + esClient, includedHiddenTypes ); }; const repositoryFactory: SavedObjectsRepositoryFactory = { createInternalRepository: (includedHiddenTypes?: string[]) => - createRepository(client.callAsInternalUser, includedHiddenTypes), + createRepository(client.asInternalUser, includedHiddenTypes), createScopedRepository: (req: KibanaRequest, includedHiddenTypes?: string[]) => - createRepository(client.asScoped(req).callAsCurrentUser, includedHiddenTypes), + createRepository(client.asScoped(req).asCurrentUser, includedHiddenTypes), }; const clientProvider = new SavedObjectsClientProvider({ @@ -484,7 +486,7 @@ export class SavedObjectsService private createMigrator( kibanaConfig: KibanaConfigType, savedObjectsConfig: SavedObjectsMigrationConfigType, - esClient: ILegacyClusterClient, + client: IClusterClient, migrationsRetryDelay?: number ): KibanaMigrator { return new KibanaMigrator({ @@ -494,11 +496,7 @@ export class SavedObjectsService savedObjectsConfig, savedObjectValidations: this.validations, kibanaConfig, - callCluster: migrationsRetryCallCluster( - esClient.callAsInternalUser, - this.logger, - migrationsRetryDelay - ), + client: createMigrationEsClient(client.asInternalUser, this.logger, migrationsRetryDelay), }); } } diff --git a/src/core/server/saved_objects/serialization/index.ts b/src/core/server/saved_objects/serialization/index.ts index f7f4e75704341..812a0770ad988 100644 --- a/src/core/server/saved_objects/serialization/index.ts +++ b/src/core/server/saved_objects/serialization/index.ts @@ -22,5 +22,10 @@ * the raw document format as stored in ElasticSearch. */ -export { SavedObjectUnsanitizedDoc, SavedObjectSanitizedDoc, SavedObjectsRawDoc } from './types'; +export { + SavedObjectUnsanitizedDoc, + SavedObjectSanitizedDoc, + SavedObjectsRawDoc, + SavedObjectsRawDocSource, +} from './types'; export { SavedObjectsSerializer } from './serializer'; diff --git a/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts b/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts index 1fdebd87397eb..623610eebd8d7 100644 --- a/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts +++ b/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts @@ -17,75 +17,93 @@ * under the License. */ -import { errors as esErrors } from 'elasticsearch'; - +import { errors as esErrors } from '@elastic/elasticsearch'; +import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; import { decorateEsError } from './decorate_es_error'; import { SavedObjectsErrorHelpers } from './errors'; describe('savedObjectsClient/decorateEsError', () => { it('always returns the same error it receives', () => { - const error = new Error(); + const error = new esErrors.ResponseError(elasticsearchClientMock.createApiResponse()); expect(decorateEsError(error)).toBe(error); }); - it('makes es.ConnectionFault a SavedObjectsClient/EsUnavailable error', () => { - const error = new esErrors.ConnectionFault(); + it('makes ConnectionError a SavedObjectsClient/EsUnavailable error', () => { + const error = new esErrors.ConnectionError( + 'reason', + elasticsearchClientMock.createApiResponse() + ); expect(SavedObjectsErrorHelpers.isEsUnavailableError(error)).toBe(false); expect(decorateEsError(error)).toBe(error); expect(SavedObjectsErrorHelpers.isEsUnavailableError(error)).toBe(true); }); - it('makes es.ServiceUnavailable a SavedObjectsClient/EsUnavailable error', () => { - const error = new esErrors.ServiceUnavailable(); + it('makes ServiceUnavailable a SavedObjectsClient/EsUnavailable error', () => { + const error = new esErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ statusCode: 503 }) + ); expect(SavedObjectsErrorHelpers.isEsUnavailableError(error)).toBe(false); expect(decorateEsError(error)).toBe(error); expect(SavedObjectsErrorHelpers.isEsUnavailableError(error)).toBe(true); }); - it('makes es.NoConnections a SavedObjectsClient/EsUnavailable error', () => { - const error = new esErrors.NoConnections(); + it('makes NoLivingConnectionsError a SavedObjectsClient/EsUnavailable error', () => { + const error = new esErrors.NoLivingConnectionsError( + 'reason', + elasticsearchClientMock.createApiResponse() + ); expect(SavedObjectsErrorHelpers.isEsUnavailableError(error)).toBe(false); expect(decorateEsError(error)).toBe(error); expect(SavedObjectsErrorHelpers.isEsUnavailableError(error)).toBe(true); }); - it('makes es.RequestTimeout a SavedObjectsClient/EsUnavailable error', () => { - const error = new esErrors.RequestTimeout(); + it('makes TimeoutError a SavedObjectsClient/EsUnavailable error', () => { + const error = new esErrors.TimeoutError('reason', elasticsearchClientMock.createApiResponse()); expect(SavedObjectsErrorHelpers.isEsUnavailableError(error)).toBe(false); expect(decorateEsError(error)).toBe(error); expect(SavedObjectsErrorHelpers.isEsUnavailableError(error)).toBe(true); }); - it('makes es.Conflict a SavedObjectsClient/Conflict error', () => { - const error = new esErrors.Conflict(); + it('makes Conflict a SavedObjectsClient/Conflict error', () => { + const error = new esErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ statusCode: 409 }) + ); expect(SavedObjectsErrorHelpers.isConflictError(error)).toBe(false); expect(decorateEsError(error)).toBe(error); expect(SavedObjectsErrorHelpers.isConflictError(error)).toBe(true); }); - it('makes es.AuthenticationException a SavedObjectsClient/NotAuthorized error', () => { - const error = new esErrors.AuthenticationException(); + it('makes NotAuthorized a SavedObjectsClient/NotAuthorized error', () => { + const error = new esErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ statusCode: 401 }) + ); expect(SavedObjectsErrorHelpers.isNotAuthorizedError(error)).toBe(false); expect(decorateEsError(error)).toBe(error); expect(SavedObjectsErrorHelpers.isNotAuthorizedError(error)).toBe(true); }); - it('makes es.Forbidden a SavedObjectsClient/Forbidden error', () => { - const error = new esErrors.Forbidden(); + it('makes Forbidden a SavedObjectsClient/Forbidden error', () => { + const error = new esErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ statusCode: 403 }) + ); expect(SavedObjectsErrorHelpers.isForbiddenError(error)).toBe(false); expect(decorateEsError(error)).toBe(error); expect(SavedObjectsErrorHelpers.isForbiddenError(error)).toBe(true); }); - it('makes es.RequestEntityTooLarge a SavedObjectsClient/RequestEntityTooLarge error', () => { - const error = new esErrors.RequestEntityTooLarge(); + it('makes RequestEntityTooLarge a SavedObjectsClient/RequestEntityTooLarge error', () => { + const error = new esErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ statusCode: 413 }) + ); expect(SavedObjectsErrorHelpers.isRequestEntityTooLargeError(error)).toBe(false); expect(decorateEsError(error)).toBe(error); expect(SavedObjectsErrorHelpers.isRequestEntityTooLargeError(error)).toBe(true); }); - it('discards es.NotFound errors and returns a generic NotFound error', () => { - const error = new esErrors.NotFound(); + it('discards NotFound errors and returns a generic NotFound error', () => { + const error = new esErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ statusCode: 404 }) + ); expect(SavedObjectsErrorHelpers.isNotFoundError(error)).toBe(false); const genericError = decorateEsError(error); expect(genericError).not.toBe(error); @@ -93,8 +111,10 @@ describe('savedObjectsClient/decorateEsError', () => { expect(SavedObjectsErrorHelpers.isNotFoundError(genericError)).toBe(true); }); - it('makes es.BadRequest a SavedObjectsClient/BadRequest error', () => { - const error = new esErrors.BadRequest(); + it('makes BadRequest a SavedObjectsClient/BadRequest error', () => { + const error = new esErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ statusCode: 400 }) + ); expect(SavedObjectsErrorHelpers.isBadRequestError(error)).toBe(false); expect(decorateEsError(error)).toBe(error); expect(SavedObjectsErrorHelpers.isBadRequestError(error)).toBe(true); @@ -102,10 +122,16 @@ describe('savedObjectsClient/decorateEsError', () => { describe('when es.BadRequest has a reason', () => { it('makes a SavedObjectsClient/esCannotExecuteScriptError error when script context is disabled', () => { - const error = new esErrors.BadRequest(); - (error as Record).body = { - error: { reason: 'cannot execute scripts using [update] context' }, - }; + const error = new esErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ + statusCode: 400, + body: { + error: { + reason: 'cannot execute scripts using [update] context', + }, + }, + }) + ); expect(SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)).toBe(false); expect(decorateEsError(error)).toBe(error); expect(SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)).toBe(true); @@ -113,10 +139,16 @@ describe('savedObjectsClient/decorateEsError', () => { }); it('makes a SavedObjectsClient/esCannotExecuteScriptError error when inline scripts are disabled', () => { - const error = new esErrors.BadRequest(); - (error as Record).body = { - error: { reason: 'cannot execute [inline] scripts' }, - }; + const error = new esErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ + statusCode: 400, + body: { + error: { + reason: 'cannot execute [inline] scripts', + }, + }, + }) + ); expect(SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)).toBe(false); expect(decorateEsError(error)).toBe(error); expect(SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)).toBe(true); @@ -124,8 +156,9 @@ describe('savedObjectsClient/decorateEsError', () => { }); it('makes a SavedObjectsClient/BadRequest error for any other reason', () => { - const error = new esErrors.BadRequest(); - (error as Record).body = { error: { reason: 'some other reason' } }; + const error = new esErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ statusCode: 400 }) + ); expect(SavedObjectsErrorHelpers.isBadRequestError(error)).toBe(false); expect(decorateEsError(error)).toBe(error); expect(SavedObjectsErrorHelpers.isBadRequestError(error)).toBe(true); @@ -133,7 +166,7 @@ describe('savedObjectsClient/decorateEsError', () => { }); it('returns other errors as Boom errors', () => { - const error = new Error(); + const error = new esErrors.ResponseError(elasticsearchClientMock.createApiResponse()); expect(error).not.toHaveProperty('isBoom'); expect(decorateEsError(error)).toBe(error); expect(error).toHaveProperty('isBoom'); diff --git a/src/core/server/saved_objects/service/lib/decorate_es_error.ts b/src/core/server/saved_objects/service/lib/decorate_es_error.ts index 7d1575798c357..cf8a16cdaae6f 100644 --- a/src/core/server/saved_objects/service/lib/decorate_es_error.ts +++ b/src/core/server/saved_objects/service/lib/decorate_es_error.ts @@ -17,65 +17,66 @@ * under the License. */ -import * as legacyElasticsearch from 'elasticsearch'; +import { errors as esErrors } from '@elastic/elasticsearch'; import { get } from 'lodash'; -const { - ConnectionFault, - ServiceUnavailable, - NoConnections, - RequestTimeout, - Conflict, - // @ts-expect-error - 401: NotAuthorized, - // @ts-expect-error - 403: Forbidden, - // @ts-expect-error - 413: RequestEntityTooLarge, - NotFound, - BadRequest, -} = legacyElasticsearch.errors; +const responseErrors = { + isServiceUnavailable: (statusCode: number) => statusCode === 503, + isConflict: (statusCode: number) => statusCode === 409, + isNotAuthorized: (statusCode: number) => statusCode === 401, + isForbidden: (statusCode: number) => statusCode === 403, + isRequestEntityTooLarge: (statusCode: number) => statusCode === 413, + isNotFound: (statusCode: number) => statusCode === 404, + isBadRequest: (statusCode: number) => statusCode === 400, +}; +const { ConnectionError, NoLivingConnectionsError, TimeoutError } = esErrors; const SCRIPT_CONTEXT_DISABLED_REGEX = /(?:cannot execute scripts using \[)([a-z]*)(?:\] context)/; const INLINE_SCRIPTS_DISABLED_MESSAGE = 'cannot execute [inline] scripts'; import { SavedObjectsErrorHelpers } from './errors'; -export function decorateEsError(error: Error) { +type EsErrors = + | esErrors.ConnectionError + | esErrors.NoLivingConnectionsError + | esErrors.TimeoutError + | esErrors.ResponseError; + +export function decorateEsError(error: EsErrors) { if (!(error instanceof Error)) { throw new Error('Expected an instance of Error'); } const { reason } = get(error, 'body.error', { reason: undefined }) as { reason?: string }; if ( - error instanceof ConnectionFault || - error instanceof ServiceUnavailable || - error instanceof NoConnections || - error instanceof RequestTimeout + error instanceof ConnectionError || + error instanceof NoLivingConnectionsError || + error instanceof TimeoutError || + responseErrors.isServiceUnavailable(error.statusCode) ) { return SavedObjectsErrorHelpers.decorateEsUnavailableError(error, reason); } - if (error instanceof Conflict) { + if (responseErrors.isConflict(error.statusCode)) { return SavedObjectsErrorHelpers.decorateConflictError(error, reason); } - if (error instanceof NotAuthorized) { + if (responseErrors.isNotAuthorized(error.statusCode)) { return SavedObjectsErrorHelpers.decorateNotAuthorizedError(error, reason); } - if (error instanceof Forbidden) { + if (responseErrors.isForbidden(error.statusCode)) { return SavedObjectsErrorHelpers.decorateForbiddenError(error, reason); } - if (error instanceof RequestEntityTooLarge) { + if (responseErrors.isRequestEntityTooLarge(error.statusCode)) { return SavedObjectsErrorHelpers.decorateRequestEntityTooLargeError(error, reason); } - if (error instanceof NotFound) { + if (responseErrors.isNotFound(error.statusCode)) { return SavedObjectsErrorHelpers.createGenericNotFoundError(); } - if (error instanceof BadRequest) { + if (responseErrors.isBadRequest(error.statusCode)) { if ( SCRIPT_CONTEXT_DISABLED_REGEX.test(reason || '') || reason === INLINE_SCRIPTS_DISABLED_MESSAGE diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index d563edbe66c9b..b902179b012ff 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -24,6 +24,7 @@ import { SavedObjectsSerializer } from '../../serialization'; import { encodeHitVersion } from '../../version'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { DocumentMigrator } from '../../migrations/core/document_migrator'; +import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; jest.mock('./search_dsl/search_dsl', () => ({ getSearchDsl: jest.fn() })); @@ -40,7 +41,7 @@ const createUnsupportedTypeError = (...args) => SavedObjectsErrorHelpers.createUnsupportedTypeError(...args).output.payload; describe('SavedObjectsRepository', () => { - let callAdminCluster; + let client; let savedObjectsRepository; let migrator; @@ -170,26 +171,11 @@ describe('SavedObjectsRepository', () => { }); const getMockMgetResponse = (objects, namespace) => ({ - status: 200, docs: objects.map((obj) => obj.found === false ? obj : getMockGetResponse({ ...obj, namespace }) ), }); - const expectClusterCalls = (...actions) => { - for (let i = 0; i < actions.length; i++) { - expect(callAdminCluster).toHaveBeenNthCalledWith(i + 1, actions[i], expect.any(Object)); - } - expect(callAdminCluster).toHaveBeenCalledTimes(actions.length); - }; - const expectClusterCallArgs = (args, n = 1) => { - expect(callAdminCluster).toHaveBeenNthCalledWith( - n, - expect.any(String), - expect.objectContaining(args) - ); - }; - expect.extend({ toBeDocumentWithoutError(received, type, id) { if (received.type === type && received.id === id && !received.error) { @@ -215,7 +201,7 @@ describe('SavedObjectsRepository', () => { }; beforeEach(() => { - callAdminCluster = jest.fn(); + client = elasticsearchClientMock.createElasticSearchClient(); migrator = { migrateDocument: jest.fn().mockImplementation(documentMigrator.migrate), runMigrations: async () => ({ status: 'skipped' }), @@ -240,7 +226,7 @@ describe('SavedObjectsRepository', () => { savedObjectsRepository = new SavedObjectsRepository({ index: '.kibana-test', mappings, - callCluster: callAdminCluster, + client, migrator, typeRegistry: registry, serializer, @@ -248,7 +234,7 @@ describe('SavedObjectsRepository', () => { }); savedObjectsRepository._getCurrentTime = jest.fn(() => mockTimestamp); - getSearchDslNS.getSearchDsl.mockReset(); + getSearchDslNS.getSearchDsl.mockClear(); }); const mockMigrationVersion = { foo: '2.3.4' }; @@ -274,25 +260,29 @@ describe('SavedObjectsRepository', () => { // mock a document that exists in two namespaces const mockResponse = getMockGetResponse({ type, id }); mockResponse._source.namespaces = [currentNs1, currentNs2]; - callAdminCluster.mockResolvedValueOnce(mockResponse); // this._callCluster('get', ...) + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(mockResponse) + ); }; const addToNamespacesSuccess = async (type, id, namespaces, options) => { - mockGetResponse(type, id); // this._callCluster('get', ...) - callAdminCluster.mockResolvedValue({ - _id: `${type}:${id}`, - ...mockVersionProps, - result: 'updated', - }); // this._writeToCluster('update', ...) + mockGetResponse(type, id); + client.update.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + _id: `${type}:${id}`, + ...mockVersionProps, + result: 'updated', + }) + ); const result = await savedObjectsRepository.addToNamespaces(type, id, namespaces, options); - expect(callAdminCluster).toHaveBeenCalledTimes(2); + expect(client.get).toHaveBeenCalledTimes(1); + expect(client.update).toHaveBeenCalledTimes(1); return result; }; - describe('cluster calls', () => { + describe('client calls', () => { it(`should use ES get action then update action`, async () => { await addToNamespacesSuccess(type, id, [newNs1, newNs2]); - expectClusterCalls('get', 'update'); }); it(`defaults to the version of the existing document`, async () => { @@ -301,25 +291,28 @@ describe('SavedObjectsRepository', () => { if_seq_no: mockVersionProps._seq_no, if_primary_term: mockVersionProps._primary_term, }; - expectClusterCallArgs(versionProperties, 2); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining(versionProperties), + expect.anything() + ); }); it(`accepts version`, async () => { await addToNamespacesSuccess(type, id, [newNs1, newNs2], { version: encodeHitVersion({ _seq_no: 100, _primary_term: 200 }), }); - expectClusterCallArgs({ if_seq_no: 100, if_primary_term: 200 }, 2); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ if_seq_no: 100, if_primary_term: 200 }), + expect.anything() + ); }); it(`defaults to a refresh setting of wait_for`, async () => { await addToNamespacesSuccess(type, id, [newNs1, newNs2]); - expectClusterCallArgs({ refresh: 'wait_for' }, 2); - }); - - it(`accepts a custom refresh setting`, async () => { - const refresh = 'foo'; - await addToNamespacesSuccess(type, id, [newNs1, newNs2], { refresh }); - expectClusterCallArgs({ refresh }, 2); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ refresh: 'wait_for' }), + expect.anything() + ); }); }); @@ -337,19 +330,19 @@ describe('SavedObjectsRepository', () => { it(`throws when type is invalid`, async () => { await expectNotFoundError('unknownType', id, [newNs1, newNs2]); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.update).not.toHaveBeenCalled(); }); it(`throws when type is hidden`, async () => { await expectNotFoundError(HIDDEN_TYPE, id, [newNs1, newNs2]); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.update).not.toHaveBeenCalled(); }); it(`throws when type is not multi-namespace`, async () => { const test = async (type) => { const message = `${type} doesn't support multiple namespaces`; await expectBadRequestError(type, id, [newNs1, newNs2], message); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.update).not.toHaveBeenCalled(); }; await test('index-pattern'); await test(NAMESPACE_AGNOSTIC_TYPE); @@ -359,48 +352,43 @@ describe('SavedObjectsRepository', () => { const test = async (namespaces) => { const message = 'namespaces must be a non-empty array of strings'; await expectBadRequestError(type, id, namespaces, message); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.update).not.toHaveBeenCalled(); }; await test([]); }); it(`throws when ES is unable to find the document during get`, async () => { - callAdminCluster.mockResolvedValue({ found: false }); // this._callCluster('get', ...) + client.get.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) + ); await expectNotFoundError(type, id, [newNs1, newNs2]); - expectClusterCalls('get'); + expect(client.get).toHaveBeenCalledTimes(1); }); it(`throws when ES is unable to find the index during get`, async () => { - callAdminCluster.mockResolvedValue({ status: 404 }); // this._callCluster('get', ...) + client.get.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + ); await expectNotFoundError(type, id, [newNs1, newNs2]); - expectClusterCalls('get'); + expect(client.get).toHaveBeenCalledTimes(1); }); it(`throws when the document exists, but not in this namespace`, async () => { - mockGetResponse(type, id); // this._callCluster('get', ...) + mockGetResponse(type, id); await expectNotFoundError(type, id, [newNs1, newNs2], { namespace: 'some-other-namespace', }); - expectClusterCalls('get'); + expect(client.get).toHaveBeenCalledTimes(1); }); it(`throws when ES is unable to find the document during update`, async () => { - mockGetResponse(type, id); // this._callCluster('get', ...) - callAdminCluster.mockResolvedValue({ status: 404 }); // this._writeToCluster('update', ...) - await expectNotFoundError(type, id, [newNs1, newNs2]); - expectClusterCalls('get', 'update'); - }); - }); - - describe('migration', () => { - it(`waits until migrations are complete before proceeding`, async () => { - let callAdminClusterCount = 0; - migrator.runMigrations = jest.fn(async () => - // runMigrations should resolve before callAdminCluster is initiated - expect(callAdminCluster).toHaveBeenCalledTimes(callAdminClusterCount++) + mockGetResponse(type, id); + client.update.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) ); - await expect(addToNamespacesSuccess(type, id, [newNs1, newNs2])).resolves.toBeDefined(); - expect(migrator.runMigrations).toHaveReturnedTimes(2); + await expectNotFoundError(type, id, [newNs1, newNs2]); + expect(client.get).toHaveBeenCalledTimes(1); + expect(client.update).toHaveBeenCalledTimes(1); }); }); @@ -457,17 +445,21 @@ describe('SavedObjectsRepository', () => { objects.filter(({ type, id }) => registry.isMultiNamespace(type) && id); if (multiNamespaceObjects?.length) { const response = getMockMgetResponse(multiNamespaceObjects, options?.namespace); - callAdminCluster.mockResolvedValueOnce(response); // this._callCluster('mget', ...) + client.mget.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); } const response = getMockBulkCreateResponse(objects, options?.namespace); - callAdminCluster.mockResolvedValue(response); // this._writeToCluster('bulk', ...) + client.bulk.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); const result = await savedObjectsRepository.bulkCreate(objects, options); - expect(callAdminCluster).toHaveBeenCalledTimes(multiNamespaceObjects?.length ? 2 : 1); + expect(client.mget).toHaveBeenCalledTimes(multiNamespaceObjects?.length ? 1 : 0); return result; }; // bulk create calls have two objects for each source -- the action, and the source - const expectClusterCallArgsAction = ( + const expectClientCallArgsAction = ( objects, { method, _index = expect.any(String), getId = () => expect.any(String) } ) => { @@ -476,7 +468,10 @@ describe('SavedObjectsRepository', () => { body.push({ [method]: { _index, _id: getId(type, id) } }); body.push(expect.any(Object)); } - expectClusterCallArgs({ body }); + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); }; const expectObjArgs = ({ type, attributes, references }, overrides) => [ @@ -498,53 +493,60 @@ describe('SavedObjectsRepository', () => { ...mockTimestampFields, }); - describe('cluster calls', () => { + describe('client calls', () => { it(`should use the ES bulk action by default`, async () => { await bulkCreateSuccess([obj1, obj2]); - expectClusterCalls('bulk'); + expect(client.bulk).toHaveBeenCalledTimes(1); }); it(`should use the ES mget action before bulk action for any types that are multi-namespace, when overwrite=true`, async () => { const objects = [obj1, { ...obj2, type: MULTI_NAMESPACE_TYPE }]; await bulkCreateSuccess(objects, { overwrite: true }); - expectClusterCalls('mget', 'bulk'); + expect(client.bulk).toHaveBeenCalledTimes(1); + expect(client.mget).toHaveBeenCalledTimes(1); const docs = [expect.objectContaining({ _id: `${MULTI_NAMESPACE_TYPE}:${obj2.id}` })]; - expectClusterCallArgs({ body: { docs } }, 1); + expect(client.mget.mock.calls[0][0].body).toEqual({ docs }); }); it(`should use the ES create method if ID is undefined and overwrite=true`, async () => { const objects = [obj1, obj2].map((obj) => ({ ...obj, id: undefined })); await bulkCreateSuccess(objects, { overwrite: true }); - expectClusterCallArgsAction(objects, { method: 'create' }); + expectClientCallArgsAction(objects, { method: 'create' }); }); it(`should use the ES create method if ID is undefined and overwrite=false`, async () => { const objects = [obj1, obj2].map((obj) => ({ ...obj, id: undefined })); await bulkCreateSuccess(objects); - expectClusterCallArgsAction(objects, { method: 'create' }); + expectClientCallArgsAction(objects, { method: 'create' }); }); it(`should use the ES index method if ID is defined and overwrite=true`, async () => { await bulkCreateSuccess([obj1, obj2], { overwrite: true }); - expectClusterCallArgsAction([obj1, obj2], { method: 'index' }); + expectClientCallArgsAction([obj1, obj2], { method: 'index' }); }); it(`should use the ES create method if ID is defined and overwrite=false`, async () => { await bulkCreateSuccess([obj1, obj2]); - expectClusterCallArgsAction([obj1, obj2], { method: 'create' }); + expectClientCallArgsAction([obj1, obj2], { method: 'create' }); }); it(`formats the ES request`, async () => { await bulkCreateSuccess([obj1, obj2]); const body = [...expectObjArgs(obj1), ...expectObjArgs(obj2)]; - expectClusterCallArgs({ body }); + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); }); it(`adds namespace to request body for any types that are single-namespace`, async () => { await bulkCreateSuccess([obj1, obj2], { namespace }); const expected = expect.objectContaining({ namespace }); const body = [expect.any(Object), expected, expect.any(Object), expected]; - expectClusterCallArgs({ body }); + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); }); it(`doesn't add namespace to request body for any types that are not single-namespace`, async () => { @@ -555,7 +557,10 @@ describe('SavedObjectsRepository', () => { await bulkCreateSuccess(objects, { namespace }); const expected = expect.not.objectContaining({ namespace: expect.anything() }); const body = [expect.any(Object), expected, expect.any(Object), expected]; - expectClusterCallArgs({ body }); + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); }); it(`adds namespaces to request body for any types that are multi-namespace`, async () => { @@ -565,8 +570,12 @@ describe('SavedObjectsRepository', () => { await bulkCreateSuccess(objects, { namespace, overwrite: true }); const expected = expect.objectContaining({ namespaces }); const body = [expect.any(Object), expected, expect.any(Object), expected]; - expectClusterCallArgs({ body }, 2); - callAdminCluster.mockReset(); + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + client.bulk.mockClear(); + client.mget.mockClear(); }; await test(undefined); await test(namespace); @@ -578,8 +587,11 @@ describe('SavedObjectsRepository', () => { await bulkCreateSuccess(objects, { namespace, overwrite: true }); const expected = expect.not.objectContaining({ namespaces: expect.anything() }); const body = [expect.any(Object), expected, expect.any(Object), expected]; - expectClusterCallArgs({ body }); - callAdminCluster.mockReset(); + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + client.bulk.mockClear(); }; await test(undefined); await test(namespace); @@ -587,35 +599,32 @@ describe('SavedObjectsRepository', () => { it(`defaults to a refresh setting of wait_for`, async () => { await bulkCreateSuccess([obj1, obj2]); - expectClusterCallArgs({ refresh: 'wait_for' }); - }); - - it(`accepts a custom refresh setting`, async () => { - const refresh = 'foo'; - await bulkCreateSuccess([obj1, obj2], { refresh }); - expectClusterCallArgs({ refresh }); + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ refresh: 'wait_for' }), + expect.anything() + ); }); it(`should use default index`, async () => { await bulkCreateSuccess([obj1, obj2]); - expectClusterCallArgsAction([obj1, obj2], { method: 'create', _index: '.kibana-test' }); + expectClientCallArgsAction([obj1, obj2], { method: 'create', _index: '.kibana-test' }); }); it(`should use custom index`, async () => { await bulkCreateSuccess([obj1, obj2].map((x) => ({ ...x, type: CUSTOM_INDEX_TYPE }))); - expectClusterCallArgsAction([obj1, obj2], { method: 'create', _index: 'custom' }); + expectClientCallArgsAction([obj1, obj2], { method: 'create', _index: 'custom' }); }); it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { const getId = (type, id) => `${namespace}:${type}:${id}`; await bulkCreateSuccess([obj1, obj2], { namespace }); - expectClusterCallArgsAction([obj1, obj2], { method: 'create', getId }); + expectClientCallArgsAction([obj1, obj2], { method: 'create', getId }); }); it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { const getId = (type, id) => `${type}:${id}`; await bulkCreateSuccess([obj1, obj2]); - expectClusterCallArgsAction([obj1, obj2], { method: 'create', getId }); + expectClientCallArgsAction([obj1, obj2], { method: 'create', getId }); }); it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { @@ -625,7 +634,7 @@ describe('SavedObjectsRepository', () => { { ...obj2, type: MULTI_NAMESPACE_TYPE }, ]; await bulkCreateSuccess(objects, { namespace }); - expectClusterCallArgsAction(objects, { method: 'create', getId }); + expectClientCallArgsAction(objects, { method: 'create', getId }); }); }); @@ -645,14 +654,19 @@ describe('SavedObjectsRepository', () => { } else { response = getMockBulkCreateResponse([obj1, obj2]); } - callAdminCluster.mockResolvedValue(response); // this._writeToCluster('bulk', ...) + client.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); const objects = [obj1, obj, obj2]; const result = await savedObjectsRepository.bulkCreate(objects); - expectClusterCalls('bulk'); + expect(client.bulk).toHaveBeenCalled(); const objCall = esError ? expectObjArgs(obj) : []; const body = [...expectObjArgs(obj1), ...objCall, ...expectObjArgs(obj2)]; - expectClusterCallArgs({ body }); + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); expect(result).toEqual({ saved_objects: [expectSuccess(obj1), expectedError, expectSuccess(obj2)], }); @@ -682,17 +696,29 @@ describe('SavedObjectsRepository', () => { }, ], }; - callAdminCluster.mockResolvedValueOnce(response1); // this._callCluster('mget', ...) + client.mget.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response1) + ); const response2 = getMockBulkCreateResponse([obj1, obj2]); - callAdminCluster.mockResolvedValue(response2); // this._writeToCluster('bulk', ...) + client.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response2) + ); const options = { overwrite: true }; const result = await savedObjectsRepository.bulkCreate([obj1, obj, obj2], options); - expectClusterCalls('mget', 'bulk'); + expect(client.bulk).toHaveBeenCalled(); + expect(client.mget).toHaveBeenCalled(); + const body1 = { docs: [expect.objectContaining({ _id: `${obj.type}:${obj.id}` })] }; - expectClusterCallArgs({ body: body1 }, 1); + expect(client.mget).toHaveBeenCalledWith( + expect.objectContaining({ body: body1 }), + expect.anything() + ); const body2 = [...expectObjArgs(obj1), ...expectObjArgs(obj2)]; - expectClusterCallArgs({ body: body2 }, 2); + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body: body2 }), + expect.anything() + ); expect(result).toEqual({ saved_objects: [expectSuccess(obj1), expectErrorConflict(obj), expectSuccess(obj2)], }); @@ -721,14 +747,6 @@ describe('SavedObjectsRepository', () => { }); describe('migration', () => { - it(`waits until migrations are complete before proceeding`, async () => { - migrator.runMigrations = jest.fn(async () => - expect(callAdminCluster).not.toHaveBeenCalled() - ); - await expect(bulkCreateSuccess([obj1, obj2])).resolves.toBeDefined(); - expect(migrator.runMigrations).toHaveBeenCalledTimes(1); - }); - it(`migrates the docs and serializes the migrated docs`, async () => { migrator.migrateDocument.mockImplementation(mockMigrateDocument); await bulkCreateSuccess([obj1, obj2]); @@ -793,9 +811,7 @@ describe('SavedObjectsRepository', () => { }); }); - it(`should return objects in the same order regardless of type`, async () => { - // TODO - }); + it.todo(`should return objects in the same order regardless of type`); it(`handles a mix of successful creates and errors`, async () => { const obj = { @@ -804,9 +820,11 @@ describe('SavedObjectsRepository', () => { }; const objects = [obj1, obj, obj2]; const response = getMockBulkCreateResponse([obj1, obj2]); - callAdminCluster.mockResolvedValue(response); // this._writeToCluster('bulk', ...) + client.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); const result = await savedObjectsRepository.bulkCreate(objects); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(client.bulk).toHaveBeenCalledTimes(1); expect(result).toEqual({ saved_objects: [expectSuccessResult(obj1), expectError(obj), expectSuccessResult(obj2)], }); @@ -817,7 +835,9 @@ describe('SavedObjectsRepository', () => { // we returned raw ID's when an object without an id was created. const namespace = 'myspace'; const response = getMockBulkCreateResponse([obj1, obj2], namespace); - callAdminCluster.mockResolvedValueOnce(response); // this._writeToCluster('bulk', ...) + client.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); // Bulk create one object with id unspecified, and one with id specified const result = await savedObjectsRepository.bulkCreate([{ ...obj1, id: undefined }, obj2], { @@ -884,69 +904,78 @@ describe('SavedObjectsRepository', () => { ); const bulkGetSuccess = async (objects, options) => { const response = getMockMgetResponse(objects, options?.namespace); - callAdminCluster.mockReturnValue(response); + client.mget.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); const result = await bulkGet(objects, options); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(client.mget).toHaveBeenCalledTimes(1); return result; }; - const _expectClusterCallArgs = ( + const _expectClientCallArgs = ( objects, { _index = expect.any(String), getId = () => expect.any(String) } ) => { - expectClusterCallArgs({ - body: { - docs: objects.map(({ type, id }) => - expect.objectContaining({ - _index, - _id: getId(type, id), - }) - ), - }, - }); + expect(client.mget).toHaveBeenCalledWith( + expect.objectContaining({ + body: { + docs: objects.map(({ type, id }) => + expect.objectContaining({ + _index, + _id: getId(type, id), + }) + ), + }, + }), + expect.anything() + ); }; - describe('cluster calls', () => { + describe('client calls', () => { it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { const getId = (type, id) => `${namespace}:${type}:${id}`; await bulkGetSuccess([obj1, obj2], { namespace }); - _expectClusterCallArgs([obj1, obj2], { getId }); + _expectClientCallArgs([obj1, obj2], { getId }); }); it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { const getId = (type, id) => `${type}:${id}`; await bulkGetSuccess([obj1, obj2]); - _expectClusterCallArgs([obj1, obj2], { getId }); + _expectClientCallArgs([obj1, obj2], { getId }); }); it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { const getId = (type, id) => `${type}:${id}`; let objects = [obj1, obj2].map((obj) => ({ ...obj, type: NAMESPACE_AGNOSTIC_TYPE })); await bulkGetSuccess(objects, { namespace }); - _expectClusterCallArgs(objects, { getId }); + _expectClientCallArgs(objects, { getId }); - callAdminCluster.mockReset(); + client.mget.mockClear(); objects = [obj1, obj2].map((obj) => ({ ...obj, type: MULTI_NAMESPACE_TYPE })); await bulkGetSuccess(objects, { namespace }); - _expectClusterCallArgs(objects, { getId }); + _expectClientCallArgs(objects, { getId }); }); }); describe('errors', () => { const bulkGetErrorInvalidType = async ([obj1, obj, obj2]) => { const response = getMockMgetResponse([obj1, obj2]); - callAdminCluster.mockResolvedValue(response); + client.mget.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); const result = await bulkGet([obj1, obj, obj2]); - expectClusterCalls('mget'); + expect(client.mget).toHaveBeenCalled(); expect(result).toEqual({ saved_objects: [expectSuccess(obj1), expectErrorInvalidType(obj), expectSuccess(obj2)], }); }; const bulkGetErrorNotFound = async ([obj1, obj, obj2], options, response) => { - callAdminCluster.mockResolvedValue(response); + client.mget.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); const result = await bulkGet([obj1, obj, obj2], options); - expectClusterCalls('mget'); + expect(client.mget).toHaveBeenCalled(); expect(result).toEqual({ saved_objects: [expectSuccess(obj1), expectErrorNotFound(obj), expectSuccess(obj2)], }); @@ -982,16 +1011,6 @@ describe('SavedObjectsRepository', () => { }); }); - describe('migration', () => { - it(`waits until migrations are complete before proceeding`, async () => { - migrator.runMigrations = jest.fn(async () => - expect(callAdminCluster).not.toHaveBeenCalled() - ); - await expect(bulkGetSuccess([obj1, obj2])).resolves.toBeDefined(); - expect(migrator.runMigrations).toHaveBeenCalledTimes(1); - }); - }); - describe('returns', () => { const expectSuccessResult = ({ type, id }, doc) => ({ type, @@ -1007,14 +1026,16 @@ describe('SavedObjectsRepository', () => { it(`returns early for empty objects argument`, async () => { const result = await bulkGet([]); expect(result).toEqual({ saved_objects: [] }); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.mget).not.toHaveBeenCalled(); }); it(`formats the ES response`, async () => { const response = getMockMgetResponse([obj1, obj2]); - callAdminCluster.mockResolvedValue(response); + client.mget.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); const result = await bulkGet([obj1, obj2]); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(client.mget).toHaveBeenCalledTimes(1); expect(result).toEqual({ saved_objects: [ expectSuccessResult(obj1, response.docs[0]), @@ -1025,10 +1046,12 @@ describe('SavedObjectsRepository', () => { it(`handles a mix of successful gets and errors`, async () => { const response = getMockMgetResponse([obj1, obj2]); - callAdminCluster.mockResolvedValue(response); + client.mget.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); const obj = { type: 'unknownType', id: 'three' }; const result = await bulkGet([obj1, obj, obj2]); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(client.mget).toHaveBeenCalledTimes(1); expect(result).toEqual({ saved_objects: [ expectSuccessResult(obj1, response.docs[0]), @@ -1081,20 +1104,23 @@ describe('SavedObjectsRepository', () => { const multiNamespaceObjects = objects.filter(({ type }) => registry.isMultiNamespace(type)); if (multiNamespaceObjects?.length) { const response = getMockMgetResponse(multiNamespaceObjects, options?.namespace); - callAdminCluster.mockResolvedValueOnce(response); // this._callCluster('mget', ...) + client.mget.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); } const response = getMockBulkUpdateResponse(objects, options?.namespace); - callAdminCluster.mockResolvedValue(response); // this._writeToCluster('bulk', ...) + client.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); const result = await savedObjectsRepository.bulkUpdate(objects, options); - expect(callAdminCluster).toHaveBeenCalledTimes(multiNamespaceObjects?.length ? 2 : 1); + expect(client.mget).toHaveBeenCalledTimes(multiNamespaceObjects?.length ? 1 : 0); return result; }; // bulk create calls have two objects for each source -- the action, and the source - const expectClusterCallArgsAction = ( + const expectClientCallArgsAction = ( objects, - { method, _index = expect.any(String), getId = () => expect.any(String), overrides }, - n + { method, _index = expect.any(String), getId = () => expect.any(String), overrides } ) => { const body = []; for (const { type, id } of objects) { @@ -1107,7 +1133,10 @@ describe('SavedObjectsRepository', () => { }); body.push(expect.any(Object)); } - expectClusterCallArgs({ body }, n); + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); }; const expectObjArgs = ({ type, attributes }) => [ @@ -1120,44 +1149,58 @@ describe('SavedObjectsRepository', () => { }, ]; - describe('cluster calls', () => { + describe('client calls', () => { it(`should use the ES bulk action by default`, async () => { await bulkUpdateSuccess([obj1, obj2]); - expectClusterCalls('bulk'); + expect(client.bulk).toHaveBeenCalled(); }); it(`should use the ES mget action before bulk action for any types that are multi-namespace`, async () => { const objects = [obj1, { ...obj2, type: MULTI_NAMESPACE_TYPE }]; await bulkUpdateSuccess(objects); - expectClusterCalls('mget', 'bulk'); + expect(client.bulk).toHaveBeenCalled(); + expect(client.mget).toHaveBeenCalled(); + const docs = [expect.objectContaining({ _id: `${MULTI_NAMESPACE_TYPE}:${obj2.id}` })]; - expectClusterCallArgs({ body: { docs } }, 1); + expect(client.mget).toHaveBeenCalledWith( + expect.objectContaining({ body: { docs } }), + expect.anything() + ); }); it(`formats the ES request`, async () => { await bulkUpdateSuccess([obj1, obj2]); const body = [...expectObjArgs(obj1), ...expectObjArgs(obj2)]; - expectClusterCallArgs({ body }); + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); }); it(`formats the ES request for any types that are multi-namespace`, async () => { const _obj2 = { ...obj2, type: MULTI_NAMESPACE_TYPE }; await bulkUpdateSuccess([obj1, _obj2]); const body = [...expectObjArgs(obj1), ...expectObjArgs(_obj2)]; - expectClusterCallArgs({ body }, 2); + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); }); it(`doesnt call Elasticsearch if there are no valid objects to update`, async () => { const objects = [obj1, obj2].map((x) => ({ ...x, type: 'unknownType' })); await savedObjectsRepository.bulkUpdate(objects); - expect(callAdminCluster).toHaveBeenCalledTimes(0); + expect(client.bulk).toHaveBeenCalledTimes(0); }); it(`defaults to no references`, async () => { await bulkUpdateSuccess([obj1, obj2]); const expected = { doc: expect.not.objectContaining({ references: expect.anything() }) }; const body = [expect.any(Object), expected, expect.any(Object), expected]; - expectClusterCallArgs({ body }); + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); }); it(`accepts custom references array`, async () => { @@ -1166,8 +1209,11 @@ describe('SavedObjectsRepository', () => { await bulkUpdateSuccess(objects); const expected = { doc: expect.objectContaining({ references }) }; const body = [expect.any(Object), expected, expect.any(Object), expected]; - expectClusterCallArgs({ body }); - callAdminCluster.mockReset(); + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + client.bulk.mockClear(); }; await test(references); await test(['string']); @@ -1180,8 +1226,11 @@ describe('SavedObjectsRepository', () => { await bulkUpdateSuccess(objects); const expected = { doc: expect.not.objectContaining({ references: expect.anything() }) }; const body = [expect.any(Object), expected, expect.any(Object), expected]; - expectClusterCallArgs({ body }); - callAdminCluster.mockReset(); + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + client.bulk.mockClear(); }; await test('string'); await test(123); @@ -1191,13 +1240,10 @@ describe('SavedObjectsRepository', () => { it(`defaults to a refresh setting of wait_for`, async () => { await bulkUpdateSuccess([obj1, obj2]); - expectClusterCallArgs({ refresh: 'wait_for' }); - }); - - it(`accepts a custom refresh setting`, async () => { - const refresh = 'foo'; - await bulkUpdateSuccess([obj1, obj2], { refresh }); - expectClusterCallArgs({ refresh }); + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ refresh: 'wait_for' }), + expect.anything() + ); }); it(`defaults to the version of the existing document for multi-namespace types`, async () => { @@ -1211,13 +1257,13 @@ describe('SavedObjectsRepository', () => { if_seq_no: mockVersionProps._seq_no, if_primary_term: mockVersionProps._primary_term, }; - expectClusterCallArgsAction(objects, { method: 'update', overrides }, 2); + expectClientCallArgsAction(objects, { method: 'update', overrides }); }); it(`defaults to no version for types that are not multi-namespace`, async () => { const objects = [obj1, { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE }]; await bulkUpdateSuccess(objects); - expectClusterCallArgsAction(objects, { method: 'update' }); + expectClientCallArgsAction(objects, { method: 'update' }); }); it(`accepts version`, async () => { @@ -1229,27 +1275,27 @@ describe('SavedObjectsRepository', () => { ]; await bulkUpdateSuccess(objects); const overrides = { if_seq_no: 100, if_primary_term: 200 }; - expectClusterCallArgsAction(objects, { method: 'update', overrides }, 2); + expectClientCallArgsAction(objects, { method: 'update', overrides }, 2); }); it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { const getId = (type, id) => `${namespace}:${type}:${id}`; await bulkUpdateSuccess([obj1, obj2], { namespace }); - expectClusterCallArgsAction([obj1, obj2], { method: 'update', getId }); + expectClientCallArgsAction([obj1, obj2], { method: 'update', getId }); }); it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { const getId = (type, id) => `${type}:${id}`; await bulkUpdateSuccess([obj1, obj2]); - expectClusterCallArgsAction([obj1, obj2], { method: 'update', getId }); + expectClientCallArgsAction([obj1, obj2], { method: 'update', getId }); }); it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { const getId = (type, id) => `${type}:${id}`; const objects1 = [{ ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }]; await bulkUpdateSuccess(objects1, { namespace }); - expectClusterCallArgsAction(objects1, { method: 'update', getId }); - callAdminCluster.mockReset(); + expectClientCallArgsAction(objects1, { method: 'update', getId }); + client.bulk.mockClear(); const overrides = { // bulkUpdate uses a preflight `get` request for multi-namespace saved objects, and specifies that version on `update` // we aren't testing for this here, but we need to include Jest assertions so this test doesn't fail @@ -1258,7 +1304,7 @@ describe('SavedObjectsRepository', () => { }; const objects2 = [{ ...obj2, type: MULTI_NAMESPACE_TYPE }]; await bulkUpdateSuccess(objects2, { namespace }); - expectClusterCallArgsAction(objects2, { method: 'update', getId, overrides }, 2); + expectClientCallArgsAction(objects2, { method: 'update', getId, overrides }, 2); }); }); @@ -1274,27 +1320,44 @@ describe('SavedObjectsRepository', () => { if (esError) { mockResponse.items[1].update = { error: esError }; } - callAdminCluster.mockResolvedValue(mockResponse); // this._writeToCluster('bulk', ...) + client.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(mockResponse) + ); const result = await savedObjectsRepository.bulkUpdate(objects); - expectClusterCalls('bulk'); + expect(client.bulk).toHaveBeenCalled(); const objCall = esError ? expectObjArgs(obj) : []; const body = [...expectObjArgs(obj1), ...objCall, ...expectObjArgs(obj2)]; - expectClusterCallArgs({ body }); + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); expect(result).toEqual({ saved_objects: [expectSuccess(obj1), expectedError, expectSuccess(obj2)], }); }; const bulkUpdateMultiError = async ([obj1, _obj, obj2], options, mgetResponse) => { - callAdminCluster.mockResolvedValueOnce(mgetResponse); // this._callCluster('mget', ...) + client.mget.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(mgetResponse, { + statusCode: mgetResponse.statusCode, + }) + ); + const bulkResponse = getMockBulkUpdateResponse([obj1, obj2], namespace); - callAdminCluster.mockResolvedValue(bulkResponse); // this._writeToCluster('bulk', ...) + client.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(bulkResponse) + ); const result = await savedObjectsRepository.bulkUpdate([obj1, _obj, obj2], options); - expectClusterCalls('mget', 'bulk'); + expect(client.bulk).toHaveBeenCalled(); + expect(client.mget).toHaveBeenCalled(); const body = [...expectObjArgs(obj1), ...expectObjArgs(obj2)]; - expectClusterCallArgs({ body }, 2); + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + expect(result).toEqual({ saved_objects: [expectSuccess(obj1), expectErrorNotFound(_obj), expectSuccess(obj2)], }); @@ -1318,7 +1381,7 @@ describe('SavedObjectsRepository', () => { it(`returns error when ES is unable to find the index (mget)`, async () => { const _obj = { ...obj, type: MULTI_NAMESPACE_TYPE }; - const mgetResponse = { status: 404 }; + const mgetResponse = { statusCode: 404 }; await bulkUpdateMultiError([obj1, _obj, obj2], { namespace }, mgetResponse); }); @@ -1350,16 +1413,6 @@ describe('SavedObjectsRepository', () => { }); }); - describe('migration', () => { - it(`waits until migrations are complete before proceeding`, async () => { - migrator.runMigrations = jest.fn(async () => - expect(callAdminCluster).not.toHaveBeenCalled() - ); - await expect(bulkUpdateSuccess([obj1, obj2])).resolves.toBeDefined(); - expect(migrator.runMigrations).toHaveReturnedTimes(1); - }); - }); - describe('returns', () => { const expectSuccessResult = ({ type, id, attributes, references, namespaces }) => ({ type, @@ -1393,9 +1446,12 @@ describe('SavedObjectsRepository', () => { }; const objects = [obj1, obj, obj2]; const mockResponse = getMockBulkUpdateResponse(objects); - callAdminCluster.mockResolvedValue(mockResponse); // this._writeToCluster('bulk', ...) + client.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(mockResponse) + ); + const result = await savedObjectsRepository.bulkUpdate(objects); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(client.bulk).toHaveBeenCalledTimes(1); expect(result).toEqual({ saved_objects: [expectSuccessResult(obj1), expectError(obj), expectSuccessResult(obj2)], }); @@ -1416,10 +1472,12 @@ describe('SavedObjectsRepository', () => { describe('#create', () => { beforeEach(() => { - callAdminCluster.mockImplementation((method, params) => ({ - _id: params.id, - ...mockVersionProps, - })); + client.create.mockImplementation((params) => + elasticsearchClientMock.createSuccessTransportRequestPromise({ + _id: params.id, + ...mockVersionProps, + }) + ); }); const type = 'index-pattern'; @@ -1436,52 +1494,49 @@ describe('SavedObjectsRepository', () => { const createSuccess = async (type, attributes, options) => { const result = await savedObjectsRepository.create(type, attributes, options); - expect(callAdminCluster).toHaveBeenCalledTimes( - registry.isMultiNamespace(type) && options.overwrite ? 2 : 1 + expect(client.get).toHaveBeenCalledTimes( + registry.isMultiNamespace(type) && options.overwrite ? 1 : 0 ); return result; }; - describe('cluster calls', () => { + describe('client calls', () => { it(`should use the ES create action if ID is undefined and overwrite=true`, async () => { await createSuccess(type, attributes, { overwrite: true }); - expectClusterCalls('create'); + expect(client.create).toHaveBeenCalled(); }); it(`should use the ES create action if ID is undefined and overwrite=false`, async () => { await createSuccess(type, attributes); - expectClusterCalls('create'); + expect(client.create).toHaveBeenCalled(); }); it(`should use the ES index action if ID is defined and overwrite=true`, async () => { await createSuccess(type, attributes, { id, overwrite: true }); - expectClusterCalls('index'); + expect(client.index).toHaveBeenCalled(); }); it(`should use the ES create action if ID is defined and overwrite=false`, async () => { await createSuccess(type, attributes, { id }); - expectClusterCalls('create'); + expect(client.create).toHaveBeenCalled(); }); it(`should use the ES get action then index action if type is multi-namespace, ID is defined, and overwrite=true`, async () => { await createSuccess(MULTI_NAMESPACE_TYPE, attributes, { id, overwrite: true }); - expectClusterCalls('get', 'index'); + expect(client.get).toHaveBeenCalled(); + expect(client.index).toHaveBeenCalled(); }); it(`defaults to empty references array`, async () => { await createSuccess(type, attributes, { id }); - expectClusterCallArgs({ - body: expect.objectContaining({ references: [] }), - }); + expect(client.create.mock.calls[0][0].body.references).toEqual([]); }); it(`accepts custom references array`, async () => { const test = async (references) => { await createSuccess(type, attributes, { id, references }); - expectClusterCallArgs({ - body: expect.objectContaining({ references }), - }); - callAdminCluster.mockReset(); + expect(client.create.mock.calls[0][0].body.references).toEqual(references); + client.create.mockClear(); }; await test(references); await test(['string']); @@ -1491,10 +1546,8 @@ describe('SavedObjectsRepository', () => { it(`doesn't accept custom references if not an array`, async () => { const test = async (references) => { await createSuccess(type, attributes, { id, references }); - expectClusterCallArgs({ - body: expect.not.objectContaining({ references: expect.anything() }), - }); - callAdminCluster.mockReset(); + expect(client.create.mock.calls[0][0].body.references).not.toBeDefined(); + client.create.mockClear(); }; await test('string'); await test(123); @@ -1504,49 +1557,75 @@ describe('SavedObjectsRepository', () => { it(`defaults to a refresh setting of wait_for`, async () => { await createSuccess(type, attributes); - expectClusterCallArgs({ refresh: 'wait_for' }); - }); - - it(`accepts a custom refresh setting`, async () => { - const refresh = 'foo'; - await createSuccess(type, attributes, { refresh }); - expectClusterCallArgs({ refresh }); + expect(client.create).toHaveBeenCalledWith( + expect.objectContaining({ refresh: 'wait_for' }), + expect.anything() + ); }); it(`should use default index`, async () => { await createSuccess(type, attributes, { id }); - expectClusterCallArgs({ index: '.kibana-test' }); + expect(client.create).toHaveBeenCalledWith( + expect.objectContaining({ index: '.kibana-test' }), + expect.anything() + ); }); it(`should use custom index`, async () => { await createSuccess(CUSTOM_INDEX_TYPE, attributes, { id }); - expectClusterCallArgs({ index: 'custom' }); + expect(client.create).toHaveBeenCalledWith( + expect.objectContaining({ index: 'custom' }), + expect.anything() + ); }); it(`self-generates an id if none is provided`, async () => { await createSuccess(type, attributes); - expectClusterCallArgs({ - id: expect.objectContaining(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/), - }); + expect(client.create).toHaveBeenCalledWith( + expect.objectContaining({ + id: expect.objectContaining(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/), + }), + expect.anything() + ); }); it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { await createSuccess(type, attributes, { id, namespace }); - expectClusterCallArgs({ id: `${namespace}:${type}:${id}` }); + expect(client.create).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${namespace}:${type}:${id}`, + }), + expect.anything() + ); }); it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { await createSuccess(type, attributes, { id }); - expectClusterCallArgs({ id: `${type}:${id}` }); + expect(client.create).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${type}:${id}`, + }), + expect.anything() + ); }); it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { await createSuccess(NAMESPACE_AGNOSTIC_TYPE, attributes, { id, namespace }); - expectClusterCallArgs({ id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}` }); - callAdminCluster.mockReset(); + expect(client.create).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}`, + }), + expect.anything() + ); + client.create.mockClear(); await createSuccess(MULTI_NAMESPACE_TYPE, attributes, { id, namespace }); - expectClusterCallArgs({ id: `${MULTI_NAMESPACE_TYPE}:${id}` }); + expect(client.create).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${MULTI_NAMESPACE_TYPE}:${id}`, + }), + expect.anything() + ); }); }); @@ -1555,14 +1634,14 @@ describe('SavedObjectsRepository', () => { await expect(savedObjectsRepository.create('unknownType', attributes)).rejects.toThrowError( createUnsupportedTypeError('unknownType') ); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.create).not.toHaveBeenCalled(); }); it(`throws when type is hidden`, async () => { await expect(savedObjectsRepository.create(HIDDEN_TYPE, attributes)).rejects.toThrowError( createUnsupportedTypeError(HIDDEN_TYPE) ); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.create).not.toHaveBeenCalled(); }); it(`throws when there is a conflict with an existing multi-namespace saved object (get)`, async () => { @@ -1571,7 +1650,9 @@ describe('SavedObjectsRepository', () => { id, namespace: 'bar-namespace', }); - callAdminCluster.mockResolvedValue(response); // this._callCluster('get', ...) + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); await expect( savedObjectsRepository.create(MULTI_NAMESPACE_TYPE, attributes, { id, @@ -1579,16 +1660,12 @@ describe('SavedObjectsRepository', () => { namespace, }) ).rejects.toThrowError(createConflictError(MULTI_NAMESPACE_TYPE, id)); - expectClusterCalls('get'); + expect(client.get).toHaveBeenCalled(); }); - it(`throws when automatic index creation fails`, async () => { - // TODO - }); + it.todo(`throws when automatic index creation fails`); - it(`throws when an unexpected failure occurs`, async () => { - // TODO - }); + it.todo(`throws when an unexpected failure occurs`); }); describe('migration', () => { @@ -1596,14 +1673,6 @@ describe('SavedObjectsRepository', () => { migrator.migrateDocument.mockImplementation(mockMigrateDocument); }); - it(`waits until migrations are complete before proceeding`, async () => { - migrator.runMigrations = jest.fn(async () => - expect(callAdminCluster).not.toHaveBeenCalled() - ); - await expect(createSuccess(type, attributes, { id, namespace })).resolves.toBeDefined(); - expect(migrator.runMigrations).toHaveBeenCalledTimes(1); - }); - it(`migrates a document and serializes the migrated doc`, async () => { const migrationVersion = mockMigrationVersion; await createSuccess(type, attributes, { id, references, migrationVersion }); @@ -1628,7 +1697,7 @@ describe('SavedObjectsRepository', () => { await createSuccess(NAMESPACE_AGNOSTIC_TYPE, attributes, { id, namespace }); expectMigrationArgs({ namespace: expect.anything() }, false, 1); - callAdminCluster.mockReset(); + client.create.mockClear(); await createSuccess(MULTI_NAMESPACE_TYPE, attributes, { id }); expectMigrationArgs({ namespace: expect.anything() }, false, 2); }); @@ -1647,7 +1716,7 @@ describe('SavedObjectsRepository', () => { await createSuccess(type, attributes, { id }); expectMigrationArgs({ namespaces: expect.anything() }, false, 1); - callAdminCluster.mockReset(); + client.create.mockClear(); await createSuccess(NAMESPACE_AGNOSTIC_TYPE, attributes, { id }); expectMigrationArgs({ namespaces: expect.anything() }, false, 2); }); @@ -1678,33 +1747,43 @@ describe('SavedObjectsRepository', () => { const deleteSuccess = async (type, id, options) => { if (registry.isMultiNamespace(type)) { const mockGetResponse = getMockGetResponse({ type, id, namespace: options?.namespace }); - callAdminCluster.mockResolvedValueOnce(mockGetResponse); // this._callCluster('get', ...) + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(mockGetResponse) + ); } - callAdminCluster.mockResolvedValue({ result: 'deleted' }); // this._writeToCluster('delete', ...) + client.delete.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ result: 'deleted' }) + ); const result = await savedObjectsRepository.delete(type, id, options); - expect(callAdminCluster).toHaveBeenCalledTimes(registry.isMultiNamespace(type) ? 2 : 1); + expect(client.get).toHaveBeenCalledTimes(registry.isMultiNamespace(type) ? 1 : 0); return result; }; - describe('cluster calls', () => { + describe('client calls', () => { it(`should use the ES delete action when not using a multi-namespace type`, async () => { await deleteSuccess(type, id); - expectClusterCalls('delete'); + expect(client.delete).toHaveBeenCalledTimes(1); }); it(`should use ES get action then delete action when using a multi-namespace type with no namespaces remaining`, async () => { await deleteSuccess(MULTI_NAMESPACE_TYPE, id); - expectClusterCalls('get', 'delete'); + expect(client.get).toHaveBeenCalledTimes(1); + expect(client.delete).toHaveBeenCalledTimes(1); }); it(`should use ES get action then update action when using a multi-namespace type with one or more namespaces remaining`, async () => { const mockResponse = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id }); mockResponse._source.namespaces = ['default', 'some-other-nameespace']; - callAdminCluster - .mockResolvedValueOnce(mockResponse) // this._callCluster('get', ...) - .mockResolvedValue({ result: 'updated' }); // this._writeToCluster('update', ...) + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(mockResponse) + ); + client.update.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ result: 'updated' }) + ); + await savedObjectsRepository.delete(MULTI_NAMESPACE_TYPE, id); - expectClusterCalls('get', 'update'); + expect(client.get).toHaveBeenCalledTimes(1); + expect(client.update).toHaveBeenCalledTimes(1); }); it(`includes the version of the existing document when type is multi-namespace`, async () => { @@ -1713,37 +1792,49 @@ describe('SavedObjectsRepository', () => { if_seq_no: mockVersionProps._seq_no, if_primary_term: mockVersionProps._primary_term, }; - expectClusterCallArgs(versionProperties, 2); + expect(client.delete).toHaveBeenCalledWith( + expect.objectContaining(versionProperties), + expect.anything() + ); }); it(`defaults to a refresh setting of wait_for`, async () => { await deleteSuccess(type, id); - expectClusterCallArgs({ refresh: 'wait_for' }); - }); - - it(`accepts a custom refresh setting`, async () => { - const refresh = 'foo'; - await deleteSuccess(type, id, { refresh }); - expectClusterCallArgs({ refresh }); + expect(client.delete).toHaveBeenCalledWith( + expect.objectContaining({ refresh: 'wait_for' }), + expect.anything() + ); }); it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { await deleteSuccess(type, id, { namespace }); - expectClusterCallArgs({ id: `${namespace}:${type}:${id}` }); + expect(client.delete).toHaveBeenCalledWith( + expect.objectContaining({ id: `${namespace}:${type}:${id}` }), + expect.anything() + ); }); it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { await deleteSuccess(type, id); - expectClusterCallArgs({ id: `${type}:${id}` }); + expect(client.delete).toHaveBeenCalledWith( + expect.objectContaining({ id: `${type}:${id}` }), + expect.anything() + ); }); it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { await deleteSuccess(NAMESPACE_AGNOSTIC_TYPE, id, { namespace }); - expectClusterCallArgs({ id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}` }); + expect(client.delete).toHaveBeenCalledWith( + expect.objectContaining({ id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}` }), + expect.anything() + ); - callAdminCluster.mockReset(); + client.delete.mockClear(); await deleteSuccess(MULTI_NAMESPACE_TYPE, id, { namespace }); - expectClusterCallArgs({ id: `${MULTI_NAMESPACE_TYPE}:${id}` }); + expect(client.delete).toHaveBeenCalledWith( + expect.objectContaining({ id: `${MULTI_NAMESPACE_TYPE}:${id}` }), + expect.anything() + ); }); }); @@ -1756,73 +1847,82 @@ describe('SavedObjectsRepository', () => { it(`throws when type is invalid`, async () => { await expectNotFoundError('unknownType', id); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.delete).not.toHaveBeenCalled(); }); it(`throws when type is hidden`, async () => { await expectNotFoundError(HIDDEN_TYPE, id); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.delete).not.toHaveBeenCalled(); }); it(`throws when ES is unable to find the document during get`, async () => { - callAdminCluster.mockResolvedValue({ found: false }); // this._callCluster('get', ...) + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) + ); await expectNotFoundError(MULTI_NAMESPACE_TYPE, id); - expectClusterCalls('get'); + expect(client.get).toHaveBeenCalledTimes(1); }); it(`throws when ES is unable to find the index during get`, async () => { - callAdminCluster.mockResolvedValue({ status: 404 }); // this._callCluster('get', ...) + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + ); await expectNotFoundError(MULTI_NAMESPACE_TYPE, id); - expectClusterCalls('get'); + expect(client.get).toHaveBeenCalledTimes(1); }); it(`throws when the type is multi-namespace and the document exists, but not in this namespace`, async () => { const response = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id, namespace }); - callAdminCluster.mockResolvedValue(response); // this._callCluster('get', ...) + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); await expectNotFoundError(MULTI_NAMESPACE_TYPE, id, { namespace: 'bar-namespace' }); - expectClusterCalls('get'); + expect(client.get).toHaveBeenCalledTimes(1); }); it(`throws when ES is unable to find the document during update`, async () => { const mockResponse = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id }); mockResponse._source.namespaces = ['default', 'some-other-nameespace']; - callAdminCluster - .mockResolvedValueOnce(mockResponse) // this._callCluster('get', ...) - .mockResolvedValue({ status: 404 }); // this._writeToCluster('update', ...) + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(mockResponse) + ); + client.update.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + ); + await expectNotFoundError(MULTI_NAMESPACE_TYPE, id); - expectClusterCalls('get', 'update'); + expect(client.get).toHaveBeenCalledTimes(1); + expect(client.update).toHaveBeenCalledTimes(1); }); it(`throws when ES is unable to find the document during delete`, async () => { - callAdminCluster.mockResolvedValue({ result: 'not_found' }); // this._writeToCluster('delete', ...) + client.delete.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ result: 'not_found' }) + ); await expectNotFoundError(type, id); - expectClusterCalls('delete'); + expect(client.delete).toHaveBeenCalledTimes(1); }); it(`throws when ES is unable to find the index during delete`, async () => { - callAdminCluster.mockResolvedValue({ error: { type: 'index_not_found_exception' } }); // this._writeToCluster('delete', ...) + client.delete.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + error: { type: 'index_not_found_exception' }, + }) + ); await expectNotFoundError(type, id); - expectClusterCalls('delete'); + expect(client.delete).toHaveBeenCalledTimes(1); }); it(`throws when ES returns an unexpected response`, async () => { - callAdminCluster.mockResolvedValue({ result: 'something unexpected' }); // this._writeToCluster('delete', ...) + client.delete.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + result: 'something unexpected', + }) + ); await expect(savedObjectsRepository.delete(type, id)).rejects.toThrowError( 'Unexpected Elasticsearch DELETE response' ); - expectClusterCalls('delete'); - }); - }); - - describe('migration', () => { - it(`waits until migrations are complete before proceeding`, async () => { - let callAdminClusterCount = 0; - migrator.runMigrations = jest.fn(async () => - // runMigrations should resolve before callAdminCluster is initiated - expect(callAdminCluster).toHaveBeenCalledTimes(callAdminClusterCount++) - ); - await expect(deleteSuccess(type, id)).resolves.toBeDefined(); - expect(migrator.runMigrations).toHaveBeenCalledTimes(1); + expect(client.delete).toHaveBeenCalledTimes(1); }); }); @@ -1853,33 +1953,27 @@ describe('SavedObjectsRepository', () => { }; const deleteByNamespaceSuccess = async (namespace, options) => { - callAdminCluster.mockResolvedValue(mockUpdateResults); + client.updateByQuery.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(mockUpdateResults) + ); const result = await savedObjectsRepository.deleteByNamespace(namespace, options); expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(client.updateByQuery).toHaveBeenCalledTimes(1); return result; }; - describe('cluster calls', () => { + describe('client calls', () => { it(`should use the ES updateByQuery action`, async () => { await deleteByNamespaceSuccess(namespace); - expectClusterCalls('updateByQuery'); - }); - - it(`defaults to a refresh setting of wait_for`, async () => { - await deleteByNamespaceSuccess(namespace); - expectClusterCallArgs({ refresh: 'wait_for' }); - }); - - it(`accepts a custom refresh setting`, async () => { - const refresh = 'foo'; - await deleteByNamespaceSuccess(namespace, { refresh }); - expectClusterCallArgs({ refresh }); + expect(client.updateByQuery).toHaveBeenCalledTimes(1); }); it(`should use all indices for types that are not namespace-agnostic`, async () => { await deleteByNamespaceSuccess(namespace); - expectClusterCallArgs({ index: ['.kibana-test', 'custom'] }, 1); + expect(client.updateByQuery).toHaveBeenCalledWith( + expect.objectContaining({ index: ['.kibana-test', 'custom'] }), + expect.anything() + ); }); }); @@ -1889,7 +1983,7 @@ describe('SavedObjectsRepository', () => { await expect(savedObjectsRepository.deleteByNamespace(namespace)).rejects.toThrowError( `namespace is required, and must be a string` ); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.updateByQuery).not.toHaveBeenCalled(); }; await test(undefined); await test(['namespace']); @@ -1898,16 +1992,6 @@ describe('SavedObjectsRepository', () => { }); }); - describe('migration', () => { - it(`waits until migrations are complete before proceeding`, async () => { - migrator.runMigrations = jest.fn(async () => - expect(callAdminCluster).not.toHaveBeenCalled() - ); - await expect(deleteByNamespaceSuccess(namespace)).resolves.toBeDefined(); - expect(migrator.runMigrations).toHaveBeenCalledTimes(1); - }); - }); - describe('returns', () => { it(`returns the query results on success`, async () => { const result = await deleteByNamespaceSuccess(namespace); @@ -2002,64 +2086,90 @@ describe('SavedObjectsRepository', () => { const namespace = 'foo-namespace'; const findSuccess = async (options, namespace) => { - callAdminCluster.mockResolvedValue(generateSearchResults(namespace)); + client.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + generateSearchResults(namespace) + ) + ); const result = await savedObjectsRepository.find(options); expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(client.search).toHaveBeenCalledTimes(1); return result; }; - describe('cluster calls', () => { + describe('client calls', () => { it(`should use the ES search action`, async () => { await findSuccess({ type }); - expectClusterCalls('search'); + expect(client.search).toHaveBeenCalledTimes(1); }); it(`merges output of getSearchDsl into es request body`, async () => { const query = { query: 1, aggregations: 2 }; getSearchDslNS.getSearchDsl.mockReturnValue(query); await findSuccess({ type }); - expectClusterCallArgs({ body: expect.objectContaining({ ...query }) }); + + expect(client.search).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ ...query }), + }), + expect.anything() + ); }); it(`accepts per_page/page`, async () => { await findSuccess({ type, perPage: 10, page: 6 }); - expectClusterCallArgs({ - size: 10, - from: 50, - }); + expect(client.search).toHaveBeenCalledWith( + expect.objectContaining({ + size: 10, + from: 50, + }), + expect.anything() + ); }); it(`accepts preference`, async () => { await findSuccess({ type, preference: 'pref' }); - expectClusterCallArgs({ preference: 'pref' }); + expect(client.search).toHaveBeenCalledWith( + expect.objectContaining({ + preference: 'pref', + }), + expect.anything() + ); }); it(`can filter by fields`, async () => { await findSuccess({ type, fields: ['title'] }); - expectClusterCallArgs({ - _source: [ - `${type}.title`, - 'namespace', - 'namespaces', - 'type', - 'references', - 'migrationVersion', - 'updated_at', - 'title', - ], - }); + expect(client.search).toHaveBeenCalledWith( + expect.objectContaining({ + _source: [ + `${type}.title`, + 'namespace', + 'namespaces', + 'type', + 'references', + 'migrationVersion', + 'updated_at', + 'title', + ], + }), + expect.anything() + ); }); it(`should set rest_total_hits_as_int to true on a request`, async () => { await findSuccess({ type }); - expectClusterCallArgs({ rest_total_hits_as_int: true }); + expect(client.search).toHaveBeenCalledWith( + expect.objectContaining({ + rest_total_hits_as_int: true, + }), + expect.anything() + ); }); - it(`should not make a cluster call when attempting to find only invalid or hidden types`, async () => { + it(`should not make a client call when attempting to find only invalid or hidden types`, async () => { const test = async (types) => { await savedObjectsRepository.find({ type: types }); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.search).not.toHaveBeenCalled(); }; await test('unknownType'); @@ -2073,21 +2183,21 @@ describe('SavedObjectsRepository', () => { await expect(savedObjectsRepository.find({})).rejects.toThrowError( 'options.type must be a string or an array of strings' ); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.search).not.toHaveBeenCalled(); }); it(`throws when searchFields is defined but not an array`, async () => { await expect( savedObjectsRepository.find({ type, searchFields: 'string' }) ).rejects.toThrowError('options.searchFields must be an array'); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.search).not.toHaveBeenCalled(); }); it(`throws when fields is defined but not an array`, async () => { await expect(savedObjectsRepository.find({ type, fields: 'string' })).rejects.toThrowError( 'options.fields must be an array' ); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.search).not.toHaveBeenCalled(); }); it(`throws when KQL filter syntax is invalid`, async () => { @@ -2113,24 +2223,16 @@ describe('SavedObjectsRepository', () => { --------------------------------^: Bad Request] `); expect(getSearchDslNS.getSearchDsl).not.toHaveBeenCalled(); - expect(callAdminCluster).not.toHaveBeenCalled(); - }); - }); - - describe('migration', () => { - it(`waits until migrations are complete before proceeding`, async () => { - migrator.runMigrations = jest.fn(async () => - expect(callAdminCluster).not.toHaveBeenCalled() - ); - await expect(findSuccess({ type })).resolves.toBeDefined(); - expect(migrator.runMigrations).toHaveBeenCalledTimes(1); + expect(client.search).not.toHaveBeenCalled(); }); }); describe('returns', () => { it(`formats the ES response when there is no namespace`, async () => { const noNamespaceSearchResults = generateSearchResults(); - callAdminCluster.mockReturnValue(noNamespaceSearchResults); + client.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(noNamespaceSearchResults) + ); const count = noNamespaceSearchResults.hits.hits.length; const response = await savedObjectsRepository.find({ type }); @@ -2154,7 +2256,9 @@ describe('SavedObjectsRepository', () => { it(`formats the ES response when there is a namespace`, async () => { const namespacedSearchResults = generateSearchResults(namespace); - callAdminCluster.mockReturnValue(namespacedSearchResults); + client.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(namespacedSearchResults) + ); const count = namespacedSearchResults.hits.hits.length; const response = await savedObjectsRepository.find({ type, namespaces: [namespace] }); @@ -2298,35 +2402,57 @@ describe('SavedObjectsRepository', () => { const getSuccess = async (type, id, options) => { const response = getMockGetResponse({ type, id, namespace: options?.namespace }); - callAdminCluster.mockResolvedValue(response); + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); const result = await savedObjectsRepository.get(type, id, options); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(client.get).toHaveBeenCalledTimes(1); return result; }; - describe('cluster calls', () => { + describe('client calls', () => { it(`should use the ES get action`, async () => { await getSuccess(type, id); - expectClusterCalls('get'); + expect(client.get).toHaveBeenCalledTimes(1); }); it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { await getSuccess(type, id, { namespace }); - expectClusterCallArgs({ id: `${namespace}:${type}:${id}` }); + expect(client.get).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${namespace}:${type}:${id}`, + }), + expect.anything() + ); }); it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { await getSuccess(type, id); - expectClusterCallArgs({ id: `${type}:${id}` }); + expect(client.get).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${type}:${id}`, + }), + expect.anything() + ); }); it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { await getSuccess(NAMESPACE_AGNOSTIC_TYPE, id, { namespace }); - expectClusterCallArgs({ id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}` }); + expect(client.get).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}`, + }), + expect.anything() + ); - callAdminCluster.mockReset(); + client.get.mockClear(); await getSuccess(MULTI_NAMESPACE_TYPE, id, { namespace }); - expectClusterCallArgs({ id: `${MULTI_NAMESPACE_TYPE}:${id}` }); + expect(client.get).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${MULTI_NAMESPACE_TYPE}:${id}`, + }), + expect.anything() + ); }); }); @@ -2339,41 +2465,37 @@ describe('SavedObjectsRepository', () => { it(`throws when type is invalid`, async () => { await expectNotFoundError('unknownType', id); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.get).not.toHaveBeenCalled(); }); it(`throws when type is hidden`, async () => { await expectNotFoundError(HIDDEN_TYPE, id); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.get).not.toHaveBeenCalled(); }); it(`throws when ES is unable to find the document during get`, async () => { - callAdminCluster.mockResolvedValue({ found: false }); + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) + ); await expectNotFoundError(type, id); - expectClusterCalls('get'); + expect(client.get).toHaveBeenCalledTimes(1); }); it(`throws when ES is unable to find the index during get`, async () => { - callAdminCluster.mockResolvedValue({ status: 404 }); + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + ); await expectNotFoundError(type, id); - expectClusterCalls('get'); + expect(client.get).toHaveBeenCalledTimes(1); }); it(`throws when type is multi-namespace and the document exists, but not in this namespace`, async () => { const response = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id, namespace }); - callAdminCluster.mockResolvedValue(response); - await expectNotFoundError(MULTI_NAMESPACE_TYPE, id, { namespace: 'bar-namespace' }); - expectClusterCalls('get'); - }); - }); - - describe('migration', () => { - it(`waits until migrations are complete before proceeding`, async () => { - migrator.runMigrations = jest.fn(async () => - expect(callAdminCluster).not.toHaveBeenCalled() + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) ); - await expect(getSuccess(type, id)).resolves.toBeDefined(); - expect(migrator.runMigrations).toHaveBeenCalledTimes(1); + await expectNotFoundError(MULTI_NAMESPACE_TYPE, id, { namespace: 'bar-namespace' }); + expect(client.get).toHaveBeenCalledTimes(1); }); }); @@ -2419,68 +2541,93 @@ describe('SavedObjectsRepository', () => { const isMultiNamespace = registry.isMultiNamespace(type); if (isMultiNamespace) { const response = getMockGetResponse({ type, id, namespace: options?.namespace }); - callAdminCluster.mockResolvedValueOnce(response); // this._callCluster('get', ...) + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); } - callAdminCluster.mockImplementation((method, params) => ({ - _id: params.id, - ...mockVersionProps, - _index: '.kibana', - get: { - found: true, - _source: { - type, - ...mockTimestampFields, - [type]: { - [field]: 8468, - defaultIndex: 'logstash-*', + client.update.mockImplementation((params) => + elasticsearchClientMock.createSuccessTransportRequestPromise({ + _id: params.id, + ...mockVersionProps, + _index: '.kibana', + get: { + found: true, + _source: { + type, + ...mockTimestampFields, + [type]: { + [field]: 8468, + defaultIndex: 'logstash-*', + }, }, }, - }, - })); + }) + ); + const result = await savedObjectsRepository.incrementCounter(type, id, field, options); - expect(callAdminCluster).toHaveBeenCalledTimes(isMultiNamespace ? 2 : 1); + expect(client.get).toHaveBeenCalledTimes(isMultiNamespace ? 1 : 0); return result; }; - describe('cluster calls', () => { + describe('client calls', () => { it(`should use the ES update action if type is not multi-namespace`, async () => { await incrementCounterSuccess(type, id, field, { namespace }); - expectClusterCalls('update'); + expect(client.update).toHaveBeenCalledTimes(1); }); it(`should use the ES get action then update action if type is multi-namespace, ID is defined, and overwrite=true`, async () => { await incrementCounterSuccess(MULTI_NAMESPACE_TYPE, id, field, { namespace }); - expectClusterCalls('get', 'update'); + expect(client.get).toHaveBeenCalledTimes(1); + expect(client.update).toHaveBeenCalledTimes(1); }); it(`defaults to a refresh setting of wait_for`, async () => { await incrementCounterSuccess(type, id, field, { namespace }); - expectClusterCallArgs({ refresh: 'wait_for' }); - }); - - it(`accepts a custom refresh setting`, async () => { - const refresh = 'foo'; - await incrementCounterSuccess(type, id, field, { namespace, refresh }); - expectClusterCallArgs({ refresh }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + refresh: 'wait_for', + }), + expect.anything() + ); }); it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { await incrementCounterSuccess(type, id, field, { namespace }); - expectClusterCallArgs({ id: `${namespace}:${type}:${id}` }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${namespace}:${type}:${id}`, + }), + expect.anything() + ); }); it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { await incrementCounterSuccess(type, id, field); - expectClusterCallArgs({ id: `${type}:${id}` }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${type}:${id}`, + }), + expect.anything() + ); }); it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { await incrementCounterSuccess(NAMESPACE_AGNOSTIC_TYPE, id, field, { namespace }); - expectClusterCallArgs({ id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}` }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}`, + }), + expect.anything() + ); - callAdminCluster.mockReset(); + client.update.mockClear(); await incrementCounterSuccess(MULTI_NAMESPACE_TYPE, id, field, { namespace }); - expectClusterCallArgs({ id: `${MULTI_NAMESPACE_TYPE}:${id}` }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${MULTI_NAMESPACE_TYPE}:${id}`, + }), + expect.anything() + ); }); }); @@ -2496,7 +2643,7 @@ describe('SavedObjectsRepository', () => { await expect( savedObjectsRepository.incrementCounter(type, id, field) ).rejects.toThrowError(`"type" argument must be a string`); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.update).not.toHaveBeenCalled(); }; await test(null); @@ -2510,7 +2657,7 @@ describe('SavedObjectsRepository', () => { await expect( savedObjectsRepository.incrementCounter(type, id, field) ).rejects.toThrowError(`"counterFieldName" argument must be a string`); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.update).not.toHaveBeenCalled(); }; await test(null); @@ -2521,12 +2668,12 @@ describe('SavedObjectsRepository', () => { it(`throws when type is invalid`, async () => { await expectUnsupportedTypeError('unknownType', id, field); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.update).not.toHaveBeenCalled(); }); it(`throws when type is hidden`, async () => { await expectUnsupportedTypeError(HIDDEN_TYPE, id, field); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.update).not.toHaveBeenCalled(); }); it(`throws when there is a conflict with an existing multi-namespace saved object (get)`, async () => { @@ -2535,11 +2682,13 @@ describe('SavedObjectsRepository', () => { id, namespace: 'bar-namespace', }); - callAdminCluster.mockResolvedValue(response); // this._callCluster('get', ...) + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); await expect( savedObjectsRepository.incrementCounter(MULTI_NAMESPACE_TYPE, id, field, { namespace }) ).rejects.toThrowError(createConflictError(MULTI_NAMESPACE_TYPE, id)); - expectClusterCalls('get'); + expect(client.get).toHaveBeenCalledTimes(1); }); }); @@ -2548,16 +2697,6 @@ describe('SavedObjectsRepository', () => { migrator.migrateDocument.mockImplementation(mockMigrateDocument); }); - it(`waits until migrations are complete before proceeding`, async () => { - migrator.runMigrations = jest.fn(async () => - expect(callAdminCluster).not.toHaveBeenCalled() - ); - await expect( - incrementCounterSuccess(type, id, field, { namespace }) - ).resolves.toBeDefined(); - expect(migrator.runMigrations).toHaveBeenCalledTimes(1); - }); - it(`migrates a document and serializes the migrated doc`, async () => { const migrationVersion = mockMigrationVersion; await incrementCounterSuccess(type, id, field, { migrationVersion }); @@ -2572,22 +2711,24 @@ describe('SavedObjectsRepository', () => { describe('returns', () => { it(`formats the ES response`, async () => { - callAdminCluster.mockImplementation((method, params) => ({ - _id: params.id, - ...mockVersionProps, - _index: '.kibana', - get: { - found: true, - _source: { - type: 'config', - ...mockTimestampFields, - config: { - buildNum: 8468, - defaultIndex: 'logstash-*', + client.update.mockImplementation((params) => + elasticsearchClientMock.createSuccessTransportRequestPromise({ + _id: params.id, + ...mockVersionProps, + _index: '.kibana', + get: { + found: true, + _source: { + type: 'config', + ...mockTimestampFields, + config: { + buildNum: 8468, + defaultIndex: 'logstash-*', + }, }, }, - }, - })); + }) + ); const response = await savedObjectsRepository.incrementCounter( 'config', @@ -2623,7 +2764,9 @@ describe('SavedObjectsRepository', () => { // mock a document that exists in two namespaces const mockResponse = getMockGetResponse({ type, id }); mockResponse._source.namespaces = namespaces; - callAdminCluster.mockResolvedValueOnce(mockResponse); // this._callCluster('get', ...) + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(mockResponse) + ); }; const deleteFromNamespacesSuccess = async ( @@ -2633,71 +2776,96 @@ describe('SavedObjectsRepository', () => { currentNamespaces, options ) => { - mockGetResponse(type, id, currentNamespaces); // this._callCluster('get', ...) - const isDelete = currentNamespaces.every((namespace) => namespaces.includes(namespace)); - callAdminCluster.mockResolvedValue({ - _id: `${type}:${id}`, - ...mockVersionProps, - result: isDelete ? 'deleted' : 'updated', - }); // this._writeToCluster('delete', ...) *or* this._writeToCluster('update', ...) - const result = await savedObjectsRepository.deleteFromNamespaces( - type, - id, - namespaces, - options + mockGetResponse(type, id, currentNamespaces); + client.delete.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + _id: `${type}:${id}`, + ...mockVersionProps, + result: 'deleted', + }) ); - expect(callAdminCluster).toHaveBeenCalledTimes(2); - return result; + client.update.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + _id: `${type}:${id}`, + ...mockVersionProps, + result: 'updated', + }) + ); + + return await savedObjectsRepository.deleteFromNamespaces(type, id, namespaces, options); }; - describe('cluster calls', () => { + describe('client calls', () => { describe('delete action', () => { const deleteFromNamespacesSuccessDelete = async (expectFn, options, _type = type) => { const test = async (namespaces) => { await deleteFromNamespacesSuccess(_type, id, namespaces, namespaces, options); expectFn(); - callAdminCluster.mockReset(); + client.delete.mockClear(); + client.get.mockClear(); }; await test([namespace1]); await test([namespace1, namespace2]); }; it(`should use ES get action then delete action if the object has no namespaces remaining`, async () => { - const expectFn = () => expectClusterCalls('get', 'delete'); + const expectFn = () => { + expect(client.delete).toHaveBeenCalledTimes(1); + expect(client.get).toHaveBeenCalledTimes(1); + }; await deleteFromNamespacesSuccessDelete(expectFn); }); it(`formats the ES requests`, async () => { const expectFn = () => { - expectClusterCallArgs({ id: `${type}:${id}` }, 1); + expect(client.delete).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${type}:${id}`, + }), + expect.anything() + ); + const versionProperties = { if_seq_no: mockVersionProps._seq_no, if_primary_term: mockVersionProps._primary_term, }; - expectClusterCallArgs({ id: `${type}:${id}`, ...versionProperties }, 2); + expect(client.delete).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${type}:${id}`, + ...versionProperties, + }), + expect.anything() + ); }; await deleteFromNamespacesSuccessDelete(expectFn); }); it(`defaults to a refresh setting of wait_for`, async () => { await deleteFromNamespacesSuccessDelete(() => - expectClusterCallArgs({ refresh: 'wait_for' }, 2) + expect(client.delete).toHaveBeenCalledWith( + expect.objectContaining({ + refresh: 'wait_for', + }), + expect.anything() + ) ); }); - it(`accepts a custom refresh setting`, async () => { - const refresh = 'foo'; - const expectFn = () => expectClusterCallArgs({ refresh }, 2); - await deleteFromNamespacesSuccessDelete(expectFn, { refresh }); - }); - it(`should use default index`, async () => { - const expectFn = () => expectClusterCallArgs({ index: '.kibana-test' }, 2); + const expectFn = () => + expect(client.delete).toHaveBeenCalledWith( + expect.objectContaining({ index: '.kibana-test' }), + expect.anything() + ); await deleteFromNamespacesSuccessDelete(expectFn); }); it(`should use custom index`, async () => { - const expectFn = () => expectClusterCallArgs({ index: 'custom' }, 2); + const expectFn = () => + expect(client.delete).toHaveBeenCalledWith( + expect.objectContaining({ index: 'custom' }), + expect.anything() + ); await deleteFromNamespacesSuccessDelete(expectFn, {}, MULTI_NAMESPACE_CUSTOM_INDEX_TYPE); }); }); @@ -2708,55 +2876,73 @@ describe('SavedObjectsRepository', () => { const currentNamespaces = [namespace1].concat(remaining); await deleteFromNamespacesSuccess(_type, id, [namespace1], currentNamespaces, options); expectFn(); - callAdminCluster.mockReset(); + client.get.mockClear(); + client.update.mockClear(); }; await test([namespace2]); await test([namespace2, namespace3]); }; it(`should use ES get action then update action if the object has one or more namespaces remaining`, async () => { - await deleteFromNamespacesSuccessUpdate(() => expectClusterCalls('get', 'update')); + const expectFn = () => { + expect(client.update).toHaveBeenCalledTimes(1); + expect(client.get).toHaveBeenCalledTimes(1); + }; + await deleteFromNamespacesSuccessUpdate(expectFn); }); it(`formats the ES requests`, async () => { let ctr = 0; const expectFn = () => { - expectClusterCallArgs({ id: `${type}:${id}` }, 1); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${type}:${id}`, + }), + expect.anything() + ); const namespaces = ctr++ === 0 ? [namespace2] : [namespace2, namespace3]; const versionProperties = { if_seq_no: mockVersionProps._seq_no, if_primary_term: mockVersionProps._primary_term, }; - expectClusterCallArgs( - { + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ id: `${type}:${id}`, ...versionProperties, body: { doc: { ...mockTimestampFields, namespaces } }, - }, - 2 + }), + expect.anything() ); }; await deleteFromNamespacesSuccessUpdate(expectFn); }); it(`defaults to a refresh setting of wait_for`, async () => { - const expectFn = () => expectClusterCallArgs({ refresh: 'wait_for' }, 2); + const expectFn = () => + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + refresh: 'wait_for', + }), + expect.anything() + ); await deleteFromNamespacesSuccessUpdate(expectFn); }); - it(`accepts a custom refresh setting`, async () => { - const refresh = 'foo'; - const expectFn = () => expectClusterCallArgs({ refresh }, 2); - await deleteFromNamespacesSuccessUpdate(expectFn, { refresh }); - }); - it(`should use default index`, async () => { - const expectFn = () => expectClusterCallArgs({ index: '.kibana-test' }, 2); + const expectFn = () => + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ index: '.kibana-test' }), + expect.anything() + ); await deleteFromNamespacesSuccessUpdate(expectFn); }); it(`should use custom index`, async () => { - const expectFn = () => expectClusterCallArgs({ index: 'custom' }, 2); + const expectFn = () => + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ index: 'custom' }), + expect.anything() + ); await deleteFromNamespacesSuccessUpdate(expectFn, {}, MULTI_NAMESPACE_CUSTOM_INDEX_TYPE); }); }); @@ -2776,19 +2962,22 @@ describe('SavedObjectsRepository', () => { it(`throws when type is invalid`, async () => { await expectNotFoundError('unknownType', id, [namespace1, namespace2]); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.delete).not.toHaveBeenCalled(); + expect(client.update).not.toHaveBeenCalled(); }); it(`throws when type is hidden`, async () => { await expectNotFoundError(HIDDEN_TYPE, id, [namespace1, namespace2]); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.delete).not.toHaveBeenCalled(); + expect(client.update).not.toHaveBeenCalled(); }); it(`throws when type is not namespace-agnostic`, async () => { const test = async (type) => { const message = `${type} doesn't support multiple namespaces`; await expectBadRequestError(type, id, [namespace1, namespace2], message); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.delete).not.toHaveBeenCalled(); + expect(client.update).not.toHaveBeenCalled(); }; await test('index-pattern'); await test(NAMESPACE_AGNOSTIC_TYPE); @@ -2798,71 +2987,78 @@ describe('SavedObjectsRepository', () => { const test = async (namespaces) => { const message = 'namespaces must be a non-empty array of strings'; await expectBadRequestError(type, id, namespaces, message); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.delete).not.toHaveBeenCalled(); + expect(client.update).not.toHaveBeenCalled(); }; await test([]); }); it(`throws when ES is unable to find the document during get`, async () => { - callAdminCluster.mockResolvedValue({ found: false }); // this._callCluster('get', ...) + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) + ); await expectNotFoundError(type, id, [namespace1, namespace2]); - expectClusterCalls('get'); + expect(client.get).toHaveBeenCalledTimes(1); }); it(`throws when ES is unable to find the index during get`, async () => { - callAdminCluster.mockResolvedValue({ status: 404 }); // this._callCluster('get', ...) + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + ); await expectNotFoundError(type, id, [namespace1, namespace2]); - expectClusterCalls('get'); + expect(client.get).toHaveBeenCalledTimes(1); }); it(`throws when the document exists, but not in this namespace`, async () => { - mockGetResponse(type, id, [namespace1]); // this._callCluster('get', ...) + mockGetResponse(type, id, [namespace1]); await expectNotFoundError(type, id, [namespace1], { namespace: 'some-other-namespace' }); - expectClusterCalls('get'); + expect(client.get).toHaveBeenCalledTimes(1); }); it(`throws when ES is unable to find the document during delete`, async () => { - mockGetResponse(type, id, [namespace1]); // this._callCluster('get', ...) - callAdminCluster.mockResolvedValue({ result: 'not_found' }); // this._writeToCluster('delete', ...) + mockGetResponse(type, id, [namespace1]); + client.delete.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ result: 'not_found' }) + ); await expectNotFoundError(type, id, [namespace1]); - expectClusterCalls('get', 'delete'); + expect(client.get).toHaveBeenCalledTimes(1); + expect(client.delete).toHaveBeenCalledTimes(1); }); it(`throws when ES is unable to find the index during delete`, async () => { - mockGetResponse(type, id, [namespace1]); // this._callCluster('get', ...) - callAdminCluster.mockResolvedValue({ error: { type: 'index_not_found_exception' } }); // this._writeToCluster('delete', ...) + mockGetResponse(type, id, [namespace1]); + client.delete.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + error: { type: 'index_not_found_exception' }, + }) + ); await expectNotFoundError(type, id, [namespace1]); - expectClusterCalls('get', 'delete'); + expect(client.get).toHaveBeenCalledTimes(1); + expect(client.delete).toHaveBeenCalledTimes(1); }); it(`throws when ES returns an unexpected response`, async () => { - mockGetResponse(type, id, [namespace1]); // this._callCluster('get', ...) - callAdminCluster.mockResolvedValue({ result: 'something unexpected' }); // this._writeToCluster('delete', ...) + mockGetResponse(type, id, [namespace1]); + client.delete.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + result: 'something unexpected', + }) + ); await expect( savedObjectsRepository.deleteFromNamespaces(type, id, [namespace1]) ).rejects.toThrowError('Unexpected Elasticsearch DELETE response'); - expectClusterCalls('get', 'delete'); + expect(client.get).toHaveBeenCalledTimes(1); + expect(client.delete).toHaveBeenCalledTimes(1); }); it(`throws when ES is unable to find the document during update`, async () => { - mockGetResponse(type, id, [namespace1, namespace2]); // this._callCluster('get', ...) - callAdminCluster.mockResolvedValue({ status: 404 }); // this._writeToCluster('update', ...) - await expectNotFoundError(type, id, [namespace1]); - expectClusterCalls('get', 'update'); - }); - }); - - describe('migration', () => { - it(`waits until migrations are complete before proceeding`, async () => { - let callAdminClusterCount = 0; - migrator.runMigrations = jest.fn(async () => - // runMigrations should resolve before callAdminCluster is initiated - expect(callAdminCluster).toHaveBeenCalledTimes(callAdminClusterCount++) + mockGetResponse(type, id, [namespace1, namespace2]); + client.update.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) ); - await expect( - deleteFromNamespacesSuccess(type, id, [namespace1], [namespace1]) - ).resolves.toBeDefined(); - expect(migrator.runMigrations).toHaveReturnedTimes(2); + await expectNotFoundError(type, id, [namespace1]); + expect(client.get).toHaveBeenCalledTimes(1); + expect(client.update).toHaveBeenCalledTimes(1); }); }); @@ -2871,7 +3067,7 @@ describe('SavedObjectsRepository', () => { const test = async (namespaces) => { const result = await deleteFromNamespacesSuccess(type, id, namespaces, namespaces); expect(result).toEqual({}); - callAdminCluster.mockReset(); + client.delete.mockClear(); }; await test([namespace1]); await test([namespace1, namespace2]); @@ -2887,7 +3083,7 @@ describe('SavedObjectsRepository', () => { currentNamespaces ); expect(result).toEqual({}); - callAdminCluster.mockReset(); + client.delete.mockClear(); }; await test([namespace2]); await test([namespace2, namespace3]); @@ -2918,47 +3114,61 @@ describe('SavedObjectsRepository', () => { const updateSuccess = async (type, id, attributes, options) => { if (registry.isMultiNamespace(type)) { const mockGetResponse = getMockGetResponse({ type, id, namespace: options?.namespace }); - callAdminCluster.mockResolvedValueOnce(mockGetResponse); // this._callCluster('get', ...) + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(mockGetResponse) + ); } - callAdminCluster.mockResolvedValue({ - _id: `${type}:${id}`, - ...mockVersionProps, - result: 'updated', - // don't need the rest of the source for test purposes, just the namespace and namespaces attributes - get: { - _source: { namespaces: [options?.namespace ?? 'default'], namespace: options?.namespace }, - }, - }); // this._writeToCluster('update', ...) + client.update.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + _id: `${type}:${id}`, + ...mockVersionProps, + result: 'updated', + // don't need the rest of the source for test purposes, just the namespace and namespaces attributes + get: { + _source: { + namespaces: [options?.namespace ?? 'default'], + namespace: options?.namespace, + }, + }, + }) + ); const result = await savedObjectsRepository.update(type, id, attributes, options); - expect(callAdminCluster).toHaveBeenCalledTimes(registry.isMultiNamespace(type) ? 2 : 1); + expect(client.get).toHaveBeenCalledTimes(registry.isMultiNamespace(type) ? 1 : 0); return result; }; - describe('cluster calls', () => { + describe('client calls', () => { it(`should use the ES get action then update action when type is multi-namespace`, async () => { await updateSuccess(MULTI_NAMESPACE_TYPE, id, attributes); - expectClusterCalls('get', 'update'); + expect(client.get).toHaveBeenCalledTimes(1); + expect(client.update).toHaveBeenCalledTimes(1); }); it(`should use the ES update action when type is not multi-namespace`, async () => { await updateSuccess(type, id, attributes); - expectClusterCalls('update'); + expect(client.update).toHaveBeenCalledTimes(1); }); it(`defaults to no references array`, async () => { await updateSuccess(type, id, attributes); - expectClusterCallArgs({ - body: { doc: expect.not.objectContaining({ references: expect.anything() }) }, - }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + body: { doc: expect.not.objectContaining({ references: expect.anything() }) }, + }), + expect.anything() + ); }); it(`accepts custom references array`, async () => { const test = async (references) => { await updateSuccess(type, id, attributes, { references }); - expectClusterCallArgs({ - body: { doc: expect.objectContaining({ references }) }, - }); - callAdminCluster.mockReset(); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + body: { doc: expect.objectContaining({ references }) }, + }), + expect.anything() + ); + client.update.mockClear(); }; await test(references); await test(['string']); @@ -2968,10 +3178,13 @@ describe('SavedObjectsRepository', () => { it(`doesn't accept custom references if not an array`, async () => { const test = async (references) => { await updateSuccess(type, id, attributes, { references }); - expectClusterCallArgs({ - body: { doc: expect.not.objectContaining({ references: expect.anything() }) }, - }); - callAdminCluster.mockReset(); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + body: { doc: expect.not.objectContaining({ references: expect.anything() }) }, + }), + expect.anything() + ); + client.update.mockClear(); }; await test('string'); await test(123); @@ -2981,13 +3194,12 @@ describe('SavedObjectsRepository', () => { it(`defaults to a refresh setting of wait_for`, async () => { await updateSuccess(type, id, { foo: 'bar' }); - expectClusterCallArgs({ refresh: 'wait_for' }); - }); - - it(`accepts a custom refresh setting`, async () => { - const refresh = 'foo'; - await updateSuccess(type, id, { foo: 'bar' }, { refresh }); - expectClusterCallArgs({ refresh }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + refresh: 'wait_for', + }), + expect.anything() + ); }); it(`defaults to the version of the existing document when type is multi-namespace`, async () => { @@ -2996,47 +3208,70 @@ describe('SavedObjectsRepository', () => { if_seq_no: mockVersionProps._seq_no, if_primary_term: mockVersionProps._primary_term, }; - expectClusterCallArgs(versionProperties, 2); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining(versionProperties), + expect.anything() + ); }); it(`accepts version`, async () => { await updateSuccess(type, id, attributes, { version: encodeHitVersion({ _seq_no: 100, _primary_term: 200 }), }); - expectClusterCallArgs({ if_seq_no: 100, if_primary_term: 200 }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ if_seq_no: 100, if_primary_term: 200 }), + expect.anything() + ); }); it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { await updateSuccess(type, id, attributes, { namespace }); - expectClusterCallArgs({ id: expect.stringMatching(`${namespace}:${type}:${id}`) }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ id: expect.stringMatching(`${namespace}:${type}:${id}`) }), + expect.anything() + ); }); it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { await updateSuccess(type, id, attributes, { references }); - expectClusterCallArgs({ id: expect.stringMatching(`${type}:${id}`) }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ id: expect.stringMatching(`${type}:${id}`) }), + expect.anything() + ); }); it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { await updateSuccess(NAMESPACE_AGNOSTIC_TYPE, id, attributes, { namespace }); - expectClusterCallArgs({ id: expect.stringMatching(`${NAMESPACE_AGNOSTIC_TYPE}:${id}`) }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: expect.stringMatching(`${NAMESPACE_AGNOSTIC_TYPE}:${id}`), + }), + expect.anything() + ); - callAdminCluster.mockReset(); + client.update.mockClear(); await updateSuccess(MULTI_NAMESPACE_TYPE, id, attributes, { namespace }); - expectClusterCallArgs({ id: expect.stringMatching(`${MULTI_NAMESPACE_TYPE}:${id}`) }, 2); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ id: expect.stringMatching(`${MULTI_NAMESPACE_TYPE}:${id}`) }), + expect.anything() + ); }); - it(`includes _sourceIncludes when type is multi-namespace`, async () => { + it(`includes _source_includes when type is multi-namespace`, async () => { await updateSuccess(MULTI_NAMESPACE_TYPE, id, attributes); - expectClusterCallArgs({ _sourceIncludes: ['namespace', 'namespaces'] }, 2); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ _source_includes: ['namespace', 'namespaces'] }), + expect.anything() + ); }); - it(`includes _sourceIncludes when type is not multi-namespace`, async () => { + it(`includes _source_includes when type is not multi-namespace`, async () => { await updateSuccess(type, id, attributes); - expect(callAdminCluster).toHaveBeenLastCalledWith( - expect.any(String), + expect(client.update).toHaveBeenLastCalledWith( expect.objectContaining({ - _sourceIncludes: ['namespace', 'namespaces'], - }) + _source_includes: ['namespace', 'namespaces'], + }), + expect.anything() ); }); }); @@ -3050,49 +3285,45 @@ describe('SavedObjectsRepository', () => { it(`throws when type is invalid`, async () => { await expectNotFoundError('unknownType', id); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.update).not.toHaveBeenCalled(); }); it(`throws when type is hidden`, async () => { await expectNotFoundError(HIDDEN_TYPE, id); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(client.update).not.toHaveBeenCalled(); }); it(`throws when ES is unable to find the document during get`, async () => { - callAdminCluster.mockResolvedValue({ found: false }); // this._callCluster('get', ...) + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) + ); await expectNotFoundError(MULTI_NAMESPACE_TYPE, id); - expectClusterCalls('get'); + expect(client.get).toHaveBeenCalledTimes(1); }); it(`throws when ES is unable to find the index during get`, async () => { - callAdminCluster.mockResolvedValue({ status: 404 }); // this._callCluster('get', ...) + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + ); await expectNotFoundError(MULTI_NAMESPACE_TYPE, id); - expectClusterCalls('get'); + expect(client.get).toHaveBeenCalledTimes(1); }); it(`throws when type is multi-namespace and the document exists, but not in this namespace`, async () => { const response = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id, namespace }); - callAdminCluster.mockResolvedValue(response); // this._callCluster('get', ...) + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); await expectNotFoundError(MULTI_NAMESPACE_TYPE, id, { namespace: 'bar-namespace' }); - expectClusterCalls('get'); + expect(client.get).toHaveBeenCalledTimes(1); }); it(`throws when ES is unable to find the document during update`, async () => { - callAdminCluster.mockResolvedValue({ status: 404 }); // this._writeToCluster('update', ...) + client.update.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + ); await expectNotFoundError(type, id); - expectClusterCalls('update'); - }); - }); - - describe('migration', () => { - it(`waits until migrations are complete before proceeding`, async () => { - let callAdminClusterCount = 0; - migrator.runMigrations = jest.fn(async () => - // runMigrations should resolve before callAdminCluster is initiated - expect(callAdminCluster).toHaveBeenCalledTimes(callAdminClusterCount++) - ); - await expect(updateSuccess(type, id, attributes)).resolves.toBeDefined(); - expect(migrator.runMigrations).toHaveReturnedTimes(1); + expect(client.update).toHaveBeenCalledTimes(1); }); }); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 7a5ac9204627c..8b7b1d62c1b7d 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -19,13 +19,16 @@ import { omit } from 'lodash'; import uuid from 'uuid'; -import { retryCallCluster } from '../../../elasticsearch/legacy'; -import { LegacyAPICaller } from '../../../elasticsearch/'; - +import { + ElasticsearchClient, + DeleteDocumentResponse, + GetResponse, + SearchResponse, +} from '../../../elasticsearch/'; import { getRootPropertiesObjects, IndexMapping } from '../../mappings'; +import { createRepositoryEsClient, RepositoryEsClient } from './repository_es_client'; import { getSearchDsl } from './search_dsl'; import { includedFields } from './included_fields'; -import { decorateEsError } from './decorate_es_error'; import { SavedObjectsErrorHelpers } from './errors'; import { decodeRequestVersion, encodeVersion, encodeHitVersion } from '../../version'; import { KibanaMigrator } from '../../migrations'; @@ -33,6 +36,7 @@ import { SavedObjectsSerializer, SavedObjectSanitizedDoc, SavedObjectsRawDoc, + SavedObjectsRawDocSource, } from '../../serialization'; import { SavedObjectsBulkCreateObject, @@ -74,7 +78,7 @@ const isRight = (either: Either): either is Right => either.tag === 'Right'; export interface SavedObjectsRepositoryOptions { index: string; mappings: IndexMapping; - callCluster: LegacyAPICaller; + client: ElasticsearchClient; typeRegistry: SavedObjectTypeRegistry; serializer: SavedObjectsSerializer; migrator: KibanaMigrator; @@ -95,8 +99,8 @@ export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOpt * @public */ export interface SavedObjectsDeleteByNamespaceOptions extends SavedObjectsBaseOptions { - /** The Elasticsearch Refresh setting for this operation */ - refresh?: MutatingOperationRefreshSetting; + /** The Elasticsearch supports only boolean flag for this operation */ + refresh?: boolean; } const DEFAULT_REFRESH_SETTING = 'wait_for'; @@ -117,7 +121,7 @@ export class SavedObjectsRepository { private _mappings: IndexMapping; private _registry: SavedObjectTypeRegistry; private _allowedTypes: string[]; - private _unwrappedCallCluster: LegacyAPICaller; + private readonly client: RepositoryEsClient; private _serializer: SavedObjectsSerializer; /** @@ -132,7 +136,7 @@ export class SavedObjectsRepository { migrator: KibanaMigrator, typeRegistry: SavedObjectTypeRegistry, indexName: string, - callCluster: LegacyAPICaller, + client: ElasticsearchClient, includedHiddenTypes: string[] = [], injectedConstructor: any = SavedObjectsRepository ): ISavedObjectsRepository { @@ -157,7 +161,7 @@ export class SavedObjectsRepository { typeRegistry, serializer, allowedTypes, - callCluster: retryCallCluster(callCluster), + client, }); } @@ -165,7 +169,7 @@ export class SavedObjectsRepository { const { index, mappings, - callCluster, + client, typeRegistry, serializer, migrator, @@ -183,15 +187,11 @@ export class SavedObjectsRepository { this._index = index; this._mappings = mappings; this._registry = typeRegistry; + this.client = createRepositoryEsClient(client); if (allowedTypes.length === 0) { throw new Error('Empty or missing types for saved object repository!'); } this._allowedTypes = allowedTypes; - - this._unwrappedCallCluster = async (...args: Parameters) => { - await migrator.runMigrations(); - return callCluster(...args); - }; this._serializer = serializer; } @@ -254,17 +254,21 @@ export class SavedObjectsRepository { const raw = this._serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc); - const method = id && overwrite ? 'index' : 'create'; - const response = await this._writeToCluster(method, { + const requestParams = { id: raw._id, index: this.getIndexForType(type), refresh, body: raw._source, - }); + }; + + const { body } = + id && overwrite + ? await this.client.index(requestParams) + : await this.client.create(requestParams); return this._rawToSavedObject({ ...raw, - ...response, + ...body, }); } @@ -322,12 +326,14 @@ export class SavedObjectsRepository { _source: ['type', 'namespaces'], })); const bulkGetResponse = bulkGetDocs.length - ? await this._callCluster('mget', { - body: { - docs: bulkGetDocs, + ? await this.client.mget( + { + body: { + docs: bulkGetDocs, + }, }, - ignore: [404], - }) + { ignore: [404] } + ) : undefined; let bulkRequestIndexCounter = 0; @@ -341,8 +347,8 @@ export class SavedObjectsRepository { let savedObjectNamespaces; const { esRequestIndex, object, method } = expectedBulkGetResult.value; if (esRequestIndex !== undefined) { - const indexFound = bulkGetResponse.status !== 404; - const actualResult = indexFound ? bulkGetResponse.docs[esRequestIndex] : undefined; + const indexFound = bulkGetResponse?.statusCode !== 404; + const actualResult = indexFound ? bulkGetResponse?.body.docs[esRequestIndex] : undefined; const docFound = indexFound && actualResult.found === true; if (docFound && !this.rawDocExistsInNamespace(actualResult, namespace)) { const { id, type } = object; @@ -395,7 +401,7 @@ export class SavedObjectsRepository { }); const bulkResponse = bulkCreateParams.length - ? await this._writeToCluster('bulk', { + ? await this.client.bulk({ refresh, body: bulkCreateParams, }) @@ -409,7 +415,7 @@ export class SavedObjectsRepository { const { requestedId, rawMigratedDoc, esRequestIndex } = expectedResult.value; const { error, ...rawResponse } = Object.values( - bulkResponse.items[esRequestIndex] + bulkResponse?.body.items[esRequestIndex] )[0] as any; if (error) { @@ -466,18 +472,20 @@ export class SavedObjectsRepository { namespaces: remainingNamespaces, }; - const updateResponse = await this._writeToCluster('update', { - id: rawId, - index: this.getIndexForType(type), - ...getExpectedVersionProperties(undefined, preflightResult), - refresh, - ignore: [404], - body: { - doc, + const { statusCode } = await this.client.update( + { + id: rawId, + index: this.getIndexForType(type), + ...getExpectedVersionProperties(undefined, preflightResult), + refresh, + body: { + doc, + }, }, - }); + { ignore: [404] } + ); - if (updateResponse.status === 404) { + if (statusCode === 404) { // see "404s from missing index" above throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } @@ -485,22 +493,23 @@ export class SavedObjectsRepository { } } - const deleteResponse = await this._writeToCluster('delete', { - id: rawId, - index: this.getIndexForType(type), - ...getExpectedVersionProperties(undefined, preflightResult), - refresh, - ignore: [404], - }); + const { body, statusCode } = await this.client.delete( + { + id: rawId, + index: this.getIndexForType(type), + ...getExpectedVersionProperties(undefined, preflightResult), + refresh, + }, + { ignore: [404] } + ); - const deleted = deleteResponse.result === 'deleted'; + const deleted = body.result === 'deleted'; if (deleted) { return {}; } - const deleteDocNotFound = deleteResponse.result === 'not_found'; - const deleteIndexNotFound = - deleteResponse.error && deleteResponse.error.type === 'index_not_found_exception'; + const deleteDocNotFound = body.result === 'not_found'; + const deleteIndexNotFound = body.error && body.error.type === 'index_not_found_exception'; if (deleteDocNotFound || deleteIndexNotFound) { // see "404s from missing index" above throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); @@ -510,7 +519,7 @@ export class SavedObjectsRepository { `Unexpected Elasticsearch DELETE response: ${JSON.stringify({ type, id, - response: deleteResponse, + response: { body, statusCode }, })}` ); } @@ -529,17 +538,16 @@ export class SavedObjectsRepository { throw new TypeError(`namespace is required, and must be a string`); } - const { refresh = DEFAULT_REFRESH_SETTING } = options; const allTypes = Object.keys(getRootPropertiesObjects(this._mappings)); const typesToUpdate = allTypes.filter((type) => !this._registry.isNamespaceAgnostic(type)); - const updateOptions = { - index: this.getIndicesForTypes(typesToUpdate), - ignore: [404], - refresh, - body: { - script: { - source: ` + const { body } = await this.client.updateByQuery( + { + index: this.getIndicesForTypes(typesToUpdate), + refresh: options.refresh, + body: { + script: { + source: ` if (!ctx._source.containsKey('namespaces')) { ctx.op = "delete"; } else { @@ -549,18 +557,20 @@ export class SavedObjectsRepository { } } `, - lang: 'painless', - params: { namespace: getNamespaceString(namespace) }, + lang: 'painless', + params: { namespace: getNamespaceString(namespace) }, + }, + conflicts: 'proceed', + ...getSearchDsl(this._mappings, this._registry, { + namespaces: namespace ? [namespace] : undefined, + type: typesToUpdate, + }), }, - conflicts: 'proceed', - ...getSearchDsl(this._mappings, this._registry, { - namespaces: namespace ? [namespace] : undefined, - type: typesToUpdate, - }), }, - }; + { ignore: [404] } + ); - return await this._writeToCluster('updateByQuery', updateOptions); + return body; } /** @@ -639,7 +649,6 @@ export class SavedObjectsRepository { size: perPage, from: perPage * (page - 1), _source: includedFields(type, fields), - ignore: [404], rest_total_hits_as_int: true, preference, body: { @@ -658,9 +667,10 @@ export class SavedObjectsRepository { }, }; - const response = await this._callCluster('search', esOptions); - - if (response.status === 404) { + const { body, statusCode } = await this.client.search>(esOptions, { + ignore: [404], + }); + if (statusCode === 404) { // 404 is only possible here if the index is missing, which // we don't want to leak, see "404s from missing index" above return { @@ -674,14 +684,14 @@ export class SavedObjectsRepository { return { page, per_page: perPage, - total: response.hits.total, - saved_objects: response.hits.hits.map( + total: body.hits.total, + saved_objects: body.hits.hits.map( (hit: SavedObjectsRawDoc): SavedObjectsFindResult => ({ ...this._rawToSavedObject(hit), score: (hit as any)._score, }) ), - }; + } as SavedObjectsFindResponse; } /** @@ -742,12 +752,14 @@ export class SavedObjectsRepository { _source: includedFields(type, fields), })); const bulkGetResponse = bulkGetDocs.length - ? await this._callCluster('mget', { - body: { - docs: bulkGetDocs, + ? await this.client.mget( + { + body: { + docs: bulkGetDocs, + }, }, - ignore: [404], - }) + { ignore: [404] } + ) : undefined; return { @@ -757,7 +769,7 @@ export class SavedObjectsRepository { } const { type, id, esRequestIndex } = expectedResult.value; - const doc = bulkGetResponse.docs[esRequestIndex]; + const doc = bulkGetResponse?.body.docs[esRequestIndex]; if (!doc.found || !this.rawDocExistsInNamespace(doc, namespace)) { return ({ @@ -808,24 +820,26 @@ export class SavedObjectsRepository { const { namespace } = options; - const response = await this._callCluster('get', { - id: this._serializer.generateRawId(namespace, type, id), - index: this.getIndexForType(type), - ignore: [404], - }); + const { body, statusCode } = await this.client.get>( + { + id: this._serializer.generateRawId(namespace, type, id), + index: this.getIndexForType(type), + }, + { ignore: [404] } + ); - const docNotFound = response.found === false; - const indexNotFound = response.status === 404; - if (docNotFound || indexNotFound || !this.rawDocExistsInNamespace(response, namespace)) { + const docNotFound = body.found === false; + const indexNotFound = statusCode === 404; + if (docNotFound || indexNotFound || !this.rawDocExistsInNamespace(body, namespace)) { // see "404s from missing index" above throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } - const { updated_at: updatedAt } = response._source; + const { updated_at: updatedAt } = body._source; - let namespaces = []; + let namespaces: string[] = []; if (!this._registry.isNamespaceAgnostic(type)) { - namespaces = response._source.namespaces ?? [getNamespaceString(response._source.namespace)]; + namespaces = body._source.namespaces ?? [getNamespaceString(body._source.namespace)]; } return { @@ -833,10 +847,10 @@ export class SavedObjectsRepository { type, namespaces, ...(updatedAt && { updated_at: updatedAt }), - version: encodeHitVersion(response), - attributes: response._source[type], - references: response._source.references || [], - migrationVersion: response._source.migrationVersion, + version: encodeHitVersion(body), + attributes: body._source[type], + references: body._source.references || [], + migrationVersion: body._source.migrationVersion, }; } @@ -876,35 +890,37 @@ export class SavedObjectsRepository { ...(Array.isArray(references) && { references }), }; - const updateResponse = await this._writeToCluster('update', { - id: this._serializer.generateRawId(namespace, type, id), - index: this.getIndexForType(type), - ...getExpectedVersionProperties(version, preflightResult), - refresh, - ignore: [404], - body: { - doc, + const { body, statusCode } = await this.client.update( + { + id: this._serializer.generateRawId(namespace, type, id), + index: this.getIndexForType(type), + ...getExpectedVersionProperties(version, preflightResult), + refresh, + + body: { + doc, + }, + _source_includes: ['namespace', 'namespaces'], }, - _sourceIncludes: ['namespace', 'namespaces'], - }); + { ignore: [404] } + ); - if (updateResponse.status === 404) { + if (statusCode === 404) { // see "404s from missing index" above throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } let namespaces = []; if (!this._registry.isNamespaceAgnostic(type)) { - namespaces = updateResponse.get._source.namespaces ?? [ - getNamespaceString(updateResponse.get._source.namespace), - ]; + namespaces = body.get._source.namespaces ?? [getNamespaceString(body.get._source.namespace)]; } return { id, type, updated_at: time, - version: encodeHitVersion(updateResponse), + // @ts-expect-error update doesn't have _seq_no, _primary_term as Record / any in LP + version: encodeHitVersion(body), namespaces, references, attributes, @@ -952,18 +968,20 @@ export class SavedObjectsRepository { namespaces: existingNamespaces ? unique(existingNamespaces.concat(namespaces)) : namespaces, }; - const updateResponse = await this._writeToCluster('update', { - id: rawId, - index: this.getIndexForType(type), - ...getExpectedVersionProperties(version, preflightResult), - refresh, - ignore: [404], - body: { - doc, + const { statusCode } = await this.client.update( + { + id: rawId, + index: this.getIndexForType(type), + ...getExpectedVersionProperties(version, preflightResult), + refresh, + body: { + doc, + }, }, - }); + { ignore: [404] } + ); - if (updateResponse.status === 404) { + if (statusCode === 404) { // see "404s from missing index" above throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } @@ -1015,40 +1033,48 @@ export class SavedObjectsRepository { namespaces: remainingNamespaces, }; - const updateResponse = await this._writeToCluster('update', { - id: rawId, - index: this.getIndexForType(type), - ...getExpectedVersionProperties(undefined, preflightResult), - refresh, - ignore: [404], - body: { - doc, + const { statusCode } = await this.client.update( + { + id: rawId, + index: this.getIndexForType(type), + ...getExpectedVersionProperties(undefined, preflightResult), + refresh, + + body: { + doc, + }, }, - }); + { + ignore: [404], + } + ); - if (updateResponse.status === 404) { + if (statusCode === 404) { // see "404s from missing index" above throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } return {}; } else { // if there are no namespaces remaining, delete the saved object - const deleteResponse = await this._writeToCluster('delete', { - id: this._serializer.generateRawId(undefined, type, id), - index: this.getIndexForType(type), - ...getExpectedVersionProperties(undefined, preflightResult), - refresh, - ignore: [404], - }); + const { body, statusCode } = await this.client.delete( + { + id: this._serializer.generateRawId(undefined, type, id), + refresh, + ...getExpectedVersionProperties(undefined, preflightResult), + index: this.getIndexForType(type), + }, + { + ignore: [404], + } + ); - const deleted = deleteResponse.result === 'deleted'; + const deleted = body.result === 'deleted'; if (deleted) { return {}; } - const deleteDocNotFound = deleteResponse.result === 'not_found'; - const deleteIndexNotFound = - deleteResponse.error && deleteResponse.error.type === 'index_not_found_exception'; + const deleteDocNotFound = body.result === 'not_found'; + const deleteIndexNotFound = body.error && body.error.type === 'index_not_found_exception'; if (deleteDocNotFound || deleteIndexNotFound) { // see "404s from missing index" above throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); @@ -1058,7 +1084,7 @@ export class SavedObjectsRepository { `Unexpected Elasticsearch DELETE response: ${JSON.stringify({ type, id, - response: deleteResponse, + response: { body, statusCode }, })}` ); } @@ -1125,12 +1151,16 @@ export class SavedObjectsRepository { _source: ['type', 'namespaces'], })); const bulkGetResponse = bulkGetDocs.length - ? await this._callCluster('mget', { - body: { - docs: bulkGetDocs, + ? await this.client.mget( + { + body: { + docs: bulkGetDocs, + }, }, - ignore: [404], - }) + { + ignore: [404], + } + ) : undefined; let bulkUpdateRequestIndexCounter = 0; @@ -1145,8 +1175,8 @@ export class SavedObjectsRepository { let namespaces; let versionProperties; if (esRequestIndex !== undefined) { - const indexFound = bulkGetResponse.status !== 404; - const actualResult = indexFound ? bulkGetResponse.docs[esRequestIndex] : undefined; + const indexFound = bulkGetResponse?.statusCode !== 404; + const actualResult = indexFound ? bulkGetResponse?.body.docs[esRequestIndex] : undefined; const docFound = indexFound && actualResult.found === true; if (!docFound || !this.rawDocExistsInNamespace(actualResult, namespace)) { return { @@ -1194,11 +1224,11 @@ export class SavedObjectsRepository { const { refresh = DEFAULT_REFRESH_SETTING } = options; const bulkUpdateResponse = bulkUpdateParams.length - ? await this._writeToCluster('bulk', { + ? await this.client.bulk({ refresh, body: bulkUpdateParams, }) - : {}; + : undefined; return { saved_objects: expectedBulkUpdateResults.map((expectedResult) => { @@ -1207,7 +1237,7 @@ export class SavedObjectsRepository { } const { type, id, namespaces, documentToSave, esRequestIndex } = expectedResult.value; - const response = bulkUpdateResponse.items[esRequestIndex]; + const response = bulkUpdateResponse?.body.items[esRequestIndex]; const { error, _seq_no: seqNo, _primary_term: primaryTerm } = Object.values( response )[0] as any; @@ -1283,11 +1313,11 @@ export class SavedObjectsRepository { const raw = this._serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc); - const response = await this._writeToCluster('update', { + const { body } = await this.client.update({ id: raw._id, index: this.getIndexForType(type), refresh, - _source: true, + _source: 'true', body: { script: { source: ` @@ -1315,28 +1345,13 @@ export class SavedObjectsRepository { id, type, updated_at: time, - references: response.get._source.references, - version: encodeHitVersion(response), - attributes: response.get._source[type], + references: body.get._source.references, + // @ts-expect-error + version: encodeHitVersion(body), + attributes: body.get._source[type], }; } - private async _writeToCluster(...args: Parameters) { - try { - return await this._callCluster(...args); - } catch (err) { - throw decorateEsError(err); - } - } - - private async _callCluster(...args: Parameters) { - try { - return await this._unwrappedCallCluster(...args); - } catch (err) { - throw decorateEsError(err); - } - } - /** * Returns index specified by the given type or the default index * @@ -1408,19 +1423,23 @@ export class SavedObjectsRepository { throw new Error(`Cannot make preflight get request for non-multi-namespace type '${type}'.`); } - const response = await this._callCluster('get', { - id: this._serializer.generateRawId(undefined, type, id), - index: this.getIndexForType(type), - ignore: [404], - }); + const { body, statusCode } = await this.client.get>( + { + id: this._serializer.generateRawId(undefined, type, id), + index: this.getIndexForType(type), + }, + { + ignore: [404], + } + ); - const indexFound = response.status !== 404; - const docFound = indexFound && response.found === true; + const indexFound = statusCode !== 404; + const docFound = indexFound && body.found === true; if (docFound) { - if (!this.rawDocExistsInNamespace(response, namespace)) { + if (!this.rawDocExistsInNamespace(body, namespace)) { throw SavedObjectsErrorHelpers.createConflictError(type, id); } - return getSavedObjectNamespaces(namespace, response); + return getSavedObjectNamespaces(namespace, body); } return getSavedObjectNamespaces(namespace); } @@ -1441,18 +1460,20 @@ export class SavedObjectsRepository { } const rawId = this._serializer.generateRawId(undefined, type, id); - const response = await this._callCluster('get', { - id: rawId, - index: this.getIndexForType(type), - ignore: [404], - }); + const { body, statusCode } = await this.client.get>( + { + id: rawId, + index: this.getIndexForType(type), + }, + { ignore: [404] } + ); - const indexFound = response.status !== 404; - const docFound = indexFound && response.found === true; - if (!docFound || !this.rawDocExistsInNamespace(response, namespace)) { + const indexFound = statusCode !== 404; + const docFound = indexFound && body.found === true; + if (!docFound || !this.rawDocExistsInNamespace(body, namespace)) { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } - return response as SavedObjectsRawDoc; + return body as SavedObjectsRawDoc; } } diff --git a/src/core/server/saved_objects/service/lib/repository_es_client.test.mock.ts b/src/core/server/saved_objects/service/lib/repository_es_client.test.mock.ts new file mode 100644 index 0000000000000..3dcf82dae5e46 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/repository_es_client.test.mock.ts @@ -0,0 +1,22 @@ +/* + * 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. + */ +export const retryCallClusterMock = jest.fn((fn) => fn()); +jest.doMock('../../../elasticsearch/client/retry_call_cluster', () => ({ + retryCallCluster: retryCallClusterMock, +})); diff --git a/src/core/server/saved_objects/service/lib/repository_es_client.test.ts b/src/core/server/saved_objects/service/lib/repository_es_client.test.ts new file mode 100644 index 0000000000000..86a984fb67124 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/repository_es_client.test.ts @@ -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 { retryCallClusterMock } from './repository_es_client.test.mock'; + +import { createRepositoryEsClient, RepositoryEsClient } from './repository_es_client'; +import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; +import { SavedObjectsErrorHelpers } from './errors'; + +describe('RepositoryEsClient', () => { + let client: ReturnType; + let repositoryClient: RepositoryEsClient; + + beforeEach(() => { + client = elasticsearchClientMock.createElasticSearchClient(); + repositoryClient = createRepositoryEsClient(client); + retryCallClusterMock.mockClear(); + }); + + it('delegates call to ES client method', async () => { + expect(repositoryClient.bulk).toStrictEqual(expect.any(Function)); + await repositoryClient.bulk({ body: [] }); + expect(client.bulk).toHaveBeenCalledTimes(1); + }); + + it('wraps a method call in retryCallCluster', async () => { + await repositoryClient.bulk({ body: [] }); + expect(retryCallClusterMock).toHaveBeenCalledTimes(1); + }); + + it('sets maxRetries: 0 to delegate retry logic to retryCallCluster', async () => { + expect(repositoryClient.bulk).toStrictEqual(expect.any(Function)); + await repositoryClient.bulk({ body: [] }); + expect(client.bulk).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ maxRetries: 0 }) + ); + }); + + it('transform elasticsearch errors into saved objects errors', async () => { + expect.assertions(1); + client.bulk = jest.fn().mockRejectedValue(new Error('reason')); + try { + await repositoryClient.bulk({ body: [] }); + } catch (e) { + expect(SavedObjectsErrorHelpers.isSavedObjectsClientError(e)).toBe(true); + } + }); +}); diff --git a/src/core/server/saved_objects/service/lib/repository_es_client.ts b/src/core/server/saved_objects/service/lib/repository_es_client.ts new file mode 100644 index 0000000000000..0a759669b1af8 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/repository_es_client.ts @@ -0,0 +1,56 @@ +/* + * 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 type { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; + +import { ElasticsearchClient } from '../../../elasticsearch/'; +import { retryCallCluster } from '../../../elasticsearch/client/retry_call_cluster'; +import { decorateEsError } from './decorate_es_error'; + +const methods = [ + 'bulk', + 'create', + 'delete', + 'get', + 'index', + 'mget', + 'search', + 'update', + 'updateByQuery', +] as const; + +type MethodName = typeof methods[number]; + +export type RepositoryEsClient = Pick; + +export function createRepositoryEsClient(client: ElasticsearchClient): RepositoryEsClient { + return methods.reduce((acc: RepositoryEsClient, key: MethodName) => { + Object.defineProperty(acc, key, { + value: async (params?: unknown, options?: TransportRequestOptions) => { + try { + return await retryCallCluster(() => + (client[key] as Function)(params, { maxRetries: 0, ...options }) + ); + } catch (e) { + throw decorateEsError(e); + } + }, + }); + return acc; + }, {} as RepositoryEsClient); +} diff --git a/src/plugins/home/server/services/sample_data/routes/list.ts b/src/plugins/home/server/services/sample_data/routes/list.ts index 770b3116b74f1..7cce0fa5cccb3 100644 --- a/src/plugins/home/server/services/sample_data/routes/list.ts +++ b/src/plugins/home/server/services/sample_data/routes/list.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import { isBoom } from 'boom'; import { IRouter } from 'src/core/server'; import { SampleDatasetSchema } from '../lib/sample_dataset_registry_types'; import { createIndexName } from '../lib/create_index_name'; @@ -75,8 +74,7 @@ export const createListRoute = (router: IRouter, sampleDatasets: SampleDatasetSc try { await context.core.savedObjects.client.get('dashboard', sampleDataset.overviewDashboard); } catch (err) { - // savedObjectClient.get() throws an boom error when object is not found. - if (isBoom(err) && err.output.statusCode === 404) { + if (context.core.savedObjects.client.errors.isNotFoundError(err)) { sampleDataset.status = NOT_INSTALLED; return; } diff --git a/test/api_integration/apis/saved_objects/migrations.js b/test/api_integration/apis/saved_objects/migrations.ts similarity index 68% rename from test/api_integration/apis/saved_objects/migrations.js rename to test/api_integration/apis/saved_objects/migrations.ts index ed259ccec0114..9997d9710e212 100644 --- a/test/api_integration/apis/saved_objects/migrations.js +++ b/test/api_integration/apis/saved_objects/migrations.ts @@ -23,22 +23,39 @@ import { set } from '@elastic/safer-lodash-set'; import _ from 'lodash'; -import { assert } from 'chai'; +import expect from '@kbn/expect'; +import { ElasticsearchClient, SavedObjectMigrationMap, SavedObjectsType } from 'src/core/server'; +import { SearchResponse } from '../../../../src/core/server/elasticsearch/client'; import { DocumentMigrator, IndexMigrator, + createMigrationEsClient, } from '../../../../src/core/server/saved_objects/migrations/core'; +import { SavedObjectsTypeMappingDefinitions } from '../../../../src/core/server/saved_objects/mappings'; + import { SavedObjectsSerializer, SavedObjectTypeRegistry, } from '../../../../src/core/server/saved_objects'; - -export default ({ getService }) => { - const es = getService('legacyEs'); - const callCluster = (path, ...args) => _.get(es, path).call(es, ...args); +import { FtrProviderContext } from '../../ftr_provider_context'; + +function getLogMock() { + return { + debug() {}, + error() {}, + fatal() {}, + info() {}, + log() {}, + trace() {}, + warn() {}, + get: getLogMock, + }; +} +export default ({ getService }: FtrProviderContext) => { + const esClient = getService('es'); describe('Kibana index migration', () => { - before(() => callCluster('indices.delete', { index: '.migrate-*' })); + before(() => esClient.indices.delete({ index: '.migrate-*' })); it('Migrates an existing index that has never been migrated before', async () => { const index = '.migration-a'; @@ -55,7 +72,7 @@ export default ({ getService }) => { bar: { properties: { mynum: { type: 'integer' } } }, }; - const migrations = { + const migrations: Record = { foo: { '1.0.0': (doc) => set(doc, 'attributes.name', doc.attributes.name.toUpperCase()), }, @@ -66,11 +83,11 @@ export default ({ getService }) => { }, }; - await createIndex({ callCluster, index }); - await createDocs({ callCluster, index, docs: originalDocs }); + await createIndex({ esClient, index }); + await createDocs({ esClient, index, docs: originalDocs }); // Test that unrelated index templates are unaffected - await callCluster('indices.putTemplate', { + await esClient.indices.putTemplate({ name: 'migration_test_a_template', body: { index_patterns: 'migration_test_a', @@ -82,7 +99,7 @@ export default ({ getService }) => { }); // Test that obsolete index templates get removed - await callCluster('indices.putTemplate', { + await esClient.indices.putTemplate({ name: 'migration_a_template', body: { index_patterns: index, @@ -93,29 +110,37 @@ export default ({ getService }) => { }, }); - assert.isTrue(await callCluster('indices.existsTemplate', { name: 'migration_a_template' })); + const migrationATemplate = await esClient.indices.existsTemplate({ + name: 'migration_a_template', + }); + expect(migrationATemplate.body).to.be.ok(); const result = await migrateIndex({ - callCluster, + esClient, index, migrations, mappingProperties, obsoleteIndexTemplatePattern: 'migration_a*', }); - assert.isFalse(await callCluster('indices.existsTemplate', { name: 'migration_a_template' })); - assert.isTrue( - await callCluster('indices.existsTemplate', { name: 'migration_test_a_template' }) - ); + const migrationATemplateAfter = await esClient.indices.existsTemplate({ + name: 'migration_a_template', + }); - assert.deepEqual(_.omit(result, 'elapsedMs'), { + expect(migrationATemplateAfter.body).not.to.be.ok(); + const migrationTestATemplateAfter = await esClient.indices.existsTemplate({ + name: 'migration_test_a_template', + }); + + expect(migrationTestATemplateAfter.body).to.be.ok(); + expect(_.omit(result, 'elapsedMs')).to.eql({ destIndex: '.migration-a_2', sourceIndex: '.migration-a_1', status: 'migrated', }); // The docs in the original index are unchanged - assert.deepEqual(await fetchDocs({ callCluster, index: `${index}_1` }), [ + expect(await fetchDocs(esClient, `${index}_1`)).to.eql([ { id: 'bar:i', type: 'bar', bar: { nomnom: 33 } }, { id: 'bar:o', type: 'bar', bar: { nomnom: 2 } }, { id: 'baz:u', type: 'baz', baz: { title: 'Terrific!' } }, @@ -124,7 +149,7 @@ export default ({ getService }) => { ]); // The docs in the alias have been migrated - assert.deepEqual(await fetchDocs({ callCluster, index }), [ + expect(await fetchDocs(esClient, index)).to.eql([ { id: 'bar:i', type: 'bar', @@ -171,7 +196,7 @@ export default ({ getService }) => { bar: { properties: { mynum: { type: 'integer' } } }, }; - const migrations = { + const migrations: Record = { foo: { '1.0.0': (doc) => set(doc, 'attributes.name', doc.attributes.name.toUpperCase()), }, @@ -182,19 +207,20 @@ export default ({ getService }) => { }, }; - await createIndex({ callCluster, index }); - await createDocs({ callCluster, index, docs: originalDocs }); + await createIndex({ esClient, index }); + await createDocs({ esClient, index, docs: originalDocs }); - await migrateIndex({ callCluster, index, migrations, mappingProperties }); + await migrateIndex({ esClient, index, migrations, mappingProperties }); + // @ts-expect-error name doesn't exist on mynum type mappingProperties.bar.properties.name = { type: 'keyword' }; migrations.foo['2.0.1'] = (doc) => set(doc, 'attributes.name', `${doc.attributes.name}v2`); migrations.bar['2.3.4'] = (doc) => set(doc, 'attributes.name', `NAME ${doc.id}`); - await migrateIndex({ callCluster, index, migrations, mappingProperties }); + await migrateIndex({ esClient, index, migrations, mappingProperties }); // The index for the initial migration has not been destroyed... - assert.deepEqual(await fetchDocs({ callCluster, index: `${index}_2` }), [ + expect(await fetchDocs(esClient, `${index}_2`)).to.eql([ { id: 'bar:i', type: 'bar', @@ -226,7 +252,7 @@ export default ({ getService }) => { ]); // The docs were migrated again... - assert.deepEqual(await fetchDocs({ callCluster, index }), [ + expect(await fetchDocs(esClient, index)).to.eql([ { id: 'bar:i', type: 'bar', @@ -266,48 +292,43 @@ export default ({ getService }) => { foo: { properties: { name: { type: 'text' } } }, }; - const migrations = { + const migrations: Record = { foo: { '1.0.0': (doc) => set(doc, 'attributes.name', 'LOTR'), }, }; - await createIndex({ callCluster, index }); - await createDocs({ callCluster, index, docs: originalDocs }); + await createIndex({ esClient, index }); + await createDocs({ esClient, index, docs: originalDocs }); const result = await Promise.all([ - migrateIndex({ callCluster, index, migrations, mappingProperties }), - migrateIndex({ callCluster, index, migrations, mappingProperties }), + migrateIndex({ esClient, index, migrations, mappingProperties }), + migrateIndex({ esClient, index, migrations, mappingProperties }), ]); // The polling instance and the migrating instance should both - // return a similar migraiton result. - assert.deepEqual( + // return a similar migration result. + expect( result + // @ts-expect-error destIndex exists only on MigrationResult status: 'migrated'; .map(({ status, destIndex }) => ({ status, destIndex })) - .sort((a) => (a.destIndex ? 0 : 1)), - [ - { status: 'migrated', destIndex: '.migration-c_2' }, - { status: 'skipped', destIndex: undefined }, - ] - ); + .sort((a) => (a.destIndex ? 0 : 1)) + ).to.eql([ + { status: 'migrated', destIndex: '.migration-c_2' }, + { status: 'skipped', destIndex: undefined }, + ]); + const { body } = await esClient.cat.indices({ index: '.migration-c*', format: 'json' }); // It only created the original and the dest - assert.deepEqual( - _.map( - await callCluster('cat.indices', { index: '.migration-c*', format: 'json' }), - 'index' - ).sort(), - ['.migration-c_1', '.migration-c_2'] - ); + expect(_.map(body, 'index').sort()).to.eql(['.migration-c_1', '.migration-c_2']); // The docs in the original index are unchanged - assert.deepEqual(await fetchDocs({ callCluster, index: `${index}_1` }), [ + expect(await fetchDocs(esClient, `${index}_1`)).to.eql([ { id: 'foo:lotr', type: 'foo', foo: { name: 'Lord of the Rings' } }, ]); // The docs in the alias have been migrated - assert.deepEqual(await fetchDocs({ callCluster, index }), [ + expect(await fetchDocs(esClient, index)).to.eql([ { id: 'foo:lotr', type: 'foo', @@ -320,38 +341,53 @@ export default ({ getService }) => { }); }; -async function createIndex({ callCluster, index }) { - await callCluster('indices.delete', { index: `${index}*`, ignore: [404] }); +async function createIndex({ esClient, index }: { esClient: ElasticsearchClient; index: string }) { + await esClient.indices.delete({ index: `${index}*` }, { ignore: [404] }); const properties = { type: { type: 'keyword' }, foo: { properties: { name: { type: 'keyword' } } }, bar: { properties: { nomnom: { type: 'integer' } } }, baz: { properties: { title: { type: 'keyword' } } }, }; - await callCluster('indices.create', { + await esClient.indices.create({ index, body: { mappings: { dynamic: 'strict', properties } }, }); } -async function createDocs({ callCluster, index, docs }) { - await callCluster('bulk', { +async function createDocs({ + esClient, + index, + docs, +}: { + esClient: ElasticsearchClient; + index: string; + docs: any[]; +}) { + await esClient.bulk({ body: docs.reduce((acc, doc) => { acc.push({ index: { _id: doc.id, _index: index } }); acc.push(_.omit(doc, 'id')); return acc; }, []), }); - await callCluster('indices.refresh', { index }); + await esClient.indices.refresh({ index }); } async function migrateIndex({ - callCluster, + esClient, index, migrations, mappingProperties, validateDoc, obsoleteIndexTemplatePattern, +}: { + esClient: ElasticsearchClient; + index: string; + migrations: Record; + mappingProperties: SavedObjectsTypeMappingDefinitions; + validateDoc?: (doc: any) => void; + obsoleteIndexTemplatePattern?: string; }) { const typeRegistry = new SavedObjectTypeRegistry(); const types = migrationsToTypes(migrations); @@ -361,17 +397,17 @@ async function migrateIndex({ kibanaVersion: '99.9.9', typeRegistry, validateDoc: validateDoc || _.noop, - log: { info: _.noop, debug: _.noop, warn: _.noop }, + log: getLogMock(), }); const migrator = new IndexMigrator({ - callCluster, + client: createMigrationEsClient(esClient, getLogMock()), documentMigrator, index, obsoleteIndexTemplatePattern, mappingProperties, batchSize: 10, - log: { info: _.noop, debug: _.noop, warn: _.noop }, + log: getLogMock(), pollInterval: 50, scrollDuration: '5m', serializer: new SavedObjectsSerializer(typeRegistry), @@ -380,21 +416,22 @@ async function migrateIndex({ return await migrator.migrate(); } -function migrationsToTypes(migrations) { - return Object.entries(migrations).map(([type, migrations]) => ({ +function migrationsToTypes( + migrations: Record +): SavedObjectsType[] { + return Object.entries(migrations).map(([type, migrationsMap]) => ({ name: type, hidden: false, namespaceType: 'single', mappings: { properties: {} }, - migrations: { ...migrations }, + migrations: { ...migrationsMap }, })); } -async function fetchDocs({ callCluster, index }) { - const { - hits: { hits }, - } = await callCluster('search', { index }); - return hits +async function fetchDocs(esClient: ElasticsearchClient, index: string) { + const { body } = await esClient.search>({ index }); + + return body.hits.hits .map((h) => ({ ...h._source, id: h._id, diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts index 2836cf100a432..6f4f92c6833f7 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts @@ -5,8 +5,12 @@ */ import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; -import { CoreSetup, Logger } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { + CoreSetup, + Logger, + SavedObjectsErrorHelpers, +} from '../../../../../../src/core/server'; import { APMConfig } from '../..'; import { TaskManagerSetupContract, @@ -110,7 +114,7 @@ export async function createApmTelemetry({ return data; } catch (err) { - if (err.output?.statusCode === 404) { + if (SavedObjectsErrorHelpers.isNotFoundError(err)) { // task has not run yet, so no saved object to return return {}; } diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts index e485fad09ba99..6cfe3d5b76266 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts @@ -48,7 +48,7 @@ export const getAgentHandler: RequestHandler { }); mockAgentService.getAgentStatusById = jest.fn().mockImplementation(() => { - throw Boom.notFound('Agent not found'); + SavedObjectsErrorHelpers.createGenericNotFoundError(); }); mockAgentService.getAgent = jest.fn().mockImplementation(() => { - throw Boom.notFound('Agent not found'); + SavedObjectsErrorHelpers.createGenericNotFoundError(); }); mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index 13ca51e1f2b39..b52c51ba789af 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -112,7 +112,7 @@ export class ManifestManager { // Cache the compressed body of the artifact this.cache.set(artifactId, Buffer.from(artifact.body, 'base64')); } catch (err) { - if (err.status === 409) { + if (this.savedObjectsClient.errors.isConflictError(err)) { this.logger.debug(`Tried to create artifact ${artifactId}, but it already exists.`); } else { return err; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts index 713d0cb85e2e8..525c3781be749 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts @@ -3,8 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import Boom from 'boom'; +import { SavedObjectsErrorHelpers } from '../../../../../../src/core/server'; import moment from 'moment'; import { @@ -27,7 +26,7 @@ describe('ReindexActions', () => { beforeEach(() => { client = { - errors: null, + errors: SavedObjectsErrorHelpers, create: jest.fn(unimplemented('create')), bulkCreate: jest.fn(unimplemented('bulkCreate')), delete: jest.fn(unimplemented('delete')), @@ -306,7 +305,7 @@ describe('ReindexActions', () => { describe(`IndexConsumerType.${typeKey}`, () => { it('creates the lock doc if it does not exist and executes callback', async () => { expect.assertions(3); - client.get.mockRejectedValueOnce(Boom.notFound()); // mock no ML doc exists yet + client.get.mockRejectedValueOnce(SavedObjectsErrorHelpers.createGenericNotFoundError()); // mock no ML doc exists yet client.create.mockImplementationOnce((type: any, attributes: any, { id }: any) => Promise.resolve({ type, diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts index 54f9fe9d298f2..6d8afee1ff950 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts @@ -253,7 +253,7 @@ export const reindexActionsFactory = ( // The IndexGroup enum value (a string) serves as the ID of the lock doc return await client.get(REINDEX_OP_TYPE, indexGroup); } catch (e) { - if (e.isBoom && e.output.statusCode === 404) { + if (client.errors.isNotFoundError(e)) { return await client.create( REINDEX_OP_TYPE, { From ff9f06b880dab63e1e577433b012f4a2d6c792dc Mon Sep 17 00:00:00 2001 From: John Dorlus Date: Sun, 26 Jul 2020 10:04:09 -0400 Subject: [PATCH 25/96] The directory in the command was missing the /generated directory and would cause all definitions to be regenerated in the wrong place. (#72766) --- packages/kbn-spec-to-console/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kbn-spec-to-console/README.md b/packages/kbn-spec-to-console/README.md index 526ceef43e3cd..0328dec791320 100644 --- a/packages/kbn-spec-to-console/README.md +++ b/packages/kbn-spec-to-console/README.md @@ -23,10 +23,10 @@ At the root of the Kibana repository, run the following commands: ```sh # OSS -yarn spec_to_console -g "/rest-api-spec/src/main/resources/rest-api-spec/api/*" -d "src/plugins/console/server/lib/spec_definitions/json" +yarn spec_to_console -g "/rest-api-spec/src/main/resources/rest-api-spec/api/*" -d "src/plugins/console/server/lib/spec_definitions/json/generated" # X-pack -yarn spec_to_console -g "/x-pack/plugin/src/test/resources/rest-api-spec/api/*" -d "x-pack/plugins/console_extensions/server/lib/spec_definitions/json" +yarn spec_to_console -g "/x-pack/plugin/src/test/resources/rest-api-spec/api/*" -d "x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated" ``` ### Information used in Console that is not available in the REST spec From 55f55bfb547e3ce89174f3806605ed56f7fe0a4a Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Sun, 26 Jul 2020 12:41:22 -0500 Subject: [PATCH 26/96] [APM] Read body from indicesStats in upload-telemetry-data (#72732) add for transport request too --- .../apm/scripts/upload-telemetry-data/index.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts b/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts index a44fad82f20e6..10651d97f3c3d 100644 --- a/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts +++ b/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts @@ -87,13 +87,15 @@ async function uploadData() { return client.search(body as any).then((res) => res.body); }, indicesStats: (body) => { - return client.indices.stats(body as any); + return client.indices.stats(body as any).then((res) => res.body); }, transportRequest: ((params) => { - return client.transport.request({ - method: params.method, - path: params.path, - }); + return client.transport + .request({ + method: params.method, + path: params.path, + }) + .then((res) => res.body); }) as CollectTelemetryParams['transportRequest'], }, }); From 9656cbcbe776fc41e9c98e53bb714af9e448b714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Mon, 27 Jul 2020 00:45:52 +0200 Subject: [PATCH 27/96] Add default Elasticsearch credentials to docs (#72617) --- docs/developer/advanced/running-elasticsearch.asciidoc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/developer/advanced/running-elasticsearch.asciidoc b/docs/developer/advanced/running-elasticsearch.asciidoc index 2361f805c7635..e5c86fafd1ce7 100644 --- a/docs/developer/advanced/running-elasticsearch.asciidoc +++ b/docs/developer/advanced/running-elasticsearch.asciidoc @@ -13,6 +13,10 @@ This will run a snapshot of {es} that is usually built nightly. Read more about ---- yarn es snapshot ---- +By default, two users are added to Elasticsearch: + + - A superuser with username: `elastic` and password: `changeme`, which can be used to log into Kibana with. + - A user with username: `kibana_system` and password `changeme`. This account is used by the Kibana server to authenticate itself to Elasticsearch, and to perform certain actions on behalf of the end user. These credentials should be specified in your kibana.yml as described in <> See all available options, like how to specify a specific license, with the `--help` flag. @@ -115,4 +119,4 @@ PUT _cluster/settings } ---- -Follow the cross-cluster search instructions for setting up index patterns to search across clusters (<>). \ No newline at end of file +Follow the cross-cluster search instructions for setting up index patterns to search across clusters (<>). From 122d7fe18fa6d82a76ccbc5cee4055ec7be9f428 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 27 Jul 2020 10:44:19 +0200 Subject: [PATCH 28/96] [Graph] Unskip graph tests (#72291) --- x-pack/test/functional/apps/graph/graph.ts | 28 ++++++++++--------- .../functional/page_objects/graph_page.ts | 12 +------- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/x-pack/test/functional/apps/graph/graph.ts b/x-pack/test/functional/apps/graph/graph.ts index 803e5e8f80d70..c2500dca78444 100644 --- a/x-pack/test/functional/apps/graph/graph.ts +++ b/x-pack/test/functional/apps/graph/graph.ts @@ -13,8 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); - // FLAKY: https://github.com/elastic/kibana/issues/53749 - describe.skip('graph', function () { + describe('graph', function () { before(async () => { await browser.setWindowSize(1600, 1000); log.debug('load graph/secrepo data'); @@ -132,14 +131,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await buildGraph(); const { edges } = await PageObjects.graph.getGraphObjects(); - const blogAdminBlogEdge = edges.find( + await PageObjects.graph.isolateEdge('test', '/test/wp-admin/'); + + await PageObjects.graph.stopLayout(); + await PageObjects.common.sleep(1000); + const testTestWpAdminBlogEdge = edges.find( ({ sourceNode, targetNode }) => - sourceNode.label === '/blog/wp-admin/' && targetNode.label === 'blog' + targetNode.label === '/test/wp-admin/' && sourceNode.label === 'test' )!; - - await PageObjects.graph.isolateEdge(blogAdminBlogEdge); - - await PageObjects.graph.clickEdge(blogAdminBlogEdge); + await testTestWpAdminBlogEdge.element.click(); + await PageObjects.common.sleep(1000); + await PageObjects.graph.startLayout(); const vennTerm1 = await PageObjects.graph.getVennTerm1(); log.debug('vennTerm1 = ' + vennTerm1); @@ -156,11 +158,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const smallVennTerm2 = await PageObjects.graph.getSmallVennTerm2(); log.debug('smallVennTerm2 = ' + smallVennTerm2); - expect(vennTerm1).to.be('/blog/wp-admin/'); - expect(vennTerm2).to.be('blog'); - expect(smallVennTerm1).to.be('5'); - expect(smallVennTerm12).to.be(' (5) '); - expect(smallVennTerm2).to.be('8'); + expect(vennTerm1).to.be('/test/wp-admin/'); + expect(vennTerm2).to.be('test'); + expect(smallVennTerm1).to.be('4'); + expect(smallVennTerm12).to.be(' (4) '); + expect(smallVennTerm2).to.be('4'); }); it('should delete graph', async function () { diff --git a/x-pack/test/functional/page_objects/graph_page.ts b/x-pack/test/functional/page_objects/graph_page.ts index 0d3e2c10579f5..fe049327fe38b 100644 --- a/x-pack/test/functional/page_objects/graph_page.ts +++ b/x-pack/test/functional/page_objects/graph_page.ts @@ -83,10 +83,7 @@ export function GraphPageProvider({ getService, getPageObjects }: FtrProviderCon return [this.getPositionAsString(x1, y1), this.getPositionAsString(x2, y2)]; } - async isolateEdge(edge: Edge) { - const from = edge.sourceNode.label; - const to = edge.targetNode.label; - + async isolateEdge(from: string, to: string) { // select all nodes await testSubjects.click('graphSelectAll'); @@ -109,13 +106,6 @@ export function GraphPageProvider({ getService, getPageObjects }: FtrProviderCon await testSubjects.click('graphRemoveSelection'); } - async clickEdge(edge: Edge) { - await this.stopLayout(); - await PageObjects.common.sleep(1000); - await edge.element.click(); - await this.startLayout(); - } - async stopLayout() { if (await testSubjects.exists('graphPauseLayout')) { await testSubjects.click('graphPauseLayout'); From 6a53b0021e63383a6202a8f5814701e9a719b82a Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 27 Jul 2020 10:45:23 +0200 Subject: [PATCH 29/96] Convert functional vega tests to ts and unskip tests (#72238) * convert to ts and unskip tests * relax tests and remove unused imports * remove test exclusion * remove inspector test Co-authored-by: Elastic Machine --- .../{_vega_chart.js => _vega_chart.ts} | 35 +++++++--- .../page_objects/vega_chart_page.ts | 61 +++++++----------- .../screenshots/baseline/vega_chart.png | Bin 59257 -> 0 bytes .../baseline/vega_chart_filtered.png | Bin 59342 -> 0 bytes 4 files changed, 52 insertions(+), 44 deletions(-) rename test/functional/apps/visualize/{_vega_chart.js => _vega_chart.ts} (59%) delete mode 100644 test/functional/screenshots/baseline/vega_chart.png delete mode 100644 test/functional/screenshots/baseline/vega_chart_filtered.png diff --git a/test/functional/apps/visualize/_vega_chart.js b/test/functional/apps/visualize/_vega_chart.ts similarity index 59% rename from test/functional/apps/visualize/_vega_chart.js rename to test/functional/apps/visualize/_vega_chart.ts index c530c6f823133..6c0b77411ae99 100644 --- a/test/functional/apps/visualize/_vega_chart.js +++ b/test/functional/apps/visualize/_vega_chart.ts @@ -18,9 +18,17 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { - const PageObjects = getPageObjects(['timePicker', 'visualize', 'visChart', 'vegaChart']); +// eslint-disable-next-line import/no-default-export +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const PageObjects = getPageObjects([ + 'timePicker', + 'visualize', + 'visChart', + 'visEditor', + 'vegaChart', + ]); const filterBar = getService('filterBar'); const log = getService('log'); @@ -30,13 +38,15 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualize.navigateToNewVisualization(); log.debug('clickVega'); await PageObjects.visualize.clickVega(); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); }); describe('vega chart', () => { describe('initial render', () => { - it.skip('should have some initial vega spec text', async function () { + it('should have some initial vega spec text', async function () { const vegaSpec = await PageObjects.vegaChart.getSpec(); - expect(vegaSpec).to.contain('{').and.to.contain('data'); + expect(vegaSpec).to.contain('{'); + expect(vegaSpec).to.contain('data'); expect(vegaSpec.length).to.be.above(500); }); @@ -44,7 +54,8 @@ export default function ({ getService, getPageObjects }) { const view = await PageObjects.vegaChart.getViewContainer(); expect(view).to.be.ok(); const size = await view.getSize(); - expect(size).to.have.property('width').and.to.have.property('height'); + expect(size).to.have.property('width'); + expect(size).to.have.property('height'); expect(size.width).to.be.above(0); expect(size.height).to.be.above(0); @@ -63,10 +74,18 @@ export default function ({ getService, getPageObjects }) { await filterBar.removeAllFilters(); }); - it.skip('should render different data in response to filter change', async function () { - await PageObjects.vegaChart.expectVisToMatchScreenshot('vega_chart'); + it('should render different data in response to filter change', async function () { + await PageObjects.vegaChart.typeInSpec('"config": { "kibana": {"renderer": "svg"} },'); + await PageObjects.visEditor.clickGo(); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + const fullDataLabels = await PageObjects.vegaChart.getYAxisLabels(); + expect(fullDataLabels[0]).to.eql('0'); + expect(fullDataLabels[fullDataLabels.length - 1]).to.eql('1,600'); await filterBar.addFilter('@tags.raw', 'is', 'error'); - await PageObjects.vegaChart.expectVisToMatchScreenshot('vega_chart_filtered'); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + const filteredDataLabels = await PageObjects.vegaChart.getYAxisLabels(); + expect(filteredDataLabels[0]).to.eql('0'); + expect(filteredDataLabels[filteredDataLabels.length - 1]).to.eql('90'); }); }); }); diff --git a/test/functional/page_objects/vega_chart_page.ts b/test/functional/page_objects/vega_chart_page.ts index 488f4cfd0d0ce..b9906911b00f1 100644 --- a/test/functional/page_objects/vega_chart_page.ts +++ b/test/functional/page_objects/vega_chart_page.ts @@ -17,20 +17,17 @@ * under the License. */ -import expect from '@kbn/expect'; +import { Key } from 'selenium-webdriver'; import { FtrProviderContext } from '../ftr_provider_context'; export function VegaChartPageProvider({ getService, getPageObjects, - updateBaselines, }: FtrProviderContext & { updateBaselines: boolean }) { const find = getService('find'); const testSubjects = getService('testSubjects'); const browser = getService('browser'); - const screenshot = getService('screenshots'); - const log = getService('log'); - const { visEditor, visChart } = getPageObjects(['visEditor', 'visChart']); + const { common } = getPageObjects(['common']); class VegaChartPage { public async getSpec() { @@ -45,6 +42,19 @@ export function VegaChartPageProvider({ return linesText.join('\n'); } + public async typeInSpec(text: string) { + const editor = await testSubjects.find('vega-editor'); + const textarea = await editor.findByClassName('ace_content'); + await textarea.click(); + let repeats = 20; + while (--repeats > 0) { + await browser.pressKeys(Key.ARROW_UP); + await common.sleep(50); + } + await browser.pressKeys(Key.ARROW_RIGHT); + await browser.pressKeys(text); + } + public async getViewContainer() { return await find.byCssSelector('div.vgaVis__view'); } @@ -53,37 +63,16 @@ export function VegaChartPageProvider({ return await find.byCssSelector('div.vgaVis__controls'); } - /** - * Removes chrome and takes a small screenshot of a vis to compare against a baseline. - * @param {string} name The name of the baseline image. - * @param {object} opts Options object. - * @param {number} opts.threshold Threshold for allowed variance when comparing images. - */ - public async expectVisToMatchScreenshot(name: string, opts = { threshold: 0.05 }) { - log.debug(`expectVisToMatchScreenshot(${name})`); - - // Collapse sidebar and inject some CSS to hide the nav so we have a focused screenshot - await visEditor.clickEditorSidebarCollapse(); - await visChart.waitForVisualizationRenderingStabilized(); - await browser.execute(` - var el = document.createElement('style'); - el.id = '__data-test-style'; - el.innerHTML = '[data-test-subj="headerGlobalNav"] { display: none; } '; - el.innerHTML += '[data-test-subj="top-nav"] { display: none; } '; - el.innerHTML += '[data-test-subj="experimentalVisInfo"] { display: none; } '; - document.body.appendChild(el); - `); - - const percentDifference = await screenshot.compareAgainstBaseline(name, updateBaselines); - - // Reset the chart to its original state - await browser.execute(` - var el = document.getElementById('__data-test-style'); - document.body.removeChild(el); - `); - await visEditor.clickEditorSidebarCollapse(); - await visChart.waitForVisualizationRenderingStabilized(); - expect(percentDifference).to.be.lessThan(opts.threshold); + public async getYAxisLabels() { + const chart = await testSubjects.find('visualizationLoader'); + const yAxis = await chart.findByCssSelector('[aria-label^="Y-axis"]'); + const tickGroup = await yAxis.findByClassName('role-axis-label'); + const labels = await tickGroup.findAllByCssSelector('text'); + const labelTexts: string[] = []; + for (const label of labels) { + labelTexts.push(await label.getVisibleText()); + } + return labelTexts; } } diff --git a/test/functional/screenshots/baseline/vega_chart.png b/test/functional/screenshots/baseline/vega_chart.png deleted file mode 100644 index 5288bd9c7b924b4cd94885f45aa28b3543aa3257..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59257 zcmeFZ2T)X9*DiRPAc`ad5+tc00t$#^Bp8v5UVDXSt@Z5lN>g2tmYS6s zf*@L@d$+Y9h!T7yOFw!9{D?%~;D#VBNa^+s9q;&gypNyGBz}K?qg8eNx!TcFC&`{C zoPMZnOx>)-7Z<%=8f=t{)HfRHIdi$R%+VyPETB*#GjDM4(@T}WkwIgER$1vw>X&*q z82D905GT19D2m!uqd%_fT#$-*PCFA_ZjXnEs9~E;|4~4b&L@auoN=10ucr$7fndzV zBnd;a&4$y~ND*V%{CO9ab6kfHXY0I=Fs~=#5@mfG_C2fc%4%v+7R;<(ixmsOD8(?g zSMCysgDX)lpXfl|V`E?S82w91O15#n`&$0~c%@tQM> zhsem`#f1xsZ)rjTbaizHOC23n+|Uu{dnEoDiY|;A+F6~&PL;mDaM%8xQ+`_W647iQ zso~+_aV61B{o6Jb1X+gL#V+i9{dJszZ2M1y#{A;lvsVI3Gq$C6{N!|XbTU(dKEhr_ zT#_GJ^}epG?vzias;(|)y~)JHlo1~v|8nCL1bquO+uz2yRy~T z(%V?%X6Z4Vl`Wg0k(57DtkK;m32-SoZK5Td5F)>u;_B+! zTV$g#IXz8SC>xFDb{wfT+}hgOo)ijX3CI&DsMdmi1G-Uc)5)QhBxh2$*(9_Yk+gfe zF_;!>9b!)wykI$beg`Y&mHi8I{f6$#60=kaeBw?D(6u- zlYp(s@xc9+r2XFYg`udVqym}6@>_41g!(C&9&jlQR=8yd)~sr+&HvKo2v=O0?!@hp zc6gLDuK#_G0dlV*)*8bKC-!(i81K?Zi`*w|c>HZ*65!=kREZIETnm&}adgacsv~)Z za>|=ZNl8`iF1M}yyw(~2E96Gc*Jo5EZqw>pq&@9gvuN70;`h3fSKnby)t>y6k;DR>mt|+9@+%aiQ9Wdkd5L zKLbew@}k|DLT$OSlyeW|)||2o^*Kd>qHK8VER@Bee?FOL{)jX2GRct_6BC>ACDzy| zi%`+A*#_S{x6+kz3&VX)Ru+FfV7FY#WlTXE?bCgS>0?Dj1%`VC-eJ2~jFj`pb#1gA zbFkEt-?z2Ve671}W_y-$n`6%&-Mt&k*IK>Wqtf0UCs8(s11kR$xXlmfpLSTx$JyCg zj0N*lsKm&@!y1kYiSm48VPTnK0&3tQV_fDWhRZi`P!^ePk7Eju&2a|P5|(ftVOvx| zB9WW5V6@ou^z^CW3b)1re?neaEEPS6C~c##o10sE5^3J%k*8-VnAg_c4zX-*C5W8r zXPaQv<=tV<{mquCVA`_{{t*u!+1SKQPEL*&hMtuW91hsuBeloIvaZza@6jY$keH*FM=ifNG`i=x>GR^niz!7;|A)#Vz+jzXF?MH8p9U){8$sr@ z6AUK2CqgCEw*o$5>=uX03i>vCu-L}%(Q2Qft$K3$55X26!1AZ&e-$akfya|W(A9@SrH;s%ln^6QR$gA8&vZh@i3@i# z)6#Aal{w>b3TpY4w#VrMGwb$AM1DTL?qWOa>QW;;AuA&h-78sAmYvPNJLomylQ9yw z?=QQ%)QC=-P6{-Y-~3A7(C1Qy_l&!!`k^aZUPGQ#%r79YmgnrR;#|9h8S&pRF>!6> z$U?gD25p0iqgk5aq{@#v;?@zGn{2;h?B*;X)f1hGf<=HyD zGFv+26v6HP_l;SvV;LR;_9r-_D2z=?YhyI9E)hA$SAoydk7W@ySMVMrez-Gf-a5*g;zN%4CQLuxO+g-Y~Fj%4jymaq)W60XDTjz_UqKB;~7ltde<@dKb zdBpPD%4|9lUG_Z}2KyF2ELIW(Ww-uBRDv&&2?L|ZWb`1v!kS5mEnr%`xtwS};zjP8)%_hSi8=}uK7Y)vQO{5S9!4GlMy zLJ7S*yu59~G{ASO*vA|BR>IQ_AZ_!>$(cSoLW@rG%;@Wg6@w)ji6itK_%{;43`Y#u zkm2FsDe-ySR#?2d1GGs;Q!{yU(|h4pks^8(cz(lRi|-4bgx_z3qI)GL`$TNJxWEJu z@SYg7^7E1o&9dX(IYinZqPnA=7H-gj9T19tY}nXVm_gu&J?Y_wFR)i zcnfKFS{|2cShNbtTLXDy+d%b*o=^z9Xd8iF05!H;(c$q_}`DL@&VP{v%qtr(|UTe@PYfJ zA3uKdR7@udC~bqd+7J%HSrxb4SbdPPx_UG>07CcBy8(fcnvZ9{QP)QNstc@xm2Zm_ zzd;) zb}84}Le)!8!N2z8sZ(NJix1ip zWsP?>h`70k{39<#}I09;(2{ZFqQy2 zRoVgW1zstF`^tlIO|<*?3npcBf4)f$Z<3#ufL?AF(3;|ht!ON@d9K22VGGA`XOsni zYGK@VTg#J`D;+W(eY*Pa_w4NK9ylNl4?l>Cj<#PMt}v=}&k6uOAEP4bFGB;XRhfz=9S6a}666qXL--U1n7>tq@ zy*wtP^yf-&rV+gcv|g2i$DzF1D!dbERwIx)BrrvDCU+Yt4M3KFl2M`Mi(2462_S|& z7Ne|APFB#G=v;;kYBI(w|F}@K=KkJB-P&wM(%cV$f=F&W5G^_ljza*DO!LW`VeIUi z-?D74m(_tp*{B^Dme#~0A0R#7^AJ!+$zuYyrCtBL1gbDP*V)1u$QCdhxHlabyzfLQ z{fvsf|BX;>CfG>4-%2~|QpJ#~wt0p{7~Q_4zUEg!QH;;=AhK@*ziy28>X4eYZFM{5A8fgy=1ws(KGbKiD-{ujW^Ww3)% z3LXE{GI8J2)3eIoR>RaZu{rcCFL1ZkZPBw1W;?O@)yv8?V2>*2vU65JsMND+etWE} zo5NvuL02C3-<{W2-hyaYV&BIPyI5rn4czGdo|pBbN2Rk9uL>;iqazNZU?K1yKHYh< zS~W^iFS6;3=7vY*W#7pfiNN~Q0jtJTEmo?aSG#V}&n}HON&_$4urUe(+;)FiZ8~rw zgl$3rKoC{#02|Eps0j6G1a50bMMl~JJ*iwM9a>H0B;CW<&4G-fzdnduV|xn*SOtM3 z2X;9>1NrUl@Kq8q&}IRqEPm763fvk?-m|jA{Kh{_gCKXC z3?t5bYv`fu;y2CA+FDZpv4M%hBR784@eP;%E1uMu0l>xDT%XF{zkeME3-a@$aRl8* zk1*Q+tIm}GXsE2BqHAcFm2xX2IAhOfVUrX9XG($ldyY51pW1oyyY5!XyD(09!Lp$| zkTd=eRb!VAcmN_0FzY^e39O?2{SHZ+wG|HpX<0ByGKz4pT)`CgTbt z$|RIp;qdowB0X0Qztw;J*If|R1z5@958GP^_zd{tB21de$fB%rPFh(a)+5WS7rIaA|z)l%nYOdweJ_gzfZc_P5y|=TcoET zGb(pU0ip9WNH33_x;O*y1$M44#{=L7J^=w0kZB=*zv6w!9DL@o0T&Q3km)S!_r-7z ztgzn?=hD>&=7Jg};$p=dimltusKttAwUabt*CLUtGbz9@9{_y<8kA%2;NY=Pf}0yC zw1T1786neL-5kAEVH&l>>xN##PeMRch>VI#fBKXxJuPiOSmbbgcQFjqf+{W+%`+&x z39=LCjR0U;HNG4C*RE*;+*ON?i11h%y8$-$b`5e3gmhLpzjEs1$8`-1G7GI*ehAa} zT;~ADYN5lWAg1x^Q-cpGC%;w*TIe@>sL+gB2re?>!KFbEBDBu)m8Z}*A zzrb-3L~5|BJlxy^V*eC#-~r#-86wKfzmkCpLWnaFz=F7z3=D=}Osu5c!GN%LXp!~5 z40f*s0~9xe|EFknTZ&BKa8~~%3p^a|f5TM&Uv{Bzu&Ai$_vw{{Znu9t#;~cjcBL$=n^4{L^Am@= zR^JL@RAS$X0l9Fn<6yDUL~|$>WMFyA%dYTK;R01uTwIPtW3b(D`IEu&C#lV0>^T`p z0Y+t`0aau{C^$#EV)*6uf3tn<4E4U=EN$bGLcgu$qCFQ6klB>vIaUo3^MPBEvmyrk z+dCb&rOtsWvwwM~|8Pr5gq~I2uIt*-ntk6u}oW=qyOOzw56Qu(~ry)A~tCX9!6TfkjR*o|f_CphslkMihWQRP) zn{m3oH|}J|L6*JSR+Fsz{cT3oB!BjT0&LK>QB@Uyg63z*>X~~$pCyUd%UjbD#;)z? zxDx0Bz%LB>uIFPgcT+Sou{%ehjGTdN=&igDbV*E)d=JQEP}C#Z6|G(Jck_Oz?+Ooz z_|^Z!3=hh{tz0}gr}S~}i?e1pMct$y*(`!Q5Jly%ghT-j6H8rHg@A0SK$JduPnv!+K0&D5Ik8D7)0VXXjB)o4(yY{>izXh#Aa$Ak}CMth;!`a_$1-YP&KupO3xhfwY-+Q)`D5w0)tZio8o;?AkvCuoJ{R6{0V}Gjr*|eR1wy4w zt?j#N_jGJl1g8<{S@RdRVr*~QSHAY^i`@jeHA^aw@IA&g$WF7lme)=YBP9gRjP^19 zP*U$xnGRG6loc|&rKM$kPZ}7z;HFS8Bf8chA=u48*X+x2r)^9GYN?Ab(EOE88F4CnY5Y?)k{B4_JXz zAsSAcL9KdrxlIVpNV%25<@d@*{8rS!8nVF_jBYkda4V5^HgKQ=kptQY4y5h1!Os@A z;yi`wc9-)x*-%c_YlXKeQy&-P&rftOB%ZS&Nh7YMnZ+#hX}8d~y%W7s5~KHsUf-FY z7E(MOi_znhNgIuh$-a162Yn)gbG5z4YllwrQ}_D5liTZ3d;L5Op{X$JhcunDNE*q} zH;jrUf9g5f7oLO##L37V_7aRL4u zL}1BVt|&$Uy%~^$SB^bBzB=Mdv?GqxAVIUN#COB#BZjY0*d162K-_R~35WsBiaWEq zLju|Mdv3J^X!tLn_ozqD@Xl7=Xw;STPmHEv;knh5w9aJz z4LEk-nAkE9Tu<)>SiZFo-QV#15y>|QJR)fA!F8fg=Bqblk*i(FH=e=0*MBl1R!NuS zBlH}(0L;L7QFcK=Ov^-f!;9lXs=P-~_bxdZk-OYm{#bzU7Wt28Cc;Ci`xHnxPB5E% z&iV3*N6NcJ{^67p&eh2_bPM`l+VW)XCgiX)nPeN1L^N1-4TDAVBIah|01 zFlXamI1q26d?Jb`KB=rt)^flhMZ?Pv4tH}&iiY8A(D@*~`a=X|{D8+8)B`~O+gNt0 ziOGy6`i zNo>SoTJ9Rrc>gsC5h>%iNRu(I0h2#v;HfpM?(~WaN#%x)% zml&wC_5GeAD<@<~YqUz(&e?+SWufkvj@g22^JldcJ~jDUOYB=uX%j{`P#yZeH)iEW z0=0`qO>%CUmE28R>ynq-x0bzTH^>qah!&N6!B?-KPaTU{m(Yc#{F(;?g429RMQcPM zlLJ%8Zp)jdkKuR5zuBJbpx13IWQ8gzKuB%t>@2G~4!JbvQpca;GpRb2!FyF{f3u;f z(xI24g65Wka*gyxd!3zw^|US<+026y&?2pwz7T4(@ib9DK=*Z9Ue#T-F2${wnKpmf z3^9C}>;)|H9-2C?{}Opuh4QW(rJA;(5}#|rl!VyVmsp|T$t;)c>;5vnFYWVY_Z-Z+ zCeoWMJrtms2NWpKNnbX?25L*!BEH+aiU9HGDvYLpOSqp>U^HDv%GUFiUtBIl#DB#7 zptoDCWO66uJ1$c`<7XDWS5>>Jii#m)rGlAxyx0C>tnd485imcyv`=O53-DE7 z2V3n2KbQ-h4T2H`t1EDZ`QtX3e+_E38l;e$!<_;bt}4OGxaC~;$)U`t=R_C|>asWx zz#y*ph<+N=_Tpq1t|($vlOb03W;6u&q?uwhq7E?QVfv+|8kgZ-h2*>-k*Cwuz4G)3jtqQ66# ziqUia&~r)5PROVkv(XcA3M0Or5oUKsA?@^5w%9aY!u{8d za~Ag)cBVMdNWwZe@x#&`>x-Tv?xX7ljHo6GltM-kKP0pAJMq9MO{Mo$kmJ>FL?wCD zOwCGaw+lQf|I|nnhp72OV{c*ff8O28W$q5)G_f z_Z0QqrK+c7CUXLD{c{_N^qJzPd!h_)vEnvZMU)qh?r|_Pce0`s^rd>;uU^SGPNtyp z^h8DyEqflkg|*jQGUtB23(#IIyj%VEDaVLeuk`{MO8a1u7@wH>?+cJ0|Aijpy_49N zD8bT_xpMB=cD`$?1JC)Exi8+SrZ+qm>HsZf=fCwHLPab*-d3j#*R-w!R-e;Bs@@ZG zEUoRTDgc3Nd$Ckanz*1}+|>D(4N?yVi5A0EJ?KN6C2`g4jmMWS8NtA8`6oR}vYl)- zonwrd?%Gy5`=Z}Ut~pb4&|h8SoDppjf4pdYFLt;nT-@&orvGiyr`lt$kafR52nvnl z>xZ$uHIRq6q7UZY9E)TUV?<@?Kx8I)nX@}l5e!?|b*BtvA{SS#(lXCn-q}s4LwuhV z;Wmkqy*(lnsOGj~(;8V^@!R0r4gxAf9n9~A+KqbivtE#sovpol<{AY4`jQp<&O<(bc_Gljn~>*&YsMD|JJ%d+hl`r5+~? z4Z@@Wq*=ibY=>^TqP+xf{{-~!2Xiszfl$B)=jV)MG_TOK3YVY7 z-8=^FO%Sq;<)_{JBl0$LfZ`i9w4seS>P{`7TGl^WG&FWQY!-46p@2Q}%TjRDElESD zQq_B!cqwbwgC+P&gw|@TFrA4+cS=z&9AZkqr8e>R|_0cI)w&vskJj@~0jmW*(cGO>De|7+fwAew+4E zpwyVwsQuZnh_T!M%z_`$_W4_L#0wLkCJ-m(Sw`pLVrHcL3yxYR6ZzHJ0ab@gs~68$6tmLrtWD= zcJa#HkRAT(>Bak4(={7tR1Zo6E~dyR{IoaS7ee-LM#hHE!aCjmpY0j>fL-sn!`{rG z>3BY9)`0d*?;fJQ_4d&Dao#Wdr#_O=xF?=>Ak|%>*`pGpJYx#B%jSxa&?2pkAw6Wv z^nc31L4!rcYq9rFYs4Ddj*BrxX2J9ir5@0{5d)kf#?z;5LFtTNNC*eY=x|jP{ufa6 zL#}o!UXW!2EUDr|Zi5ftxwW>#(6^rJv>zY_N#mxynd_x)s#K`;3b<($_dj-ZK;rcU zE(16Rti){t`bHL9*@Y7|_y1aB2meCiwLnnX)a*?VH=bhnVk`!RFL15<_=ob2c5l{{ zVJ@H9F{FYgq(-p4>i~Vf$OUz=GNP_>UAdwLx5Ib0R)%W!GvrBG`Q^Wv= zhWT>z~#J5+mZXzm-esiuDqJM&j`ZROSu1aipHgnqFF} zXJ=;zGcDJm3jFRkd98%wsL2Eb1RBfE2B&}$c~g_vR6PM#fCId$!RmY*VIl2)yp#;+ z+ZgyW75XdNAC z=FR@xUm)Q{?5#Lq8aS|6=+y$sum{!H)D%+D4bZCsY&bKIy{+kk&M@ffrONqlUjkAF zjEQKwSWsvH1!!H+(gwT&9U~+DtS62)K;b?9DkyGJ`xhFgU6WUll2AY4&a ztR&qU0W1I^I`;gpd|CI!)>pzaRH)W7uXnt>m$doQqpr{^WP>!;634B?6v~FTpM7iV zJ-QMWmH9bWN35nNDo%|1_rg%he>TC`k8yk?V`~(59OMSQWU&8B9dPcd1!X_1)^Bw|7ppyVt6eecX2a5WBS zN*u_UbD)PcBT=^%3P{oa>#{da6LLS*U4249jmiTxi@F7pzUJE1BC$Di@8s=;=lElCQfG@$v<= z0ti(VNmnt}lw*LK6`7dGQR`oiLX0nUqGMyT0qLn2nFt!Lt*z&F$r(;`2cXSaqGJVP zES_hGAtZgre~k9dkVcIAUip&5#5X8j%611l=InR>YhfeCvo)p{+yt-FC@hm)siJoX9xDc{|J9QxAOhjKD?zc9 z9J(I_B4k&3uE*0)Qb#`8rf7(T@%4!iN8LWW&qnGYhg%#!|1 zdjpcCHazKOe@@S8y}jB@{IS?7Ufz#o56CR%NG;T;^AZJLa+%n)5@kP9kQ0d)=M z_e5wre4+8z@NuZ&085-_pq`7UGy~Jg zWj-aV@X2G&MELm0nFDdWOuy4$%=%%t+`-((Bo8DupohSqZE3e%zzrwYGIBS9G&L1} z@todNEiA2IYC}Q_*ZzH@hUlj(Y8QPmT~z(Y&-{UIR%z1rw7j#IP5TiPa@v>k_IZ^) zzG_u|`$ik!+OXauaIa?opgyRJ=QT->NAqz!ck%5e$i)*#$7_g%K`jGA6%N2iTTTI z@eq4{r|{uzZz~8Ec!TjNV+TkXc9pj;n+`kV&rN@zOZGib^vXkAw)RRxQ!Bm=X!lk4 zj+X6?Z$Ic#wA!fQ9ZPD;UJgxv;|uU<@(EbbSjLtyY4aH+@urFI<`o~Kc~IG=B#yea2mnM=dvjyErmtMCGJ*Fbc~!f{+yR?B9<_ z11M7DBpTpyV7)`Yw(Ohip#k};O0Z|4eWBCr#ik$uZ}W_M|6Qoi6Wyh?qF%2`endT_ z%noUis|zUuBpn}SP+Kl>u_l9Ca1Jpu0!uI`fC2U&oM>wo|u^9{x##n zHvg5fq0*GLXG!01*(3wkU!AsudfCr*U~Rb59-g~?#M##xXbYD^)|;r^9Rr~ zIeh+{0B!2L2NhrXKuw;A10hQdW~Dln8K1hS+NCCehbj7m+MPvhJ-ujf#Ax-~DUI*K zG*=8hD}Za@;~p?VbI7k~#=J+kd`h;n57eeKzkCD{y>!~#lkAAPiZ^Y2nRpD&B$6Lg>*43nBbR|*hT3N0Q&@tNO<+Fhbi36bXk0L3R1JxaLj_nLhMN(ltYg#{wJIU1+HX3 zyAS}Z=sMDZvzU231em*k(RJxGb*u2(B{zU1ds7wZVWPoTb4|dN>IW2rY(R3(GpjQN z{3KiKBA|$Jy#I@E364@+Cj_NZ6_<}HkkZUb@%Y(y=~UEfxYw?DDf7&!jUS&ugz81k z8+>#U66j~F*&&4e9EjHyw%(wU9@fk#3(@FBZQcek{dQEHAM*CT$LD)ygkKs0UxI&u z{TAu0QX3){+ulDiJ-Zvp0k5oM@K?v!LuwbAp<=rTK;z8dP4vDA2y7p15|8OqU*zR= z_`gD(a2>)>`PP^kXIvcm^)D{hbg_T+?|0mGmU^YGMe;jhsayU@U7D$NdXWU|pKpS* zsoDWkh5g{_DuJN_8nzg2wIv$KS%TuuKu8SZ+)|+{<<9#3MJdvSK~MeH>l+EBR;i>p zlTb#K)wSzMTb140wSkM+XM+Ks?~2I+WQ-Lfz3DHMsQ^77n|XWz__2Yku^~MGU1h8* z|57BMcCl3p9n2>^puf3RG;F}f0{}Y*8!zR?t>iZKv->T}wbMr_+&g7AzEAx9PTU{> zCh(h0W=PW5JN6wp1`5CDkjr2W?8LquaBwz!<+*v(2;osV1Ny1EZu5(@3?{MR3xyRO z8NJnXV~MfKalT~GI@iDOsCegq>0$c;W+~19(vjVo!C?ybKLw)C@6nT^8X0$3ab2!A zA=?Gk^0)}fvB+55-rIIDh{w(aJNU?#^q7Jy{`)atELO;KaU^I415$L=HtKrH<7Y0( zl6v>M)z#E=?d=Hy9>EruczH8awyBbRk3WCPp96!jAL`15RWj`24bq07gd)k4qwHyG zKOao4XC2{jf?T1pS$-m;O2SGr0sgP|?No(}^ZBR+oJxe{SP;cYq1#$g8h#<|! zK-%*1PGgYvaVk_+Vbxv|H;h2rgmr#z@C{hDTPIh!^)%JoSBvv4ET%#&zC3tVZb^}4 z&B@I4DY0ju{4_YRCYHB9jUxK1e<`8k(nX=t2hQo%o&?|#fA$~-8{9yh%BHw{0bH!! z%BPJ2g4c%1W5ncxgbBYBPlYYOf%gEbWIX9ndfF6ARvicCZO9O@omX5=D{Cd(Yu zd|q3vc^sxbSwObHU5JAC3pVe5m*~ecN8`Ev4dXl&B+WxD3c`uf=EtZ|t%ZS4oK=l9 zKw(dOove!%hR@P3Jf4~_e4ggkYl5(-p#EGt_x?ej!VYT5Toa9;ML_^7K}`UZsD9j2 z1vr^K8dMgOHYxgoZ-T-ejI3@0U=Af~d$Skc?Ro`bw{21cW*JP6hTovt;YVmY% zGy|o+%ClrJ9C7IZ)u7)G5w=MsD`naZK?tQ*kX+K?j+{P92~eQ93@^tJsB0!y9jug^ z_sYotpbKi+D8p+{Y=`l;VAY7B{~^f~9uh;bNL}uILyD zw3AAzdIzA+{TTldRDW)$$iI^qch;O9J0Wcpf>BI&E9^0KdpsAY@q?;0jGcaO{KB(C zFPY(KhDiBr7l3TVw*3`hUkSLGUlbI0{QEQK>ac=~g>=AD4z$;ziH?>GE(01Bk=3VU zbr|kz*jF4oir}g&1I&ncug9FtSJ@RL;Ii(FutS+DFMwCMp$eY*+E<`^XdKS__k8hK zo!QM#V1K3c%rfftJ4b31Tb|%H_5<7V)A|P7AIlL}J)xE+^aw4ab%e&ds?K>fh2us5BzF_I0b#3v z83eN^O$k#tk;}Ti-iHM>GKaXN)YkaIjdU*-;@WL%quNrXV>_;H>nvbz`tPkICH1x>fm3iW!v#L4+T7f{ zT2kPz7Hk0%>$P=tqry03vjG^J82n=W=2Qq}{RMdP4eex_3@e*HqXxQpm^oJ3u&|k* z_w@98pU01!G#AL(PmkeT3^l|0WKz|0euvzH+@taj~+3BY_XWG{6d%?EJ%?kG6?v5F@K=yL*YwNMIBz%8# zn)9p&QU+yJ%>hNGO*Y-&QeLdmqiOOZI9vu` zsOMy5DFs_(rl#J)(AlfTI6(=C8e*;q3xO!_YlPO3Ij#TLea@`q3UMb&w@+GDJe$ZlT+q}v$+za! zR_JTzn1P!fAeDLY?)K5CFh&?MCCcewCUjL zq-D~G&7Jm=tcibX@td+64ch@GRSyVB-ycQ#^AuF{i5ZoBYV0quvCJkENZd^?lE=D8 zeydpQStC_?O7-s>&fih$F&>mDzI?spvI1CX&JkoWVP464T0G0q^vwbxCc+HoiS!S{ z2Rg{q?5UE4M?zHt^nsg=D(l4KA&u~12~Xlk-uSr10(hrDLP7$3pbXG?G?dVui^EwR z9UWRnxFFTOua;-t>7F4_3A|~sXO|h1(A%%`3mxE>UNEdunYgNi;x>sQyHaY7At)CV#?Y z7C#76ZeHNrdI?UU__c<^XRD;af$reo;NG5|^Nqr%=|bRAa85yiMtgfZIIvr|#teC@ zIA;s9W81XJ%-UjEH z`6l$96tSfZ)k^j}a5JkW#6y-ig`}&ef?YGFUBi%UI`!ev0CA4ZjdcbFyIrrL6TmJN zm2vBzHwqR~6cnzce0IecZsU(Yrgz{7;MmXE#?Fy+WZS6!FNdK_8+`lh*em$_-J0hK6+ua;@9xz>P<= zPP;$i&KXqDJbtqT40VQv3^B%Z@RXA)hOfG|q(8l&srUbTBgBhrKp>ibm}`e;$vH}D z<0SN4gH3spz!uXHn5k$?4~^V~mvkk7aKgRT2h2=gS0Jv_fh*1vI6%i;@vW*F$(cX* zVo#cHKI1G{=)Wb41dd4>4x4s=C)66Y^t!1SIo}WdI*wpP3DoRmze?q56oSy0d`1Hdm=0 zgw(_5XnP*UMo~81F5&o*unU(E!J`I2L7qw0B(TW=J=&L7%C?8MDRJ&lfC4Nv>wx(a zt496p-RFM17&{ek7P3qMd-LSWhe?@?%DX^Su|tK7U{e2FUDKo`DNjr0owuEB-=1@f zFRt?%`{`N+6-KAG{!9=JlM0H34hmnHpfErF-)@?aIJVag!cdqNr@|L#+&`cldT=uA zk}Y$3KNU*fkbYyA1-ks8(^vaghZ~f5pr6)-X@Nx)9g5A+?l);7=?p}}8XW!n0{T1I z^Rlgq8pTTtU1*ovQrNU%PLvG{mQ)S-$c5(%6zUxe5%sqypmB0wf_nlap(w;DW>iQN z8A`6EwBY3u09R!P*Z=a4M#(#$yevQfGLrwJs!f93*8SMm1l~LvvLO0R(x5s};DQUV z0;7TOPyKrPG&to&iA6@)#3iRQV@&;>{~1#cFF0wIM1NOj~kEL9#oPGNz~ zNt+x3E65H|J>eMC#>P67o7mwx&9hPJIa=FoeYv@U`-8!u$dyw zAfeak)DqtUXra2?t;e z@S5&-)b7#h_5qJ^bs_?F3~B&}4bF7om{2h%0FHVfn89S`cRz56DlmD6ShxQ)JW0;q zi0C&hQ{uDo2p}qTv#lTc+)D843PwPW+a8c1k<&7x;DnyK$+8>5T%_aeLm-e|9h20% zoK$2TWuLr@Tp}^!OPA_4_KS*xSt!Ve=d0>aK&C=Du1Ioke-N+W1K(@lQ1pzNGZIV5 z2_JpWub5($1!s0ynVGx5>0bA?D`mEX{yb^l^<37oXFb5HNZ_~1h|o+t?Zkt(Wt@Ni zl3ejfy(2T^nxe^-Y(1VChKZO{Z}!kNd4@G6`|%nwNAT1yWyjmi^{FZYl%EqGg1R?B zzXpS>^CFN*GT_9+FA*34@82<5oq)mH+#D#B+B-RcmM@Gei$Oydy!!;4&Gc}0&#bE2 z`IIChENs4V0^yP~QY^;PH5!D%owtHa&j_eDaznPXh;z_U=faC4g0wp_o4iT_ZyaTj z+Q+_ZBfRNYP=XfB%{=nr*RSWohtsD4wcTviy>c}+O^a*EGU@Em)4%JhJQsT0I%U1S z;=-FOC$C5HJ#ht?YJ&ivP-mgph(;%r-2oOy1d$mH}8TU4*T973dKbCkbe013X zkZwJ9|M#ngXARprkD7hGh627uszQngQ=H z0PF#P=fP`5=H};hu5;9lxxp1*B|uDn22NxNk2}bN7lMGJu0Mq2@xhFr^H9YD!}7lRZ5(w>LkRil*^nP0nY4>PgRX$dl3 z*+oU#-L^VF`LK)lDlxEQ>@uF2MMW#0k{qldvU&pF8uXBCi6RneY(px`LIn1aS1W^W z>UJb?<2$HH-TS-ut+&>@|F~Q_Gu?gqoT^<_yLP>q zoxwIbC10iwR^(l&E=9&4|EO%1$FxOHAPm)a7G8)d=8qbmvsu^G^ClHIgbUVnvgv(m z1STgCJjl^EQ*rW6j|O8l{)Kg=J~E|fZ(B%62yARqUENroxt|%{1FKW5(4?6E;K32* zJHDS#+fi8TtV^sfRS#U1u&RsnnRjjYW0FtogK5wA0MMIO!Hug5M-6pE zG8|koK;Y!nP~12w#In7;-Ln94G<94#NB>25gkOf)hmh3XY)wJebW3wq(-nz`d%LJ3 zV=ZcS!n{*)bi(OY);=T|YUi;JJnvmY+r@{N>$@)`OafT5`k(MlgN)dEG>}=0nP?@> z{26iFpZc8YO~tiBuxG3iTbW>lOjya>&psKLTQVko+BR`ssQBCUelukJ8fuM`&fV*p zPLaR~FyD-%-OTc8Abpl<>FxD+$qM@hJ` zWj!SKrK2gK+s{z8*DpvO(LaJNOhq(?D@mo3D^RnPX_Ft3Nm##epWgoU_u_<7cu_wY zBS-A5yTr848We^Stx%w-a?ttv>`QU7@ha1SZb`Dyv$*jVK*bd~gt6(ee-H@zW@^iO zbg|6%R5f@pJP&w~Kyl%y8Ckd$c`QEFY@X=haR>=m(N(iGEA>mF^$+AviKVv>)V#1- z>m4t#wG;f`frQ`j3}3*jFV?SkHI&B50kD8q&4rBnC-92NMtg8mkYME`6HS6W`{4cc zUfch%?CfxpsG^?*{PDN=#r^iF#~ernSbHSUh8W>rE^*{+G^1CcUM7B6->ieHD_un~ z=B*pRjPZSBXROgTzo3ocZWKZKSU@oAs-a=bN;cn?e!q95IB@DTr4wai9k{aV?ac#R zv=YdKZQUh~Up9IkG9@_Dd)Dm;9d-3-nqcCzF zQZcswtl~Ewo$p$Jr5yRIn4N#i+88k8p8o1=PNN2pm(^lu&w4~Oo)SKN4(U>F*ve=q zUJfJd6cEP7#wuPxNlwmp@>axKEWZ7DPqCPg7=luPsM zWBs4fg6%pqGA zw%8B-d}+o$v8#)r9KUowM3GtSA+_P#7czcakww#my^)G*#{gUC z-BZn7vr|!Nj;Y_6Y>AeZlY=_B-^QW^NOJ&zRTFyM%#4hZAc3f%5n1#0ZU6W0W##23 zq37u5PZ#)1czC!3J1x!~v3Fj)_Po=$pGjr~nJjO@xqLY+s{eIDwnZL@!p}!dbez?IV zFBr57UTrya$4{q!d_1QfYUNMM%5*bMdk=ut4dXr4K;X28|f9A*8f%^adso5XB+7!ztU})DzfW;nbf+L zAZ)otIPyXcoT%X=T}tLuu44jK<9P5$JB_Wv#Xdb>OT zT6g_JLl$kwxBmsEd-o^Oi_JxTe*GVL(bw0!g^tx7KXFgBuY2I~!tk~9vh}d?Qvz9M z3X@Elv_to_b{_3GhcuKcq^)PmUC~c`vC+2WJmy-!x#L&Lsy&|_sVB%mWw+t+M{=g& zV#SZYdx3xdB<3%el^S1l+Yb3y^nAlJqpVuKbFFwrwV_9s%H6}leP=uAKT(2S1!r_i z;hr}+GCUh?w>wEfq|Ear5NQQzrpqX#1^%pQSXfvw<>vo?{?g^eDP$R%RLPBwOR=vZ zw^+gAxdSNTN3n`5IL#uBsuC(lK0e@;bRY;Ln%S3m~chR2)NX?Pxex@B%>Fs0=xjXUA| zEa(0NCy0{K-#+)4In1kB?1kUl?YY@*x3!y9Q`#XXk<56(a7hLV;yyMCNU@oE3pMSK znaye*>Dbv22x;PLmaDk&3Av>mV>goaTj8)1M5kCR{uI;%6~!kAsK!?9%gf(OjD<`d zaB(5YNh#gu#@)B&a@Qdb;V;8H6LL$~eCWi}`9iz2aefrKu-plKpbMUpqNB1xQfwsf z^=SpvM53z|vS2l4$d{DBGkrxC6*x=;1r$`HM&^w)@>yo|{M~_tu#QK46HeC4 z4+kgLuHoC|621r|bCEgM~qQ6 zJ|lq?d8@s!-I-L8yU*0a%5r`b8CN7myw(E3{st6KD#H@Pa+g_FYvHDkBSJcmUj391 z1NU}{ z>b5BD&0RQPJSUugQWC`^f^X|p*|4+dk|euKuYEXFPjQtw}3AV zf^_FA8HGd>zHDG>p+Q2{XxxGyW5pc2px1}NR)gGS_ugLsDD~@Qr~naa{l|8{jP-%k zkRK?(6ulxv7{rkLGib)?7WYBM>6=(il|@>^Qj5q@9lc^=8C`NQp^?qzx+l%@zD290 zq1)S7AAZA6O7Cs`{ zU&@4rv}aiScg+re_r+1<0VPap+TzdJ4J0qz2oV(A?r!jpfH zL_X0!ERJ)ld!(oUK<)4e*YqIiBtys)i+sMWbQn_CaE7&~Q_xJQ74~GwE14&E7Wd~Y ztXT~pp9i4HQ?WuHZ_R@IdgpzDb#G~Gzbv==2^dDjz0u{pN03b52;23&-3(bp9Kz5= zR2gh~L=s2ME5Lkix(eHu`KR}yv+gy9Sq+lBy4KGY7JL5$JBSWDtZ&fB$Y>#Ot>$kZ zwbkcK>V~Y6#tMzHnq0ELxA(RtgX1fD-{JwI5w(T67E06i3*S4oUPy0gHTDAzLiFUs z=x2Y`h3eCaPnthDlzC@PLCx4+>m%}_8*)erXvL9*gToXme-A|M%E8&o)tx{dj_W*& zS&B-hE%0vU)<+< z3Q!4PP#nvF->zd}Iewc>dHQZj`mL)G`qTqo-a*hc6g)cd9XYCj8~<#MY2>~HO!O-) zACXLP$T+=*1Zzf2Z0sda71*BDP$~hDp(ju)%HCfqg6#^@l;dj~NX{+#$G<69aC=Sq zt3gplCq|-Azj~NHFiFI~hsiTscQu3^magJZwd0z@i0Jqzkd7;^AulwchC4hwe4wuf zs)fcjHd%^0L!DZQ!y_Z?qwGk`MJ@X+Sg6C=ynZsW3eBg55D2bNEf98+w#8NH9&;Vpo3*htzY9sLFM+|7$HOxDu>(ej@G07ERcG@G|ChbKd{qy zadu30U6OtB4(4!Mo1RU}c=n}dp>`bh!>5H9hxVqQL75x!fl1uVEv@PhT`a9VK)O_BU-M!rLx8vnxdCzk$e_azE{{Y`-*YR5Xge z?5BFQ!~@FEVi@@StB_x`W#K>#q_=100=ffwW-)G(V3k0(unzDG|0Ze9@dRG|&?(*K zPt@n8AMd_8Q!eCk!4MatWnA+zQnu4I3L>n_2YGu;6aM#<3nnGzYWtU5_9 zsr{B1ITPLrT@n;3Wy$%MtPv#BVo)tf;fybGuZ0n&!PJvF5p^m zY5VyHMJ-%V0D4c4WTp)`x*Sfz8{C^+wgM{(c18j5>H@n07~LIGT-%^DR(ha+>bJS# z@Ey{R^Gi$psHq<1MPkOdXvqXBava>iAd@mQ4`(y=^iy4hwa|BTMqX;nL+0zPI5|^_ z5JYO)MQGGUm4uNoGDRvg;FYHyL);?Z9L&e>&&E5Y7b=3KZI=;!ipr6IkkvIGFgtPk zVb6>5n2F${>R+MMPQB7#B3PMSapl7zWSadA)QBze+zJ8I$Cg+q-6@E*reF>;X3 zIQbDOBpn@kBoR8_i1eAskPZ&vi{tt?S#ickf{+Iq9=GW2jrnl+WHW66bVnF*Tl|k5 zVF~`OZnS(&90XxIXAYBf#nIvaI70XuNwxR#%y?Ssl7M6X$&UC@fcyXv}Ktq_EKlgNio0`YQ8 zFGMv`p(sB8Ds1PKiII`fwz&YKP&>~^Z{dZ9CC4Rj#qL3yPKp{`nl-VnG|34|PZ(E9s))_%Zj zX(D9;z~Hp|7Yny1(jbzyN#$QDM7VXZ5XX1&|TiZG%N_> zu*2lUpVQ;qA~rsd`g6gnMDNu8zjpa)5m&I*v}yIzgJ{v2s~2`Dr^_u zEkGLA5aPNQ4Zq~aw)9ZVXH-h`g~#AY(Dt&KUs&h`t^NMKJ|4~a+1ai~3OBUE&o_(p zefuWTD?%4n*Jhj=p-z=YHQ z^2{Za0_2g7d z6c0Xm=MIitA;=iGv_^{A5N|^=-0Bhys&5A)1X9|krCdAMsHmxhXvt71N>p(6h!#OjE1C5L917^BeXT8U}z{?;=J18ma`(%yj_U zeom7_MT1n~w$hHQG?gB}lW>;G|2T=(+urN?tc%F=T{okaQP$0dq z3)954IL1BTlYjG1k)(lh=S})M-U;34)RHpi;xdn&>5#DYG`YFJ_)wRJP*%CRd*fH? z->hoU`u%&J>O*F~(L`|nlCY=E2>G?rhR;$3zZB+Kzv!7qXUh`UZxqxn&wa&c8qTEr z@fwM@8~s}y(OSJ6Xjoi`t3Kgg9kMt0`=1)qjyKesp!U?e5BVi82{_;0!BeaU5vtCS zwsR;rnLQe|dK8pKo0y$3=!xsQG5Uw&gpK8EhcM8>MdduvDHo_1InSKUa!~4js4AD5 zZjxvf^=|(PXpsLCK{QTYw}xc;v-{tsA15M*9O0R^uy#>5CP+oDKlik(a52;|j+_dL zo}Se~PAc(XhZw01|Xif6<1b1SKXd1V|nLq$e-t$w^;vxM1F>M~t|lHVNXGr>9eR z5VF=)AMXvcG%!L7um{9rfpV3HCYKm&35}$R`ssc0Mu(2tetj-Xf6v<|h1Frx<=9k%foRQS;m6)@l61&FVH2Qgzk??XsmlCE~5 zX}`2gEyMD{-BYvZC6ezlAh+^@5>0XTkSAMx0I6tlV1#5Ukvyayzp`-m$i9nQW z0_?wK#NtRvtHVI=5LzG`Rt`1fY;Oml+RH4=%)UV$NN{=B3;j5)dtHlG7Hv%9cQ747 zDTs#ba{H&Y7jVn$qvX54*3O$kY#wXU?+#VoMIh=JLrV8~9(PoHEJS0FAcuoAi~8*6 zor*V_7ErZF6QgD|X8hG{bqS!n4Lh9sq))}1>O-x7xN3Na_qGYu5oh*j-wzwGIb1eK zpw8;we_uKJ&F?mvf^9#JxFtc&+BG}G6y0Q~SR+Y9Q%`HIf#GkA%+rd8nDbor8y37O zk%_*S;+iwUy@4D8n+d?tgWFs3`I!P3^#TYv7p?v~8hjE6o1LB#w_YAlrwy4LIK9vs zo4z8B_!y3n@itz2CV|lK+<`R2u4k<%(@gLi3rs(EaXzHpfKXY>y_sTir{lWf4b+?J z0v60$D1WPmM((9x@*pkH6V}{pEr!TE4eJxa@G-c(w6R!ZPX03E?-;{T>4TeYc+T&} zWNAr><)fHN_l7-*%vZY)TI;~OMOtG{lk=-Io;3Y{V3cKUHxd@BLbEwE zyNPH!n3&iNI;1}Rdeu}?gWGdjiJ|77N?%@cLB=Iu(>*CF(%1^#zd6Xt!ov2I3SsQx z{um0dH2_efO!K^1bxB;qS(k|Qon%U7nS%HU+m~~92aZCA1wd$HDL9s>`l&yRVmV|x z29n0P1}8g^Oy65NTq9*7IGdU|3>j(W=L7bIv+mu`ITns2NW2&c6ZIVDiR8R1Ua)(u z;z#`q2T9))5$V6d>UlR__`OvNTv-%UfvEX>L8@&JutzDIeb93l!*O=Q<4MT?@LhQ3 zkl1F2=Nuhd7O!5P%FS_@1%AUp{cYuOy`xf%X~!RnSGf=SJJ;Ei3%Ft7TpwLmM;p(8 zOV|gLRZ@;XM1A(0!LtMt_-tM415bWIXD+? z77wcq%a=ioEb@I6Mo^2$)y!ixz#T0$;`beKTyWUO<{ho#Bf!o*@qwtan{{*rmv!d) zm3JO6bioo5c`EYQ@c`4X61Dzgu;VD^0?FTru^5zdirS5XG7U4*Fnp9V znWuX276dW`^IkK%dr26w8x{Ty=yWl$l()yz8Kr zZ;M_R7dt+hh#2NlOxgc&IF9rDY@P;Lj0Gy@^#=GS`1m11r{w3kh3 z8dIasnm3*l=L;LjHO7ACvHx27DlqPqdM<^8XV(!^&PHx-W-!@Mh(T*te{VDtL0`H} zMg4XOnCpXTio){sc-Sw!Rz%}QrnvDoO9|q|mt4kA&vQLV2)1n{qiy7qO@q8k?s5@T zDP=o%i#?_NQDNjGLc0U0f>F<5DCIX6z#+YNDIN7IFFl8H(W)4F_n=g1ps=&}e12}u zH|Pcu?Cvc{bRWrKFg-l>cMqzXx^Yg?PW(Lk9h&aX$8d__}Dm0W_;$%OA8ye#gtRBgBatJcko|%m?+%=KyJ60)#t(h8rXlr z9PtOD4cqGrpjC!&$|bXzLVnFEU#}Y18Ijp#%2xl5i}q_vo8DtWw!(wkd9c?F32y4( zKXLWl!0NsR)uqE?lnM+=MYWfNl$X_z!)PLA|F(-4 zAS)!ejRonk==9SnGEYl|O4P|#EzE&!q-p2Ajo90X|Dmr`DJHeQrI>xKn4I{PWcL>#BvzKrqt5QtQMn#kHTyYY;4To`_v zFC?!8u}}WYCI8}xWaT!HUf(I!IU#b6UMfk{ypfQOIuG4KYPmn#)d`R{DTBcg^JvKQ z<@8von^Xx*yZJolg;^2wz=|>BJ1h*@4RyJ4fL@z*Tp3|y6W-4>z-K=`frE{2;!j1r{GPLv{<9)>w^B1e z;-sdfH3Qh5{pT+`C=hD7S_b5`j27PO3dElZgxx>e(G8o2LKZJ_Ch=6ic!7p5K(QY< zY-TOKRIxYI8EOiZpUnJojmsdcIkvw0r_-bJ5$FzfYgD^+ABP*ATd*V4P&Rj=apF@D znFJk=eo$$D3W?w3X(r6;_VyMfeY3&SvzuvGsh2-Am<~!G0JZx}1EL*S&=fN6!2g`t z`AaPdY|sihyIp0_iC>!)SWG+P!FWE*Q<01W?w=0YGh@V!NPjz^aFNwc68DaEGo1zb7>a% zlj3lF0|TZH6c8WMZGuT$e~Y1x7G8YB5w@*7MtMXv!o)pR)t@J&Z~y)MQT@-fSeNMj z(Tlu$1ka;llwf@QxiWf2H*-e6$_&n(0Ez-?vw`n7XBM_@w*JLC)Tp_+xn}x-RUg#F zURDs$2xNOoY5yWb(*hjvPfOf)M@As9hAzo7XRL$1i&3KFy70-~Vi*{eqIIwHXmm^4 z%huv6|IMabPQ5thUt*W|!tJw2*{7XfG22b&8VXZMJHOV`C^amHTJMBK&+L6m5O3WW zZ_R%5(mpm$px8Bt`f2^D*2rM$?pFPd|I$onW+G1+rCM;nVoK#k+wfg#C!?U6nacF^ zpF{Qac4Gnms~y7YHz#40bNgw-wlIjvUjUv|zvy0%s31pt>e52C>{WF%g@Ml5(G+sp zWta1V`6Dp1Aq%*e&{zPtXMS1q8v?~-$oBld`Xxq1F?s(bz)2Q&fuz2sD z0|{lG6dv2bvP>kixS}`iTz~-3@v44VPm6qO&Efnku`uyyU6OPM=+(r#7-KH5bUrXu z1s%MIs!3^AKr75ybPCA+V&^_EeceA81)^if>{fk4dilC!hrx zeuX@L%b5{DWv3!}j&cnXX;UxeeS zr4zWcPrm{>Cg<&IIF5@`q7Q;oNWaQ$5wS2H))6x!r z`!1OEtM|(%c0(=3W&{la!!?$K6;+`qBak&6o@i9VlD?ML)H|HY)U$qdDVvxQNrloQ z4jq*6@#9A|qEP6WawQ{0tm>>k)0(T?({HJ~`7AQY7sA4`%|3w|E9o9iJo4t|_@?q^6Y~h7 zn0Q*3UF!ot4}S^F>s+V@@hRhfj~Fc~yt)I!vC17K1YFV%Tido1@w8fuMWoJdk>|Wa zd2pOVX7C_j$?+1sQB^fe_d|fY)+Um|Y9x}P4*+Vq#euQX%u1jX?~u$Fft~(yxVEbLH6|hQ%$&vHQ$a4qpISl9EUoB-$ zWhCV;glC)Q0Xpu79d@PSZG6Zp%1DYY_0B}y<(IbF(RL^O$9ciF4#AeyNf`{?h%>NV9OBX73JyH*mK+NDGfK z<|wB{#u>P`YHOmIv8HbXg`!j5JX1Q$shq4a3N!iw&tsaq`eTWaELC+{mqSB)DXrTf z+T>z*t%HSd4D-dVl(-Qw;DwDZ(ULKr6G*CCJrFN$Ig%=rscd7cjs@KX#M!`5AksYe>AG;het-aPayw&GP0aqTOr$#!9MApu~+YW z>WNSYgJ~pH|9yR(ksq-;%&U@Amsp^7sV&{T=;%GgRi4qWl?RRnEl;K)tEX4FPE*ME87UUiAcvMfn zvISRLBxPfp%ij0u)cR}$L${w_=hz#01 zvWZVQri@hnuGW}8EJcZ$RPj?k0U;U|e|rcej40x#FOFLRWc1;XxT$|~CY1o|Dd7w2 z$%C%v6uO>;^tWC5=&+cSlfZ%2Y}}#$CT1Um=5G>TJX&mkzI|e7*p+m4PVpZw*taT% z)SiGn)4U$9R3b|IMHJCRIrFQ)k$K!5o)s6NEM{c7wY>e|4Ny-j`u{oPi7g_!_BEv& z9I&d^m}Y%fo4ntJnnxg6g7)#m=Ml1ZlCgMrn^H^6xdc1jJQ3Py{Wj#swr#h}7{(YF zxg3c)e23#(`D-5>GzD+^$)@QDU0{(LKeR@;idqGbg;8dKjg?zXrGx$O*#g5Wfe4Qb z<2+VqX5cVnS&MzI&}?{@Vk;OC>FzTRAZE22d&5T`|A}jOmc6# zf>85s{RkaMhjs?{Mk4k{mZE#PKYE!;k~2w)x&K_h608r$xCYm;qxIr}X5+)h;hvU2 zrDdctEtaN-_TAT%5|ExjpJ0WCutMYR>Df2fu6bNhA8%h*6;v$ zU^J^600fj#2tAbkyPLj{RxEz@Y(?Yj|DyFB4CdrP6V-7`?2U`yd;O%cM0Aii3C!0M z?vSmB?*nqK+(Vv2v_>j~hBj9cVp~ndo7IiR7ESqjZDGQ;5a+_d+3peKy?PO<8~AuS z=-B{n6RH&g!}I>4lVs*0clHE|Jtij7kH&`%fHGh0Wu`knJF_d0G+w-G!m)AN0!929 z%OT%N&f%CZYsHUdoKm0w-s8;;pvp)aD1(h5xG+MwDaclNkl?Ii*($q?zjeRk_GX^yhHQQIr7|XUaSp3p9<{l#+>{OI zyt>3lw_o~YMVR33XI<-^;Wb%>LHT^6gs#$G(SN^ZcyNk5nqf67@5voLbmRJst*^rV zOYLQZBeApDxC`!F#tI%gQz0{7Ek-yOS{niS_?F=R%juu#cpmiH)?sg2ZTWZT6Q_lD10vpG1?;cnn_&;m z^B7X>XL-Y*aAE=yul@5wU$$m3AFaMse#hLBebhWiGFh|HcO_|oXM6oV5V`;8N=o@t zCfcvrOu(})yoIs*)l2_hKx$U?6w3eEWJEpBlhiwhn6Wlagf~Z$@Zu&n?nGgf!JF5* zRce94z_hMWh5mfJV5J07Rx0tjIdDkHq5;W7z4{sVS_0>3vdh&qj}-9_S+wxqoogu{ zUS&%%YeP!8iMn)4@h9K{dT6f!huXdSI~L5~{v9N68XA7ky2@j&Hq@YfpYGON;4`LK zyLKW)vuC()u}xCE5$gXnpu?}Bhi67_!kF9^Hb=UD{OSbJA>EW7PfEV&Kfm|jBL3XYZ<=5Fp0pN!lO`*qQom|gD!_;~qR z*4MzM`E{7e^pb;@`6qCog-NLWqCtHWO%-2w`2?2>$Hg6{~8DZrdgt@@S62Jq0#L zp@Wy;RO%mg{2scZq!t$*kRvr?^FdFEA5tn?voPSlN7LKuJUA?M{Owp>>D-qc29>sq z!sfS_@zW??xCme!_E;WlA?;=zs(Eq(Tuk{kxkG)64gcc zkl9m!zt^|H$)u{_a!!|eMDXhI!KE_~&;6?RzU82G< z{e;UUU}mY%PIN|S>lhcOL{=XGCEu4Bj=*#Sz=abYVvY607>u>`sFZe{{il_HS5Pa0 zR;UeV*FiWWIglT?CSk1zvouf(k)u7ieZ$wIO7Rje{oj(wD*+g5JR_+6` zlao#~kuoo6sfIqN2nh>jFa0htMj&4uHE2WfiN98xs?G!Qn{^$MO2YdZn7$F1ZusR& zwf`x@rP!SaSwz9?nI8p?T=q6Zr^##$0VJL<f@&hMMn3M^6n}hXd1Fl z$KEk$TMVlI+Dj+H3EBodEvV^3oBKJc;wdqmLb_N!D`aNN&I2LaBUo2FFJHNl5yNF2 zb68+<`xl@p+&D4%q{p1d0tW**m2u(r-yR*q;i1(516oUp6~Bzn(PNY=#)C=!{h$uA zQQpI2uZ8!0q|n?#sk}{iGdhgJg^y3#YElZqLeiaBi@LQ_=2eNs1A+BxBs6gZezcW9 z6rLJmbr9MaK`I2$&Uz{Rcxog$;+VS{dBb|7E?2>@2x43)wBQKn7%*7NyU^JUn^OQ# zytcsN+BJO_k0?wd(LIN0o!*J>IrK5<*Ox9(J{=}YW@&4#15=TgMbk!{;q(^d}TfW_Jpw>DaCWNu(JoW(U}7ATI5;+l1?*n>_Zg z1iQb-P=q|~m=XwG{WH(YmrWbVM)fH+q1toseXfizCD2=dwjDshW}QFmH-BP2I&~UV zu0~nC0ku=?a{lwldfAF)=-<&XJtzRE2%Z+EUgl7KBZm39V>)OB4c657{V-cq9G9&J zPc}?ldW?4fMGtW>l8dI(YdEp1b#-;y({}sLdyuTJD=DEE>v{G}JY=((a{wA3&{pS} ztN{0eR%ew#+Z$zy>%|w)mK1P`2YjW3yUiLH`$8?^E^zBvysHX@Jz{j4fn0JBlv@9%^-X8L#7^`0lMH1m^SL@Jty z?UyZ$Sp})~Wj~E`BB!{(xkDcjtY28Ju~5O-R@PXz_uhc_rNF?~$y5TGy1Kd&l)#qy zr^|$bcvBqt_A8LD%;~bu)u$dWxQu~ODW(lxo*r3y3mqqhZ^Tg$OrM|AEPQ0(4)_bf zG*%-!xoOyTn1{%sm$!&w@!5ppr=>{&54b#5i8KDh5ESC^maSJa!8pE&x|V{CMcv)b<*{cJ(Wqo9So(DG<) zx8A+rr!_(*%Qk=ie8bH8$-xw!oT@7NCVSdM9tRk0j^K-n1)W@69HEjyMvVRV=~L7D z_ry{xRM52$4PC-ED}ORFG8jUV?z%nLkmFlT3#YbrXnQZ(Hm}zDSjPR#_L0mcWW&Vv z4z}*B9~=FT2G6|LAx~JtveTlM)cSL$^eOlLtX`{Kdg0)zc|y_Cp<81~F(8+rVuXLi z)8O!c>q%PnNXNyDqMg;r*N4YM1e#s198zmNcKo)0sAzWY-&O7@#!47pOi4p?5w>6; z6sQbrZR4V%D5RvN9iR~4=IQxfTeR*LNQgHrU7G^6r4|5*&w%9ywPg)?WU$mC0;tBB zf1kg){9SH_bTAl-x#U(>YuoNN@P}6Y5@9He5%C$y-Id*=99?dEvm|`sujAOJ7~2~Q z0XwO`(IoJXwmWOxU9$H%MMTp3JGF0*l)qnheZJuwM^~$0-+Xt7DXF%0ET!&R7{LK) z95XS_I}=Q)dVDbZP<6RN>UsqX(S(O9qN=$*^K-9bq@%hY2*P$_nGLBVjf_5^D=?n! zQQib>yvMzBD^Ss+K~OAQYS+Ml_&~A75m2$u+wWgrda;6r_QaJpD?oC-8>6QxE)k+m z`&P}+#!LOkMl4NU@8#y<>x_uzkPJq~-&aCFP4(qXRw)+aG)Yx>S8gAF{K>jY5BgS& z`sU_$H#V-Bmzgp$GO9Mz1N#g9uA`@yTPfz3;3mbAr*e*hf+A;dL9K-^m&u)`!S~0p ziQ@POWilC8tPdY4K0p1VQ`fa4#Kxi$l!YJOkvA5Xghzzk6$nox;N{bwU5yT-<4Y2> z+?6-;^%ai}Qx)X-{UL!JUtDq0nr(KRi?%;~<+|_i)VY_Vc*i_VKeni6AAHPQg%9CN zj?Pg$ow2wtC~W4f?2t|uhvy_Z896JXnmlQa@EkXMee9K_940W~m@a(j`7Dq9HKb&~ ziXjgvEYdJ^zo&EcFD^pn=Zok#6|7p^U$>7=0bxQ@FT;*&F&;tg-DNy^CA{nX^G4&x zeQtNo<~3Cg61^Q0m7^wa{6jFb%jatJYMlQ-t?Yl7F>u6l*FOI4WHgEL@#D9rw!-Ea z1gTXi&gP$t<*Ar-bAxq731+qj)G?BZc0>WeFLmzco4{Q4B<8l@|eqPLG#nn}bt_9?G z;r2d|H_n?JWhZ`j73n`N(C$VRKvL#NYTq`+W4~S%WX<&Fy2HS{zM~Ypc=E3MpW^I} zM=2e=agkxn#}nsD7!JN_k_Suc+89r96{lmFH7mpDiaupRuJ=VgyOI9f(L|D1p2w}? zEqMxe1up+{s{<>yC$z2+73+EB6W#xQwnV&cFSRp3`Yt8Otjk$t|Kns-M8JYrw%mc~ZZh|RHNl422& zw4hCPW}E(FZ4y`;Dd)>7M%Ti6@U$(5i2Xh8ULzf%z3<#~Nq|lUV;*8otIrRc-N=dF z#|bM%XHfbK-D>*1B+9lxvN)S}wzxY6FOt_P&MQaF+n51I0#=?cfqZ#M1S!#S`bwiT zZgf-+%a=1Td^2fDzbSurJ z{g1`I2*AJ5k`ibQYX7^%I^Tbueg6;hENIsR3FwxGo!?64AS>gNnf4{YDnW>pK zPM5UrauYk)SMXX?1@9i5r6_A2k(q1C{ZV>Y7kGiunS@t~Y8aE^S59ZSHD?degyi-e z-SVjj=loS{=(HUm+==1Im9a-}IXv#RC@wWW16HG@@DM(eHhn&qNZEYF>@d8wVYc}- zFDfORK((6Ux2diA`S)Ips*D#z5PCX6{T;?mLf)Jsttg|RUHcCvBMKJluqNdXqw>x`H=d9=@{fnO%*(2#=NHG4g zY`@k#ou1jHut=yZFUV|0-kl6bpN}N{7w!0wW7W~rScd@pHaRxML+)y()KZB&GE}^l zKO!ETV$Gf^E>{Y!)d;neyRwe!_C1exYsz*7>%n$lJ(M#M9lGwTNKqiQeTn-s-<^td zH45w!SX)DOc#PbYp$^x&%Q0KExl^WR6xL*bgN^1$V0k+}YgpRR@U5MI#5)02`To_~7`Fa&k$ZPI3WHfrLCCRKIr zT&9T)J2^A2P2VnwPw$k4DB7;91-w0zq=u=6m;KWopA5DYTNp+-Lwf(NV=Oo#2{AD| zjCZXCePaX_APEi;KTx4{a=`SAa}a`~@W^~XPZWZBEA zj2DCX(~j5LilWNnA~rRQ$oStK-(KC*4vvmk3k!?apv?_zYU=Zew?H=iA~1U@Ez1N% zMBc9t+R-F^;@Lbx+pSZr@siBqE0yZ8YG#YT8;*=w8=4|*Ul;(#2vqF_kh~(4So+b6 z$st!c`LaNKNC>J_OVAG8QWxW+TBU6E3@d0vm)cF1>uz^`@SnZxj7a z>f*b>cwWVyKVNM`8GT20fzcZa^J*@o#~z)LJSrB6QEsq{SY@{N*G z_V#tNw;!HWgwb=m|SmBKtQ825tzZxI7+H!OiIu^*F zkKE5DHk2D(j$V#QHCMx zx26+~g46jU_sFx44h5;`M1|#h_C_dgp1GiwF!=Z5a#~+3iq|;4Vjg0nqNDQ&kJ8fa zB_`58)Zz0St!0KVlN7OrvCQUgubudI`84#q-`U*_erL?9`4@EYDm!IVh>Ab8~W#pgLP)rm28H3KTTEt!Q%v; zrh9h<4zC>MYe~~7bX#4vZfR)|*$0Q-TqK%VDRT#9!MD;CkR1Sv$!{ml#fl(3Z~N|uvT?`L8P>aD z?z7{G?KNx20Wkq^E&6t{+gISu@fo?_yGn9qKURI-e?XlQ>w!~Jb&A%Pg_}@Q#f4WA ziAl>4g|5jJ=A=qJk#M~mr=oe>kn-}(8+*aqM9Q;{sDE*cHM8Wpb60A~E+Mj0&lpISU2{-QhFc?rnhI)DP122E9kQ863f zue#lF-72kRsX^+*6>2W9R2s7}r{$#*Z^<1xgqMm4xbr~7%dn$z)k?Kyu|=4&=nx{ZM1ZPs(yzmZ?oh5 zd|~gIv+38&kvGDUy_lZ3DJFl@P(z*+-^B|*Zk!?6tTMh?qo?|(^}r1iJ2QWe6GXu3 zX-{$8?*o|hVwf){fv2>s?J#>};`#DX{23KFHjEO^ni|#^SJ+8_=RGM^NA<8`K~*a3 zV1<=$I7Q!=jj*~kn^;I1?sKRz(f=#|L{bDSu(Igtmfp-$bbXzJuwqKz@(%gZlF_x$ zkC^~oK)}tW*^}D4-@Qzj-U{=h>%NJP?fhfhb*cqp=Xa?$slfu_$=y0wVa&gmK4Hi- zYmd?Ih6VT85w#I8q;-4soGZs~)Sh;4g1j+-`f~17H93{|ubaXA|Mk`3p<~LD_T6*( z4_#txjT0rXd!xwbT3YQ3HT73yfOnQpRK=D?v|!X&{A9%^{^W{%x$B8W^IGR%AFIvr0f=3@Rn(j$ zQN7x%kdC3o9?LaHVTZsce|0`2Lj?W#r*RR=jAboH=J&0dmKxSLWXNTtD~ayAIMyH^ z2mOzybJ}Xs)A7oJb$TvdNAmC}h1PrWMk$7bo5(ASFhGn;d|xr=RiC|sw{MU*MS}gJ z?sQ13lftm@pB+>1j4LM?j+Dw}eo7W9AsuGOEYN$P0j=XLKwY^!t<=itu{Wi-{{aQ1 z9I3N1Qrg|r%9k;Pe;ZFUwhglE-!JS)Y$0!4dlVJR2SHHg>TB_zf1&`GQlMxEu1aj5 zUiRU${>mrmAuYNk(y6cQe=%_DID9I*>kh5vEHl_k)iscmLJ&uhv3F{ymM<{ zB~n#Y^=cFP6ZC%|_&o9mvF?OtQ%Shgoppv&$ADVr&cU9jqlr+D0E2nf!sW5u->ErOCt zNh8u7f+8Ue0@5KZX%GSuN`sP8l9KOOJkNjcecp3E>~H7(;7`zd#hhb~agA#f^-+jb zX6tZ5n2TwH%y0!3$R<-FBess8L>04gIY#? zbjuE+EI+>KgJt!5!@J2S8xZ2OyXi)n6oE@}d@kdu8HOMZqm(;U;yo9-#;t6DX^2j9 zdH?-ljEO+k*q5nE1SGnbq0#J z(i|b(b(Rvw6reFjb#--C9v(tyRUzT>8>-}DOWd~~+I%m=xl?d9ARwUTcz@Yc=#|1G z5u1DBA_;~O>(}3oWWi+$oJcvGxAb%837e#u___GVQvxmDMqo8sk;eue`3 z=I~!!&$96IS9Dw2NS)&*HqCQOzq>P@B*#VZ1&sO0mv8>1R4VP1j$COU+&dt038%~- zG<0=%3=8Q7sh?vRrczV zYHRE_r;bvSsvE3r$c~22so#I_#?#SSZrEEU-?ORLWo2NT1Zf@&P@75R`EFNtcPMK9 zN2Pp4amNVBRP zL6%rvwS%B9AzK2Rzn96$ z3h)d_vz4$JWb)H8GAdlVhOy<|@Nq%GiJ%XIwO;nk(>&;@HpLokF)T#)3h7!g6!su~ zrQgg=jUAscNX?B{5^Bh)wtbLM5M}|MFj_fg4RVnPI0gddrO(pPaI#U5clSAAMAP64 zyE?~^$Qp667lFuQ@#CrAWlDI(n=u=Wi4p*wshkbh5-Gt7zIVs^)87v?InGea8cV?i*gX)z}xWTiks@ zS$#0TNC(g>=`u<}-FKBC@1{646u}0*+|)pUfSE)szjzK%AYs8&h_1zBxI(v391d;- zE?`@5H~SnHq}D4_%+md?`>-XUgl2GTIEWEA1Yj)A+zNz4;lPL;JC-qjQhGjkE}h$u zOq9x7QtNRNGtx3EWwpwNWf6(89_1+gq7@s$NM`hfpqUB_)iY;R75}l|Pp7V$+&95v zZI?R_PQ0+vso|po!&fRSMevfz_frQao}Okm+|aY#I~S{21tpd(T0J^Y#AlO$n> z!IAO)AIt+#*ew6*d7;xU<#b;SdBd9tq+df@wS}-N@vHQCF+$8w1(Vi6U`>F0wk*;B zV^X5}1Q$R@qy&@XZd|Zsz}i)KOZ5uG%2bK0+t`B8L4puVCAS?~Hi4Z!W-F#Cx|)jd zve50Ar9D(J_T~fAzO-P=s8ee?U^h=~5}I;7P?%RR>jU%pZ2TyA6e;EaK?!|@Wq@Vv zy2}K*GTKa(gmx;Ac-d^hVFI$pY_>b zrfZ)aJ^%WO%RcYw3`#-eE_Aj{pm?qlOvJpOKU=MK`?dC0Z6Kd?(8#fdB$i2Pnf413& z`T@7y_>Qu#3+2$_6@J)Aou37w=MOR^il+x1D+<(S9@Q zb^Jh6h*i!%j~i2Dur0}+Y?jw{O6Su>F}O4tJ27H@H6u(p0Z177;;_Q$=Mu34_95+K z30Tr6Agwcg{<6VVRi{^=pu~{raT^n~66O-o2RWx2pl!Tqm134FD`YhdTjkH8fvKSi zV9L$vrqFRyz_%=gpGl!ev7+Anm`3Q<=U$IpU1{?`J+;oYoM zO2xfby-p;{mUQm)U@2(UU`0bd0&@;0MmfTD*Xh8Xy81-h=6Wx-xzf%4g^E!)$iOL^ zd|mGkGvkb!_onFM-MAeb%%uXi-VnZrItqOxH^3PUje0Si@t8*fiBrsCC_fiB?nCAA z<@~&N*@C9R<>u3qC`y04RW;EPh8@}f@(<*`cP`T@xqmy%37iyIn@i<)g4!1qQ5_++ z0+u(Y*SxTLy;Uky3iUs~qm(Td0g;E)OZ+{Qo3|7K5|~?=MdWH4@vlE>c{ zwzQBJ*s!H-XNIP1iB~m{9MMp9OljaT6@;-o=T#Kqu`>*w>mga9Zz~lzFaY{#iI0&W zu-G#}alUcrAuk>v0q&UNa92Q!_j)gaIu!r8=OXg+uMd@GGB&T#gDm7D95*20)R~!f zRKPg}o)KuM5cU&pa!Y7pgEM&pp+;8;Wg;U?EZsG?y1?PG>xdK@-F+Cu89 zHeuy+Kh746&Qc{m4~_*~&TZl8$>=9LRbLEsiK2{=gvW&_qP@i$6068SN|1{kOobcy z!xrB+0E7I8KD5kVT+{AMw;XW%xh}toDG~uN!)@cvAlQpT83N_0Nga}pzr6)moH`S+@ZE>Is$-O ziUon_?WMtyvipREcX-Ya5;{Ul?p6!4K0oF7kJRZ)!qTMN#}E1RMOU4sUUmjNQV7y| zMLjBtxSVJmUa_#4C4HcgF)khQ;%@nZ=-a`LSt%~)8TK;RG8DzT0IY!=M4F+^6lZ*5 zV(Z#&W_>-QzJ9t0BMhFjvbOePSE-px`g*|_5Cqcvuri~*-ca3*ttG}GNcOfkp)Zxs zHl6<7T6wNRxIqBLYI7SGVshUSC5o2Vn6BC0ij%!c=*w2~ixq-B$iRFtMZ3JMqE(BN zfbnq1f|mn;HbMG`uU5k7D%jObLm)v(=GnMikcP;;$?l$>Kz5aq?>2qKYO&Aj>m{v3 zUxX{?t!=a5Af6w)_~fX3VhFVW4h#hk`>$Y0A-*Rm)p`!}s?ITym+dg9b32)qI_~yF z8i$rUMNlW+50KW||1@>!gLN?1D*(a1xxP(!4g7z@%M&FC7-ll2k=ry5(u_`6S{MdVg3fOx&?W+k(`SGU4NWEA`(56P z<}i#@KB9pvl1wIF;Cw$oTc)0a@6cWKY?V|EOpRF>vWWSI^yPH7eLWfUppFzlj#tV^ zF=ubReZTUltZHHVdSS}^E>`ELL)YxgQ7UUgB==$2CUKUvaE(Ay{B9lV!@8BToImfG%i$@ntcW*0%qkT}Hd zIKzbf@IG$Bf%!zNDK+&%vuKgN$}o0@s; zTF8d%H9ExP$u2n{I#hV*N|BeC;QJEjC-ffVLbeEu|MVSvcwJYQ#(N{oST>fOHbU|D z_i{Wu62|6*1+I7Xe^mQ6LUgRZ{%UiLaz>Dy`?uO2!R-Yr74|tLp zbq-3<3IcoXkV*i+8@LR}$aJZMiX~%ha;d@;F0Es4Ei3%?6s9$NjoAIGRY9w>_QQqG zuXo1jaye!&`7-XsIb3Zb&Glrat-Krm8U@s$3eq|g?uA(9(#2>r-R2YwlmNYaXp_gX zNY7WLBR~U7Wo7=9n0k3YaN+UH^y>MH*{s4fQm4!T=@4->YP*a29I)K#Sp(H!Ouhue z)Txb(f0@WGdVm~TmmeLK^OsrGzY|tp;q<{;Iig>fUh<{! z8N{uknmG6SIjSe{kfh8~M99cQ4SjN_a>yO?PtO%&9UwbNgci=#?H{=u7|1pqAfu4o z@gKJl-`s;bhmKk5Eqzx)vy3;yPlyV0bYg2BZu zj8sgVza;wy0v*$682*I8a{qDUB7*3JUY>V-HW9bOkRv9K(vj4Bs`eI*N}f$$?e(JwKF>|I{g@aRQPlX1F@fa zmyi}!5rEa%GgHZi)wAmQ*uj^b-Z{fpWKWqmIw7XAB`>j6wxK2Ydb6)lNeGbA#J#7r6Z&9=lao;s`mpSB!LI-s2H=wGWS)8qU>ZRkClEza0T9^x66X!6ZMCGlutx zo5)>T%4d|>Rj5eQT~*3}kgt6#qf9fM`tF_z?9{*5RGol#jP?;S~CEWa6Z8UkoCe z+AOjUj*Kfa^h4~V8Ebu-?)3R)j489GUX_j0+^sVUPH8EXqujYB534aQpY1)jK1H z(w>EHXm#g#A*Eyi6JkO;frE%I=f3z;lRZYaXV*MetOyYkhU&k!V)d<*TQjeurz5*A z%rmmlv=X9Xfu}a_(5Zb;d0?F)ELI-){bK$CEM$zuJA#dEd!~SrT#J#(Reb$uR_d!N z1#`i@bK)d@7mDub1V_yY_;J#Pla#)Bq$UL?#tcHw_l5K~UVVQF z=|;w!)mR;>vI&mnU%as`=+Xr8n{SH|URPoh@0I%UFs49cYLJ{~Cz2S=Rlr2u87d`M zSL`&#wnSAPt@}L?`CpK-EvO@MSMfJallB|#>yq3}aH5(aLQDX9tQ{Ok4F`2Pd zEf?2hXsP7A`+E?QwStypKZ(x;7)W(hBaXfUbGF-*6=5=lOyf;$zuItK*f@S`=N-&3 zoqAqzvgf@Uf9W1eY(%_k1;wx|pp>8?AWGiXKHgYC|JQm^R8TB%Qa=bNG`4H#Aqb5t zt_YC6-%y-E@;(_?^~^^1XGLD2tn^mtPo@}`9jqQR$}lbYs}!`JzcMzb`N{s#imJsj zG4MN6cv#~Zvs^#AgC>1>$eL@f2h?~w=ELJAW+e){i5{A6c`1>e<0?PY%u+m+j}Jbn zbS=jlroaei1_@AdUklWTQfdt-EM~F&5I@N&s`a?8XoU}H7P-g(=7B?$xbHy1mGvW* z5~+FHGI_G{D{t>VqDIWp#9?=WSOl6>VOw{dpKJ(07Sq@J)zkC2k*7O`o-`_Ruyneu z@>I<1=xJf|>i9d3 ztRE%S_q}SQJQIE-B%S3_6(P`2*g6Kshi{s{+1{m+t(jkzZSXYi(czN3aJ}063>=Oz z!+LdiApJPx@C~sADI?xOZsAzh zEEjurTm_f5O>W=eZ!D|03zwif_!cXqV<6PGRxvxNu?6^AfuO^+KtbWsPyD@&?ss>P zW=iKo$dP$QN#5O!8ktY++3TOKjeXyUTo6^6hdUG1FxFgraIW{5?fTu61_UG=Y4&3+ z-}tdW_nT1?w}^c@;H<8H@t-W=sNukz+xAP+n684T1YS60elBh8m1-n!*E%})uz3pm zgutF|mK7Zf0ruHzeHG_+f3E^=WIi_E-a1{>hl+c0pw(*QlOEaV`AFB0$ha-Z53JIz zd&Ed{!iRAQtZkU*uBNbS&v@!l&+kM%73Y;gd+YBbiI}^mf{q2sQ%*Jk)&F2BK^eDylM&!=2eG)cx~Uj6 zme=hzAdhBtSK;xw36sNh5|I6nk(~l6PvbIaNuFZM-OVd|8&M&&~{} zeTJP1&3~xIVW(F{1&7l+Cx`o1UMp(zb{HU~U|EGt>_bZd|!#Db<_C`P!q z*Z+nbP}j(^8O5u099xu?SG)>qr~}9Dc}-?L{n~%KnU~Ha$fu22M zcOHK)^Fg+xR`nS)W**FSX)6|2b10yPz@ug!9{|vYkeTIFhum4FUxQp%9=C|(w1G!UhWg|U=YPB!*Dx~f0 zy~g>gSDgoWvKi0!I}h0k@970|1M<&VcS#rNP+Bh-;Ec``nPPa6ynbT|n3k!!X+Ks{ z=YmJ9!Nv(2uM&=;F~)8ih3x|XhzX{c0bOFTxHaOnLq(zFpMjsu#m`O zceNcFwDIq~hwM9o1}241tKpMjO^agKQH*Uq0tS(9g;NO?mM%{@o3E5o0|fOktin8N zt=Jc1W+!jV^JAPu(oiqb=hG+jY)(rL`Cu3EOT6ng^vw->3uh_#K~PYMts7bUzVKnG z8BYI&UxtKa-NQREaWLbXz|ihjJ$mMrwCvz_!9{)T)2SvLb|_6chZ&lqhy#C*z@0qIG(gSOthtPwM<%fC4|7&v1hl-id_)# zXjCHsviJQLi`@9L_cX_U-E=1*(JQL`M^yX^vw5C7j1C&<66cvtN~|1PhEhqhC)ZT( zod)BYii_=gCci{gXvgGVG@-Nwu-nVWlD}2lL z7UkaJO_u&EcRlMY1HgF8NohAQf}>%?A`xbsB7II=qYIGun2)d%ZvRflpyWxIP6792 zhUyoA;wOzG`<93eplnnJzP<`>PZ^mHo2IA`>;YQ7>_15$kXS?#e zVb;GnVoF+COO~EUnSOy-Wj-OXr_UqeuF{77k^9hKt7oYstXe-s|bDWv$rO|%z{C*nSM z7=4art6yyG&HL9{m8gGC2WDiW&*y61r~NO2{G0whO+-=QQvgb!)46T@bc9@sH(eGT zP5iez*%dNg?K{{pgBb~gyBwwPWz2Sc2aoX3sf6^ExL3u2U$C4DFDY^B_xnt31?TU; z?dE<>5A5yU!V-K~Ir9Dx3vPD-lI2a+_MF$#-wnmfoW%WOOWj$(wjPMf;I``CGSGo}0|7huUQ;QSN~P%Lw3El9?$$>zbj!!9|}e3_ULUv|rgxzGkJ zRI%3xpK6cmgqOPn9%IT%ItHRSXxzbXoT2vd>*9uvn`qB@DJ3c?vY$|(k-EC}48*D> zv>^QjJ81FPo=Jg=ZAoL-|C{@=O5Gz#3|^3u9P`2kUTAoZ@57*27+@IHha5#DC=f;( ze~{DXq+|DPxtfC6MdcvvOekOi)rTi#Swt*d`q;qv_}F?FI3?`5pz>b0>RH z@5KuqaSE;eA0V~JR9&aI;NmWUhP=Win#I(2A=yb{!^wvR@&P%Qb-`cF!WW_9YY**W z1X#8H60-_F@}Pd?n-?~P?+URl2xk^hiL572E%EDmQz@7p45j{FRyg>^9@>9nM7sg( zdPbJZ(;ZCkC*fpbSM7l#vXa_VLadEG2Wh22UI{ci+3Yk?uTn+2>0ji9s2PCkg?n1H)JB2mYiumW7X=^ z7kG4bThBrn@VT-U#QReVNmGb-^z0?}lU+W9OiYGxM$K0Cm4EDC6dMV%N?Pb!>NVg^4tSt6@0@lPI62IPL{dI>)?S2VQ<7^ts23>h zaVBgaVsEKT99`>0cydL&XP%|oH~JhW2C^oDB!0-VtlAbd5fSQbaVEXU<{-4YXcP!i z^C2?`u9_i+M)ejLuc1&i2ti171%mVoD3gPg-@}oU59tes$czgKGbIzJDz3w5%-5~{ zwV`)%NrCo|fb_8QPXVO2x;eLno)Qu7vm}0yn0#CQfJ*L@_Alt(W_1sKNlz5sYvMfU zyI&2m2Z;f$^$=2W^UfoMy@S}3qnb5J3x+1-^qCmAg-so$1Wgg`;c%Y$`)iNxKR$tx zHgK~s8H0L4rKOU>WIuUe48||n=wZd)sa$z4;i6(|-cX0j!R z7@lPaddvj@<_wF=+gmah-@_?l2QDDXbNDRPZ0q}{v^(x4`38p{rr}s>rZQn8Hug1g z;KP1fFM8XtARFDSa+S52l4#~h&?JI*mcJ!eyFm?k$nh&5{^8F$#DmY@qC`c7_tcf! ziR&8>U3Q+u5+mH$XrX|O(e5aZ0M=(xY{<_W!ygkQ*zJ)Sl@FbizlQ{=DeqKkE_LGc z+vEU(Ub*;CRA@*@2S3EIT;S|?}jH9P{${&|E`8qlS^zL0oXb zWvXVt4{E$P4N0~{bNKhAKnryUW5xXN$|%A1sVhNFE}OL{-koeqE8dsri}||ZIMZjo zg7~7O4Z1!XgZG`?cHBgO%ZX?TPsNbA%BhVDjHUASP6V;JnmvOd&<93(&qzyng1P3H z-#u*U)nx9pFOn~>)dQbYbgpC;656G6nIH=k!fizFlNVgS19oulT@z3hfds~96qy9S za)_w18Di{R(?_h5P9oScHa(jhue{woWG;p|tzX20m(PR-%>Dh5|8+fGB4Nk`__K8V zVC@k7f>x}v+ckBwtgCA_A9`sv$86Xxpwg~m{TVt62JUb8`=xI72s|yc3~1I-$baN_ zj+j(e1m&A+8deZ*1I8cHPJm+w60tg4nLJ{88vGn+(z@3?deaOnh0OE&;Nj@*b6i%9 zm~;5h8~#Tvc^f)%(rk_@DagJMqa@Y&HrqGhGS=sUEYt8&C;a+;%wJLS8=}kW%G-}k z#e%g5VQ<_riYZY;<0C|qVd%ul=57o$N6cF=svBb)c87;QHx?B7c~h80s9wyT4A5zr z_K+nF7`1?gt7uV&x8BAUm6rW&fQ})?- zHk6UeNPLfiOe4}=#t$?^ugZ_UOIfv3vDpnG3GCH?w+V6VYB!TJu zF*;Lbwz-WKnL3EjZO)jzK&n-k{ip)S83N;nUX)R3Vt&2;N(*EcBpT@{v_3xp9r`PO zc5hl%0CZu9_tEFy0X3C{6lsQrp-vGA_pT`rtm_=z*V~;tG5NOUuT^k8|B}u(!n3!Iq!$24_6B6K89&if2nFTzCz&{p!zg-z|KCdq}(O&VRS-_0|CpY+a9z zU_HLOV_oc`t5Hvke6*R+wva&HSLaolg$dBJ?^ngF{DE;t;31o5M#=Mms*qSJ;2`45 z#8=|>jyIT5|2=>Gs$ATiAcoL{nS_4T!jE0#$xjhFaP+KSw$KlNOzlGdsD zGDMew)9|rL>5Ti>TfpciIyb|sl6d+oubK=mUOQavBD-rdYgHP7+EmCVSqG(LsH+#w zQ!Zu|rOe*=nLB14X-2vI?7NQY=v|Gf^`1YoK@yc<9pe3M0rt?c!blAsiNLe}ebO`PjB@7~a|=tq+l zv4=$7$hwG1xEb(EO){6Cvk+}f0S@iUY~tyn+tXIL071CGF;`nWHuRIR2TuDteLdet z)YMb&J9tWbY-sBW{YUOg`ORwJrAkah08M#b*SNN^ zc~nKF2``-8Gqc7y(=U1{9=$#nUD4;y?QaVWCBbupy0YKmQo;9zuAz|upJZu2l%;~) zyJ245U=nf&qfj!v=DJ^k7j#l@`+Lveh=Doa2!n^I?ZLzP%tm z0=TC2M*A0sNBU<~BP>4rzBP4xj8Sh`Wy6y&GVlRv@^#IpaEX?sr*gGf(Rs z#+Qt(exEK$x@`$QBP#6#vqQUMP3 z(d*8Kx5xe|;xeTZmy$Nni8O$4QDO{QDujM6+>D&??WInfEpPvYa?wYx;`X)|DN(`( zGJ7EQ%M`U!6?9yZRGjk7ee||_6HCECGuj(K_C@J;o38=67gf;YkopBN-+xJ&o`X&P zi07R@f+$ks@DYa^mZr{vy@mxws6$XtU?2N6?GyZMsd9RFqTFv;3b>$GCx-iJI_1^_ z98aO+LmBUvvdOi?%R)jwWvBY9H(_{^nodR5-T$;is9#z7ul5M1Jre$3zxbcO-PC^_ zWS!p?dX}dIMu;NK|D+rDsx+TI4E(_zZs21Gc-=B+% z%h#xwhUrvTP=T3IK8UcF-s0;!7yIhY`H)C58mQF#`mplyemU?xjv8^vkiUG&o1|6W z^ZL)<|G$6nKY#15w3jUj8S~+w68|4Kl_anjr;m& z(s{Vz>;>R-ibmS1}0*fpEO{Cg0G(m)(B7&~^CvksNwN)X$rH#Hu($6aH9YCQ%$F?XK@GJHeKPiycQ;^Y?l~D56jN!TP9C*e@99}GLzyu5h6W&`SRt=s0a(6cUbA2CYqX>!ahIK`uqDg zy?4tU^LKp`-B&sN!=#1B!mCnqNpXuV8xbTV=DV`|%ddc!St zX;*r5xhfcLa=oGIUg6O6?ty9f_nrd@PPvb>L(7C859j+&qIsLvTL}M59_WQt2hPWtn&*XzsWB2RiINIll(4d+?{2rD2l5c&i9 zmmEPC#O&)~)LJGctoefv_hHzj;^Dz$jiZ0O>VKGUp-zFv|ifz=?2r*a# zH6$e^MSwUvIpL>~l9F;F9GxM3PQ^bsWMtA{&Ec72?iS_1L^R0C)X~zq1_@iNuCBXv z$E)W9%u~$0@=BRmSTs{pQ>iE^<+Zf5;Feq*9D%{gTcr^EhE}C7GBPsIbVbsou<;CF*5Ue=J2fN+$3!9Y_!OPapw}MNzEcZ$ugaZ+sS5OcJ zw<7KQ z1%FcMH;`)?`^f?qYaoclo9ob=G7JVs`e&^`-vcXXa}@vbrLukf1}-`I_SV+TyLahf zwj#kVexrJ2c(Imc&`g-JfI+BW!GvCgAMbU_oLR$zy@r+PX0wLZK?f2~JKw*50S1dE z2fK4Ntgf!I!{9f_Fq9zQ);L7hNIO5C>I}|pU0sH6G)zo*(vNC2b3M2?)G6n~$##*KmmSQ~bfr2+CdkQGC}m#+rXH3VhEbs%%&xunGaDZm zh_euS^)(ShU*+P9SOqrnb~fvhs>R>Gvw+Z#hNBDT>molt2U`X-0{aT%9F^H(qE)jq za&kEQ-RHYsD?NSsRN)%~C#*-Xd*@fsC|VZ~u_3XcAo}HvjSVV#`iu4a+}!6LJ$l5( z*4OE7Tv}S%au|@ed*XYHZ48A}w&S%gg2SV>-T$AW{q^fjmzG z^rXSRa!`+XbStpV?(*4XGvn$PiHULWjId2wFcpieY0_&q<*PTU5a~xDV1p{Bi!Zbt z92}ak-;|c7XJoWY`Q*!`*Q}|WOurl5f@5SAW!v$X>EvMY1m8Cr#ydpjR6ZJ}kFR4n zN=U=_h+7FQnw|#)m_j6Dq|r-)or1u!Cplzn%$NrvBoG*&CTH=dKFgWBrMWIY!r6oUs-4_ouZdNKUGA<#Lfu`Q9$w7 z!%BMSBCUppa#SiPL=PW4cb2|&Xvu+JJU%vd^WMFPFNp%@ z`S^%otO0!YMc)xK*AR$LXnZ^sA`A6rpfpYI-@hU*aRnQzvKxkHYC#LH{(kL3y;4m3 zhHYx(4WzZh0p{Z7{s675W75*>Z_dL&tV=$}o`|fwd+p81oExt}!-|D^h=@iyM{Y}m zY{SZs&1GCg#Yxl&}6%X-*q2V8UdxKQ7 zThPW9w{kV6XWGM}mDwF0KZdgby@{X0-u4d+Y(P_wor2Mkk*S|QpTmoRZ9ESo<=L}m zU&4RNTiqlb@2<6dqsP)++O7AlwWS49#D1c!Erhb~$B)p$LS8PaQ1AOnb(4=VqlrIv zcH+So^%_0J04{@c1i}C_xqO`&n3Q-~`_?T-=zk5d<5^CbzXZIzye8)6mlw)a3JVL# zV5}*$<~4y~*U$*>Qcb5<)>y5xnww?%t5~hImegkt3r>Cjw$o%8Ttr!2{drLlUr&kzr{rbWp62i0>9Vu4Klbz}0u1YW zz&9=sCAC8f^8`zvg)$mG2YGjc2|JEEzxDO>^`}7rFjd|MqmwOz zgJ2C?2nC=3u3}?j zi6&SIN~Png=)agV2p&2!2s#voIgeXfTH=Bpj*pK=m4`t6`f2e(vgA_=a93{@404!D zz^Q{r1tE}CR#tAR#iM7~>Kz5s)Y=3CwfiP)v0TefPmy5N_wV1+Asqt*I8f)}73I~% z2$v?N2?4PZmWb6=J2-{2Ftpf>vr4Z67xoJm(xGz;;7LW9 za{Ec?I8xf}-CZScBY64vu0O#o5`aeJo}1kwtZdVrYM_vCi|&LqH_O)4)HJuZ<1`(N zuids;3Y-@g}w;t&J@g-X*Sy3|_*;CKpJLgH>$R#%@F6eOdjrmpe)E5z(`=vYx% zNheihCTQM4^lfl(8tpOE{aWPGlR5Q~@83Us`9iok*NuhLAO86Qq(n1}eP@f8@Z1wj zPDu&CsDes)I9n|TW@~FXw2x%WaYLOi?Yh3%Jt;d5rABHTXK@Yh-P>5mj4~U1d)vXu zsg1$D-Vds>t4@Wvzn?-{TKZ#GmjVnNn(*4a2c#ZA4Gz&(2$wC_M+VU%1}`M z9dj=mhSgePLFVO2pA#v>v3L|l+4@4MzQuzF=cT2Y!3)7BAou`FL{3XJi$){l;@)5s} zwA(uCRr`tXtSojAR8&75?5@#F_yAbifCjX<^bwZoH<(eZ-`3uqo|pI0;*Y%`KmR%D zZFU5~{jH*KO@|+7nih$*dMp{4Ci3Gz*IwAh$L|@zHL7u5rqSujsj^wztmZP49>1emgir@9vW_OVHf=GIm$a1Ei!n8NMbXHgUR`!^9B zkl`(Qg=g_88mX;iFM-+N+-^w9cOyq;+V&v4+ zfzYeMK!*S^7abyYER0&y6X_Tlq6YYaiTs?K!)*37mojlb=A0W5pj z)p)(8G;;>>bF8cgn2^@hC%YM^NZ}z=i~&dHufXOP7Dm{UEasn=$A$KV0}}w*L*A$@ z)6vmEc{cD&^{Q;>>?Z21E2NLIJ7L@KiHQS>iv^OCli&1Nt$sS|tC~##R9;DSb?2kv zhCo1=AG*4TLPA3Jj@Epl&~!mSuK4))WS=Xls?4ByEpFU1Xahn59>U&Cn80(}#>PSs8z9Y|O;oo>W3Y!t{^N`qVYh(^EiIUDMNhdCdzv zcj)RtwjTnJLVYiqZ0tJLo8(Z0~x<$ZS$RlkW*w1Y1e?7Vu4_*n=5 z_yWFx5$t?`Fiija{4`SjfOG;ZiuZlIFz0M-f)tIQ3u+07Z{Cj2o2rfN?EsD3)?1oV z0Vt}2ZhneiPS&aW;APLTvB|kbOlfB=H_x3ySu-;>c<$$; zfAQ#-F$OkeE_F7R>ap1Hy8;^u-EsY03~LdCM)kDI$F)>nQfUhjf^lKkgcoNA}p! zADYV;QFe(bz17v##pB`O85tSDAH8c}&^Iv=3G4zbBz+2shy(=$;Mk4V-d}(3hc>S% zDEKm^B)^IS_7_THb6*OYJ}Zd`{yH)e3}4wxJ+^i60U6E6&(EJcOAcczOG--@6Au{# z;1U5q%y=h;oH*2h?4!*_I5|0$*<&TdnR6mnj)n#Y1E7_LdaC4ill5#L!US&d!cU<8dRU%#ib_)hzZDt0=}tT882+f@av}v9;nB zTJ!Kr9kk}ZnP>j%AEEHs)Y_W6c2wrye+-Y{)j&v|QTm?#*V8|2cxQi8|MS(;pJ!<~ z5xW2WnV}QC&wu?qBHBS93wQa?_d70WAVUBB^VLX4%xLr}{`vP($MFAotMCZ(WW?e> rU!^s-VVuQg{jW>Wn*aZK>yZX$dKTQ4|;vP+D3kX_W5qYhVyENVk-9hl2y7C`cpS zAvvUU4s-Xx@2}qb-gVde=UwZvSc1$s`|SPfCqB>TdG>j8PhFXb{wzHNK}=|sTbd9= z4SuCaKSBe3e8Ap7LJ&8EzI8*}Gk#&%$MeHR0-4+^C)Qwb^Crdl^C%=sIELe-K$j!K zZ%x7CuJrtW-1B-428`bsq~(#a9rE%W9Et|ZMoHO=1#S+#-R?QY{(9ZI*SnJP+TZq= zfBg`z!i`dqn(~c}Y1;~ImSdp{M2fkz4Xyba>E$7g?zDs*k2}}w8 zGHRG`Mg090`W*z;W{i9LP|9|2Cm&D_%e)@gT^~iKn ze31s(`QW7G_D_|+oBAH7Qt@{s(Er=^8p%;1*%-6S%5;W}jp+kx=`E~toI8*^CB^YV zOn=8vInqmmsd8V`G&aur=j+(}P{YV{o6bAp9!o|KBB$=T{k;nHsPZX;v$1i4`_g{S zKVOr5O_J(DxwuI2SC!P%eAZhBotoGt%p(51c>l8ylv_}srKVPfY?$A${q_6yZd}by zVa7>~GplYbw6S!Eze^eO&Zmcbe7v=_v`+Yq&aBxCRX!;eP@PHi7%4n&rmCv?vFTs+ zJzl4S7#s21d*!;mv9Uqi=r!pjpvY)N0^TR?xbdaG4}LEKhLe?*)!5eeQQo6FZ?^K} z{Frm-zqT;+O$T8nA|GJ zbfo#u?z*|U&JKHwN^eG^bZ^@Jdr|yYHst*K`x{&CH02OwT(OLJ2*JZ&CT7h@B)uD|D6pL zR#xgiBNPb{P~6Uqf7&Ki9`CLb=?YFj5VGdkGlUm@B)s~EmYrSBIsfg4a^(HBC&t#| z*PKRj1G)V51DQF`u9%N{PB2Z)%-D6Lp?8PS{AiKFwI8yO zy}f;x0(nhg{zTID3;E5W&>wDjVcxSTf$Pm;ZL^eI+ZcL!rSkCKEA?D@K0a0X_)P;r zlP^;0tRby2?%wKPZb^GXGurp>U(Im4nbNMW3LYY5o<9eDNP99-M|{rp1?693itXwu zRW880Kbjf+M$Mq3uRW<916=mY{q;KmqjubY{mtCqUx=ETn!>Hrv2WBX99-Sq z8Qq_j-2D@%ko)D!^@ymbd4AKKx45dRi)b)tZ?R=lsmqKyxL; zm7*n4&QNm5`8m)l^-nw1drO@Yp$g~lWaBD#VvnIyH+bCoJJbD}Knn0D`DP-Ymi?A} z;PJ1Y3q2)hkj6#ct{1Jl%)x92o{QU^%W+%AZ`qD~@iM6P{ao%ioL%6zXcsHzQ!Z3F zMu7*P*UE=FeOY;>n`e;z`}ZSb=daYet5G$%k&%(dNCLo8i3I_qoXv%P9`tT^fq(i` zQ`EfA=w4zzZjKL@#w&Km_I%?iBw9{R4!2l7sBL1BfBW4jA~7@R+*3RbCu8i^t-~iI z)bkHDgDY4&KX7KBKR@8t%{>%Q9lO(~)KtYB-MkK1UITUo39B>w9L)16f#*8jvhr&i z8Rfz%DO|n%`}GnqrOLZx)X&P5ygYp8m$fDLlAX^pEm|51=84|BXV&6IxKC`Gd%;tgNd})jL+}1D`*C&dAMeRuL_+ zY7tdaQ~Nc4ij_5{Y6&<0VqmFy&k~!y zt%QA0<(_x7E;?N`>MVLXSaL+9j&1Gx2@N*%^mLrlwK}#E?~S?HjaCKn{^NuiAp2&; zqjDB+-@Xk(y7XH{#Ym3&b`E(?1lJV{xGa|S#sVn~sE8(Qf9D07qpYwScIC>ItJvmj zot)-n|1H^)M?c=q8ep3P4rT(Bmeo4mw<)Z*D_&w_RGL4d;O|>{YHxmVt4(HJ-~@AE zU@jLUP0M6McremsXDUj$#wls1**fTIU9NFekBVr(b}d)4>A`B$?A9`A#A}9=r$N9N z2Me!;yjP=acbT96oqtWupO?aH=0Q8(PFJ(!d9}t%|00{`)%a?(5##sv0l>ve3%8;u%*Y4zP7f7fq}u@fPjGYqVDeQ>5X2~P#T@u zBE<|&S@&pXDSNQHLWisEJ?4;riRAyCVGBb-`YyLx213}p#ZdCtA>of zT6c@GTEea;8rTl-NvRJ?8kqC#nrb|Ln30k3*7G}iv9w3eiPuGI9r*UAOaQZN z4%d*kN%Z_px7K8O(6KOFdc4X{Qz6|9b3!1hC^gBaEo22)z9oM5)Ju!vuk z5Am1?mdwu0y$jp)a_7!Wl5kUTsYSzabVs6`&nsW2h2^*w`EX%LNj*+ZPTu$;$%o&l zIHjCX6{G%D$REDgHS9&z$*nydBO?JD=lSIkyickG9GS4L-8y==X_Ncf>iG-4d|X{! zJ>M(qJcem(YB~%%Ahcm!dyPG^l9N&6&f6apOkoS#0OHkbliMRy{LM0unXI?~`?sh~W(eUq6E1udXrR}z}G7~lZ z{O}`wTgLd+M*iJ8$?9YHrIqKmTf4r$IiaJec>yisz5Xffs~G(<5QdJ9jtu}W*po~Y zK0ZFF*0pMh*B?krOM^cg3zRL;ID1`e$JN`%N5XAR+hL#-ts%dY=7=L+DyyiN&oL$0 zt_OV;yXwF1iB?p^zj{T3)&%O9_LZfD*`)0S=e+@c;HhUv4;Dwo0}gyiQ^Emnc3w=4 z)6`DR%&4B-HyP9lXJ=&=#>U3pK=C)av^0pVvdh{JAEq44*Bq$U>`w`sd$$2Z;l=_{09;fDKiHPo zk*+vmYio;Sa@R$708#NyO;0OjZh}~*i!B8}fCQ|kCsxAFb^PV&IsDd$?4q}O^g0;% zac72_qP+EkNco)xjXT)<&(mi~O*Q-P>69_)qEiEm$sYpZDAFO(xva|1;Ht|dW z-rgeU%zfJAmgOzw`I?2|ruB`20AoE#AK+HPa6d!WQRv>M1vRc52DSJ;1rA)i((DsFR)j*Og>TWgi?U0SMIz6N}-H_aFbNFPm| zk~Yc#eC_P*wQdF4&C?L?G2+GB)zjY2Li%#y`7MBo?Ssy6xIdXm@LJ zDyP2XED{NXtpl?lX?L1rzr^ILbsRFTm%Uq)N$zm;1YY#KH8e0nuJ)7vz1IeDu(64p zHg>(gzEjxN+|<${0ld%KM8tL@e?c@r&`Fz&gm*%?pKLA*jD+O?^4_nR;p%)6N#1`u z5Fg@KJ<+M5-~e{-<7kz%%QSpPTy}Q$Y`ux!h7Q0XT=+V%w(Bm9Nd*AdF@4rUPrv2Q z7x`-D+&2TfW@S>h0?XS)#ATteA_3A|u-|UrTIS_i<#51c5F6uv$5&3sHiwXLTLJs+ zDT)I%0X2<{jpB|&l_$=b2axtgj|SGtJe_>)E1v|sjJm!8z=CawazJvLw{QP=A0lfW z1mLTVo?hlZ|4^tU$ZWj8Z7z=%?C<+hQ#?CgR#Ou)ZacrUq^qYFwYs(z5f*mdJP1tw zBqNQEfq{%Y-uh|VGP!zjxJs`xQ$x?+{{YS9lF?hL(A(8@a=6hidryAYZG>Ewx$FP( z=K1d4UO1Wgux_PepNPB7+5OlO`G)Yu#O4X+1rA-)%7pmjV4l}_smGWQmBrC|3;qNE^M>I+l zU0^K`jm-E2a3CPRX4d{UUFF@07lMjhM*X%xs%je5JFlam5$eC&nFM>ESiAhNwizvR z^HfLthOTh{X+u2JP0L$3Sy^c8N&1&?I!WxzRK-RLJotD~HIngjU0pD{GO1Qv7dY&Yyv6n1 zA=#}&$9b3KAs<{`8{TD^SX$os-~U_gj~m^4TtwY>ad&SI=vOutdpytRvu4}%wV+eN zCb`rMr)Dc^NO?#b;B{kVJseB_W51V_X_HkgtgPme4v0yv01;<_v;u(Q605evJOCSp z1IQ$hcj$s$g8^5jD$EK3wV^l$;eIA~jAR))e!l(H?A{4p^$$pD3Iu+=LlyRYZ{NL( zh>0OO|I37^ASqB$03q+wag_-0doOYQl~D&n;Madi2_Yp2xIp-;k`2KP{{%qj5fujh z3H0IJF*xeLU;k$ps_=7uML5jcdf*u0n^o9AIuAkbRgPZM&MB#AB8tL)<$gE|*uk{m z184oMUOgMVfY8o#`0uE1?Z7Xqu!mmj&PlJ)2tEUti^EyaDbjL7+=6WA(fnZ{^gy@9 z;oQ0XU_0yhmO-lOZkO}i9IbvPzwxq`S?|ym-rN8esW7nHc`$m_yh0P;2Xs&&xFN!Z ziM%VSpn_93q&M^E^l25WZIuiMAaKCf@#g@Ik9lu(<9X3I8>MvlXazL3v?TnH!bEv< zgK1X|Y=)$Ims=)@%ANDuS&+_Vi{gJHNPzF|phbY;yBA8x`TSS39%h(ENV`KSVCq1i z>nLbe6woi6ocrmgAf%0{Q>cG+!Trdzf_T3WWY(i0Pqd~4xE)+7KJJ2pzPGmdZ;Hv0 z);k*;8twz0;+)syF-vRfoKK%rqD;KAkTw8?!U;G>{z>S#x%ndV%iv!MCzH|}RsKlw zh^U)1X}Xw(?tXi?Be!qJ28M+^cp%p>kfj*6DUiPaYb=Q1*PHus+O%tbXRYkf4|=)H zKJ(QbPaGmGl!ubE+mL90?%okuMJbOTx9=9=N{RBrFS$rJ)tmUbia{kUpyP-%I&E-Nc597e!c za$7^G<>lA>wyH)WdgsBn&gGNXwwX=$^05>E$DZ?)5$gy#>Qy+vL+4BGN zHe79GwRN-g8&0a&rPz{tmYr7dehQNiKHuWt!C7{op)RxJ$ zKSm3Of+rjA{DKF12UD{iAdKb8T>A$?5tA?xYWO~nG} zj?Klv?xCUUljhKvF0eEetfO!ee;fKBNm8O#xTZc;k>odbg!yuBRM^ksu;bxQ(VW1@ z#$T-&Ex4kU!AQ3&&?GYsZdQsj+#(8dDodQ_(Mpn?@3P+031SyUXU|z2ho2AYF0j11 zx`8bux}rHg57X<@Sda%S51qYq0el2}mm?Jhg7mi6ekpqIv?Hj$nB*l%&zG8-o}q*6 zqz*L$(*!#;qlf0hW?Q2QYQnuQf|@)1@p$UWjK2nsse&)iG*R@3IB*<+A zozn`dJ!J--0Rb&#f!?n!%|N_!6EN!D6;B(b&8 z%X+`^Q2@$6R2ewNvh|I+oa^vN&KwBf(b9)aP1AuF4A2wTi3oEcwRXi1T}Lxgz@j=_ z5avVoF^+ahrfzK7U}M|DLd`_~v-2}xl3)Ze3KWED+`VreEb|Yg!3W9vr_}|{u1xS> zn1MQAcnc9);Bw5BE02Q?%9iBN9ld<5`(T8>o?V5O3Ikz2tUuPm8|?0BOAG1cylXq# zEzw5-J8o`H*tKeggq0S#2c~^3A?MyVsb7MI`q77;mw-PUd8W6;M|-$C<=fpF;PBWt z+~`F~jT@GqggJUfMwZxg4+BgC>{8!9UCQehLhqVdw?(v_bs&!>OG|rU(`;K2hkK`a zIG5GN@l*H_cm16+F}Rdc-{#(u3PkU5Fb@%1AiGRekOfg7V5!;Xa>nmh<(J<4Tpe`P zw2c`$Lq!1*W=VRTLjQG@rM5@eyriH{1aW2do#o8;$=C?#Ff<{-6&}zabRkQthECR) z1{y;iu4Q5{tb{D@($mk^yk0*U>rQLNWQajPY-OCB4!h=hah;YBr2Y@Ql;fKlo3k@M z?2|5g?E;d#YtNIa*FM4F?B9!F(NhQiiqOiiQ`)SIHkRy-N@+L{ zco}rxTtlxpp8-PQ9?vd)K$2k9!k&WHN8~&++Z*cMtAbt1jXu3LKRAf-aPOc#0ufZY zZ&MIn9gNHdP(oM+L~_%M+J|ng@tz?77e{@2$UH^;KK9(>KG?}}C1~e*ecriRkaOBN^twnMH!)WpK{BB=Kv#7)QA zO|I>9_4R!+bi(Vvgi~ia1>wy6(ZG%8yES*PjM9_iq0Bhz=BI^c zA%gP}$ar#d(5V-Y8U;IRGj>yr5#B35PQ%z8=2-t5e9QxBYkz;gdK3d>JvlyHCUd>L z08AQ}=6nYXZgx1hyD#n7GV`?@VUEbrS}QR~2EN&4rnNhSQ+EAVsJ}Mw za`8gd+m8Ud0x01#=3PTWxy8kznN8Ffu~pJ8A9_W4uT3{btdhDik{QLFVIu3T;%2-! z;5Umgm$0DT`;p|0*)9&mOnT$zj=Wq4%4mE&t8Y9Tg6iSO2{PLomX?-)WjV|{=jLny zZP4`J(CNM%2j=8TlV5Q5@{;xg_mCcS!Qep|R(k}^@84LxfXt9FOjl)X(OE~q*!-h5 z<>vF0M8diKC$~+){M|ik5*_-Lfo-Ad=D3cW$$Y0`b*8%ONRzFdozDIHDWHk~u->ps zn;iIqmcM^BsDTxLMELSadYf-A=wJ>65b2 zYKKeSUg^_8azVyHb*b-2=Zdw^nI3mz=6Wutt|PSAX~XV!V9!=Eh|g+oC(g z$32#$+Wy7CsyJz8Ii8aLs^w5UD(@D3_;6^w8`)6`@H1$?*WH=*b7j21BP4UwSh2E z*95pTVf;E4R5?}wLsze)u6)^|GSc~vPPjzF-nD|@6^ zQOi5x`{6W!0e6$+6aE}UHEvsNm;$a&IIsOh(D(hCcQ?3E_WcFvF$Wl)`8@&Ea1w$# z_&xxjJQ5HwS^hW_NnMA=3JJ#Yq2amRd^ItR;C=wd80;MoZT-8yD9bl>LFNb6;l{U?|f3*)!=`&#l7vFxVUSK9*bM0H2TJ$_Fe;VbI8MCl~FcXI( zHP+!c>Dg25GFrO2a=*JV_j6)`<&5VT0#B3+jp=TlGULs(jZ%`qp7l8bJ@d>8jYsH5 z`q>X`HL9E*o9-cnbWuT4$6yC+Q@XiD43l#!M*5XKa=h8QX`ox`lM*R}GP~=p{Lgv` zMSo*y{NBWL&`&$u`QqEhQ1%eSbHQ^RMFohktp?vA2?S21eJQ95mZ-b0OHcN|XCEGp@G5U^Eep` zjyx*o%y2j8PDeejM!2{qv)Ru-^i)tL{A7+T{Z669#l`zml}3KPPv4lk6;iXk4z`e{ z6dBvdCPbwqU$rI9yOo|I1gm!=^sy0nE>BmxIO*p?l?_f9pM}Q!;0PTeimleFGRCfV zzx68S$hA~5i~kat?uVBH8_bzYrDA;E{9|}&!V;J{maztzl@jOzJ=2%fp-n~_8+;cx zgH%od5kksxF3IR_W6s!E)^>?bD;a}#y~Wz+=mfH5208MmRdkr-jOov$7B6sk3G(ug zZv27@4fgh$;Y&A0i`U*Z5nicP#3mvhQ{n&F*O{I4Pk~#-V_-cT>w~Ap% zsqIar40%(7aaU<DAICg4Z!4Pc~i7baGi>#8!)<@gVhKx@9`Vmf3fgqP# zI^CS3fEYsofEbH*>SjzNd2|Nd0SGuS=ZrQyz}8zlmBs0 zk9ti0nf``j$2r=TjCT2-qcF#cD#($cO%alG8Qz0dwP3A*Jn}#yOccSAgM#Rc#oXc4bjJM1nDSqD16sWzR;$bSVXMGQM`>mZ1k`|XQ9d)Gi4{;zs zV@D`ZfMj_Q{A+Xb8oERI(kU6EmWBvr8OQ@<3_(9ze}0bPp`$)No{K6^Gq;XJu*Kni zx|2KAAo-EE->zWOGGpT`ervKqW7>zCSAtJfqr8-RCguTfR`EhJX$yh`Mg&4B1ILf9 zfRtaI6nF1L9Qpnap4t9_z>ZR5*T~ik#g8 z1ss{>FKg?d%$WVZ@*MGeCs46IIiy0Qjic_W+JT%&kpeeJF#)Us$^taP1Bc`$0{&-U za8R(O{={OX>Z#3BW5#m8trnSibVddj4sI>KQ(eEOSaz*qsq^>#Kb{|GASpV)lts(? z;jTFhAklD(1*j=*0M7j$=uLua`Rje=p$#(=M4GYOiop;SY%6*dx`&;foF?X#^JEik z^&VQSI_lA2U*bs>7aDM6wAf$>%XZRU1=TctT$_*B4c<45V zDubXEZpKjfPCxQuQ?V=PYOwMa>J-g64{`L*`|YGPtomH{-uQW=jGFfM zec(VriesDs3Q~p@j@@uU4h@qb|0}V=QN=(wQ*gU=AN`V<@K)vZ{aS98v7oM7t>he) z9!R8Er|21)K!{#{r3hfUaDip8XH`IXZ)W<5cNRzo$A@P{AS4!)04^}eyks=BwM_FE zdJ+iO{OX++r#o1{lY(v_@Xbp95+oL-)@|n^K74rmFPlr(t6yTJ_OXfPJxwglZfWkc zn%b)6W5cNnnN8ApHyGv0ynQ6mETS-cNhdk8Bs7>P_X@%G4|U3-!aC_ z^EI7cl(D6czm0i$H~24eNsF2c80L`Z>q~oFeMZ799Wr}00)3oDO!?8fac3;~;yeYQ zyOBPoV`ttoFY5Uak_x@IVr3ZeA2wLM4i6T*(EgW_MT*&9c*FRRn(BJF*Ppy zPj<-E)Kp=v{y2nF0Y&NToE*2M&ofF2?zK#kv!Fo;?p*<*UVqNDPxwlYNE6%Juc7uj z!I3}N+&9RU1L9pil?oFukx4m9XJ-ayb4f$Yvm&-dDzib5oWrK7Q zH;{K}YHO!CO9ANlkt!n@-c|=eVtU0e5GCk?AYI zP5etJWvWtOb(Nh_f+RdpPrgmw89PfQ3j-U)qlzd@k<>MWrz-+P*x2i8WGC}rV4@(# zVyMu0luOPAwbZ_mmNtzauDl3bCm^pLp^^f{#ODQJ(x1JU^`1Vp zR?dj%5u;{f3omj9c%<>$R~M-yOOQ(<89|`sUJ7Aex6{z50JvW#gnijn+PZm!#)z|j z;E0(g?A@2=WdSMylLtvOu;F+iT9pSyv(T5cLVbX%xI#;_Wlsj_0Q=Q~qs{l>IES#M znw6|h5-%qMWCxI?RpKMb*qT=UyT<-SbN0+!vZim8(lyPNlCy##E2=naAe*-yuR4g%$u|sv#71h{vc72f}x{-+E{+&daNDMt6=hAYINB4FK1; zwsI>uEW&xx?F#Y$UtRxmR38l^W9FsX zmmpP5uWN(5&x&5V(T+8K%vv>w^beTD;1s_`8=Zhet{**!?>ch<7 z)-mmc#gz!@2028Z)lmwYF(VDLTQ}=IWmOnrwa?WOMo0e6*OUt0sbX2Etu1cW`C_WM z6QaBgpkHl17%&H6{Fu1`JP9kTXw_(HjO;Cb+weD3%lv{k29eHZLLdmYeNU zv0CkRcMay%h&nSszh42E$Re3lP13^xoOP$Tm{s(W_vSIojXiIluG7ppm&H^+S2KcL zE^n=!F_R^$TX1OC09F3P507*NmD?-XY)2|v1!y3S}xG*U=WP{XgYwq zb(Fp}4mg^$5mdUAO~fqwivsX7fU1x@LEn9vdENtToxdLh!H-vu-Sc)$F3arl-ar4nLTqd+F+#JPN|hhx?>V7!@F5c3jm-(YkS4%;f#k{l@`@`$0iaq17@!3gEf*w{wu@xq8RZ z+dI^Nm(0np(kkQljDW7fRMG2q6-Hf%*)oBapu2K_nLW+T%|!FtZ@duf>C{$Q57!lI zDjL%^z$Ujg=S!&r43~cUE+q}d6P>4!hk!Ke1|62zphr1?!xMSocVNoL1QkUnfeCqo z+%BH3iqhHY3nk$uyl~I+(A^V%E6wUr2yQjkz<;ur=I*_k`t@b#dS=rby1gL4Mw{K^ zQ-{hqWhvh?qsHzcvnidsCvA$6q$RVG-R&v{==(`Tvg*PJ-Z0A4-)r3g@D{ka>>SrW zV>j~ji*DNW=68d-Zw^dIf?5POQy(R2I1Oopfb5Fr z_#;p*jE}kH22JmXmP*y2z}kVaql|hfFs>7D4XtjS*jZ-Nc?qovYTmj9CV5;3e*$+` zLIZODw?yPh%m_8q@q`j(1aNbnsI>@`n?cF){=VJv@cAY6_)nk=){>-P8flpYTCD5XBqSwejrpLc zcg)a}uT*C2AljyAdNDZrD`fz#1{Aaa?dt&&9wOi^N=~$|R29E=0a|-5_u$$4C%D%Z zk>jd)L8So20nnrc7&B2((NgzC1DFQYGx17C+N@4Cz_lf-=IE6FtYn#kO4b`8c1)A+ zqh8bXs6=(StLBOF?-RVo3C=e~WioB!fWMlHXZT=~-m^^09T^fJr`@*m{>vSF-T_xZ zfSwNC*c-P1vV^lUuF|NE0fqL0ixN(kN9Q})OHS4M$?@2XxAt}&qqnj9L8~1fc4kWg z!5#4-A|iP1i9NzS2JQmK9qi7-{TOJt-#0ck7Ku)|rvmzJV*%(!+B9GkjY{m__|N*BOQsKBMpX{|GFB}LePTg0050V^| z+(BeL8jg9U)z2CenecjzY;O;YvCpMds03E^=9qR7O4q9fE-i64imJqsV4H=iuiT#iOSD%buC zu+3MRzJG>7{SUhE=_?w*ck@D3Hqh%lQPCscm;5O@A~>Oef;ZZ(xs-*S<99HVkw57O z!F<|uxLVXl>X}O)1;%A``Ar6!yLpX($c?ygn>~5AbdvmcXsBbQj!|mes-MN#Kb+2V-sab(8irK{j(ns)-v#T$#f3=@R9fbM%GPLqzx0&=kpQnvGg_V*Z;INI zDsl#G-NpS4N3_91Cm85Kb?{jy7O`IlZlQR{-ke>e!&of}xYjvlsOT?vAvn)*4jkB3 zl{&vg!k$(CFr)+xBPvDO{3ucO5fcj`=P6KWynEI)qP%Wkn{U%T6QN^c>k$eB+9EIi z`Bul6H0HcHM-~Daf(Zr|F{g@5McOg5UM(g=ny`!^5$L-TkhiV62XxA6w+zI8x4?gP zC&F}Oc>-s$)oE&fjMyPfh; zG`5vJY1#SWilq}G|Iv{n9olZz77T^#G7#nAj=9487!snwME`w;j6FFb?LSMOoD0h? zz`3P>js$oU0DHBr;u#6rnz!2f6rm%G_00R$RO@Q2x5zj%<`H*qT?&EUK zWiGJ1orxe%6HMyoAdL25U;3G4$5@e$A6sTFfT* z`xe1H3yyqhNRt(y4zuR+ii#`tL9GpE3K*M6H3kcRo`;-Ii?fxa!94qkG^2S2DFV)w zHvTB2T3jq4ED3`Ly!T{s=M=!WPsyVlpd~p{OEJ3L;S7}h!J$&}2RT||VN}G;DmbtvmtQ2db&OY*CT4s%F??R(7A{c@s! z;Q>A}U6uZwB~s4CtN>nO6^ zr`@gToceC$dR(Ny5(T8w&zuxgGbk(&Edbaxy-*HqLucS_A#TgY>qZ z{`Oav+4Ty+04`K+Gul@^(Ge=z@SfaH__0HD11Rc|Q9kqfMs`MyVkZyhEew1AaKKEZ zJw;*55;Vz(GHcKOX?OmNECl#Yp+xc9H8%Y@z<6A{yEFk^fcby|-EyuT9`l4}lrX9S zot+z?n)s=-R2wwb4t(({+4rxJUr#wooQg7a$7w@CtC4b$g)fc<^ZcQT#2{w;sf17> zB=8Gygrh)`LsK=N;7epgDY>$&_%+C6L^O5DsMmCJ|D#oC0Gve-s@Z=!?Aob@10DC` zmW?OzJJWGEVn*CLIO#H5HA>0{E4O$M`F|_8YW{kKm+;0dCRaNRkJN4Hd3e>Q$UfOy78U5istjU2@8^dS8@H04!#iUYA71 zcz#-%?`gz&xaz(#71oOfOhv8jx!GQ2P{F`?Ji{?b>QFL1X&T^LCO?ZjW; z>ad32oEBVl-w}))urDSB@KHCvX|beVpuZn9&%@1+j{*Kt%r5~4+wPM^f^`6_ zG`Ew*%Wt*fGv{{8sz$?+e!0Gpjde2sOclNE-aSp_tJ&mOwtd~&SFU2nCmZgT%<^9r zRNlOU6}lXgUu?s}P^C3BV!huo+C>Eh0KD1qM<>7ZIswD3Qn;-NhFu3dDW z1BO!q?ck)<2F$2|F0{(;aSnm3*Ob=kWiumfjxHOhA;wZ81N?brLM?y-efN21rhh7YByOARZ4;iMy#vfc9fxlka_S5$ugNvS=WTl`5ic1R9> zf3O*;QjVz2v&G3E(Ge3vgMiqYhb$NXmdSR7cID2I9BvfdtZN7TDAoX+K_YxkFcp^_ zWdEjA@i@U@!MW-J@8_y#b$kPeJ~dkI4nt!-Th~K80n%X5=Ot@I2O$+;XZsqoECLZ; zM9+W`-asr%mSp=XFX*+a5iPehbRn2?c3#IxDor3%@8uznBduPf6EY{StolBmmow7 zVb3B!kpBL0Fkr322ZS1(TQ|dOWuS4*?l5Or9g1f~mxEmqJta(RHgh2F%o6LG162eh zx@P(MobyHK{SX`hc{UIIj<*vzBIMX@-X7e3;{uIe+XyrBG0-jFKet6LdZJV=M;FZu zP^h*)7taj{`r8Yz02#^K6oc(GnYRVCaNug7_oYK*UTFx`jPI70f?hBaZj^is$ABvC zk|sOgBS`>45Ep)SbpnKaL9Ktdz&cbl0TLOU@4QX@gT7&wypZjV}CW-*NY*>TeA4J)$#tOS_-_DOlG) zWB=sIli#)ZHIoPU1oh;6@w zo1t4f#n=N~*)MwgZu6q(4!OOp%Q2R3jzV8Jj)MibtlsrM7`@$TEzR-9r?gu?4ugii znaU_NnNt@2FresAq~>nCS0^s_04T3s%Y$Tzu(SC0cV-Rc64V1Jq5NAEEReH$`OzY1 zGClV;H)m=oRBM`><}PgyMMfiDuUcKKp3!;9%1p%>GCkzyZBl|H@*B%$0cJPq#^J5G z^r%yqyZQzWEl?T-%fX|TGJz^sH#DBCyM{Eyk@RO=!dt-yzu-v-&7;`=A}wxqwcib1@F z`zG=e#Vs%5rYFTc?7`+21uW=Vu*-a8Pep^#dJlkJe3_?}(cbUWw={~DC9LcAR7|Ob z$cMrXggkSmROG7WwFqR)zXcRe2RU0i>Fd)n&D6cS2gAFeu_ z)=PL=xpEN&2eLJeI10-TEK=;c@6x(O={yt45S})r*R#n2f;*G6teDG^{+~c8URcl& z9Ko&ftCA3xpLQ09#?B#uxmdTvl9l(Ci38D;N0P1xKv`D`Y)7s5p!*s7_Yjw-vLUkl z(oLpd@Zh~q6jTrc9dH&8O0GdA(4^}LRtypvKiXsWd1!#dAV{D)j=3!Cp9P*J8#$8$ zRe>$|_XZ!UXlicjM;9U4$B0)Nu3%l=mID+Vnbd4kJTWb^w`9_jAAY)$M7331<|ejY zzDx5%bYjrLrXx(K#(FigFH z+0@47=;^)2RX1#!)6!_p3F3#^Ul3+Z1;D}9Mi-vvXT?ekOpG(>pI3xN*#_OxVLdXd z!k2YyAb9Lamc1_iP31qY#^1RUT<)8)-v24$DxRQyA^eF92jP_N-tx!S0o9{=U0p;R zb;}(qF@n;n1Ku{UG4}kZ4Cf^%)cdJO6!gXHEGx!Z z^D*LFIt9y!?h-5J1RVbIQ)b(yp#1W;+dfzYa?6L zdhmI$vuDqi`fND{hlb9>Kn^}nKK!Jf6TCm6_uESbH-K)hoDG)Y6oQWSb8+ch7t4R; ztli1am%vQ>nsk;buRA=4))CY`0Z^ZJU8AQ!s^%tISW%I z14W)avEpf@9K!JnK2!%zoXmp*yYO28a5!A4_lE76S1G%@-Z2?j2M7L{wbWx*1t4iz z-QvleU*`jP5E*sAN9iguh>UpzhSOavIrEb8M$0-e&{r+&lYD!n{+Z4$2Ts{|;4C;E zrqzpPbl#|ZaQi2_bO-##0b`st=qK-z=ru|OU4patWxs-R-m~Xnnc_AF>@K3gNwZFX znz}kV{-fzVQ?3UB zMwU~(;fqLQ4|`Rju5L%_yWZ>~(FIo(NaPB5l|cBe&rrh@xknp>bpP0b%uwBLlPF(cZ_6v zmp0dY)HyyN@kO6?@;0)vW z?+72UuISkCXlKeYl4hMWRZy|9ulZY|tt@JAXUcyud@6YL+L!DHC01D}DK|jzJ;Ejy zJ`gJOdGa_++a6We2i*)XrTF=2It3!p9h#}LMVRO*GyKVk>7#1qKzV8@AC%d(lnN-n zN{f6Pd>6g9ZSty(^%|s1ssP&V_yBh&;u+>(d7N?}eAJy8n)>JNdkr>VW#GJgPo@IN z29VBnj~}b_8o30+uaO824hH$62^Z-1cLT3AkmclqLeFrh=13mX6KCuAC|W5U&37F0 z2iLm4uV9zs_Sv?OQ2eFi;Hk3!N*UM?mLYlUYBsy#JYYhjbT>)&E&9?_NMF>;!kYIuhkLA z-ll<9(R;Pw7@?psi{f6R4#IyAmFe%hjd8S-HEn8g?LUTDRisXCqoP0nCOhZ-e)!Yw zXW*1Gk__FwlR^R!^+5^+i-|Sfy-6OS0*vyOv<|?Uxs7~&zP|t{nAx=VkU9$$9);Z< zzqi%pD9EU3zoz8TMK$+eMi~w|5$U#2W_0W)D?vSN=p%>4T`j+c%Cyxxy8-SiUfImA z)=LiL<>JW6C!qZhIQp?VaJ7~}1#wCgf9#vHb2S}4-2{G|l=T95=9x(lQ^HoR>QGY{ zu!Kl)hNR}w6DYNzZ}po$Tv%%Wl%eMRyg2aAN=KKN(KCfjGu*sB4}zlEQ5dj@flx6Z zCzDIhliXd_2^@}CYfj9iH`mNOLpNfL69br{N~RxIQ%k7>G*PBN-W7`42@u{S`m&A% zoRdrArO}Y$WQIO)zzaPiCo7(uYh!D8 z*Qs?9oiW(iRy`;Z2<3F#sqFU5(@EgyQv@Fhvc$ma4Jt2zEHnpfpEiVt+`opXHOyV? zo)TwRxXXxc3A2rfRl!33TCJ=Y?0VJ@esMwV6y=Z_6e~A0$^!>b9y>}A22^WH$Nnl6 zA2cp;0#qV=5U(vC9XkT>N9NN(XkWAY>wEbUq#pHl%uJFy*sIHehte332jHh6nVQYS zQU@~7$(?F7pz5f-z_Q(S;7d_x#1CC0o_H5&r)wE;Ff)1IL4IV}RSLQ%dI&Gp7ycw8 zi4vty(g{2N*oeD`FTZy{0YV!x0C7T@Glc4LuwT(?0^)HN^DEc2-x1ifwneYH@yAdy z@JAGoiLSD}{z`MDL8=4*P3nOYSISc8TXN|6BU-X0k9o*&jODIp9=~H*lFg*^Slu$B zbG-7VOEtwGh!yiEud9Et-E&@`CclFwQjsNhA-e0l@}b z0D|>=AOOY0UOV-SU@oJaN6p<#E-AgW3cwIRC<-Um$lHlgX`$CDva0z$TEC)Qf5+no zgWB850Ga+NPQ0QGVWIe-^q0~wlw#5?BNR z>A9L}7u%b;m~jMYkAW73k8j_?3MCE{kEXsO*5rQHOG|k+-{QltTA^Q{!v|^nxMa4? z%8JR%VWe7rnVth$-In}%;C-ot_eI^jgeFr#}e;fAwJXW@>%t%&5LWn}yd(X<=gfc@s4Mavp zWfQV9LNw%wqU=4(XqZ`v5T5V4^}gTt9p5;P{_kuk$)DIRA0E z$Iyis3-DBAoxuN0h)!QgrUW2#$w{P6Hs#)lhn;Tzjt||GnBZjziEI;3VITme-ZaHl zritKBF-ub2bRA{WcqxQGxvqfOB1X5DD{O1O4njKp7;c&=vx6X)_b4QoR|LPB^w!}r z9LzD%nmWhl!_xkUUd|tKiQW95wy~%5_uvuSK6nJSnra5XlZ%q9qE)rS_&G! zcJ1x@q5 zm_uA?DFij_s`kXo9E*1FO5)@iZ2Z0K16VIb9Nkbx=1YxXl2;3`5!2q16GKjXh$|+d zoyihsfm}~(QLlxU-~6e*r5Jl#Pam_j08s4|!0E=%+ZlbMWzB~%_<@6D;x-BQhiXad z3mTMSpUdr&nWrR9^PW-875@O~nOZ;$1-|;)(k{P9MRET-<;%3c{m(*B(k3TYyyMoz571-L-FpypGd{s7F#eW!u+@}J5=2Ot&3d2#1R}Huh?Vq)*vJ1cXdNm0*@RgJ)%v~_+$HHSEk%S_aoG^ zcLoaOpE`&a0|wh$Fl#gf{|K}r<^2BEH{D2b{DM(h zOBPXcp3t@X)`Ij=bEwVI++30TPH+4|9@k!x_E-;+@oqg&3C%xD5Au(Dc90W%_T<5+ zhcBeoD6gmEVENF(s)$V}x_$DmtZ*|+R8#S${m&2E7 zBJZedTF!m}TEBUbey7EB%Ez*tai?|{KZSmoXtZ8QJFRD(HM!s`gLm~@oUB;M{5jmY zp0@GC0#Ib@!xtYSH_EfFyyCVj<*F;YF3Vw~d6bK@hm2;q*FDc~$F^Owuz4*;r0Q71 z3VuC}m1;ih(OXpm(A9dYyDxrUKEXmU%b=vB|F`z)xD*)elmJdk9GI9W-`w0ZI_eC*7)hb0bxZ;R>g=86ZpsgL)xQ3*tJF;k zWT!=sdEdzAz@?|xuU$1?n5;N{K?!iIqu-LJ?D+ipmyjj3xDEA0tMQStp&ivr-7l0* zchU0O8Fq_qI7`g_Hh(iy?=PLT=iP0k(HCt|egA0Tc*RX2OTI4(zRG%3Sv^Xc3qH>Z zLs_O~-2>K9P;2Em8xRnnx3iLn{ElLkaGFDz-tsrUG&TAhA73d@*y~pqeEQg`1Qer} zK}!Lht$oss+d7fQNv52JNT+H&5K%Tb9sFyM##iCfMN}G*iPs?1U#!t-YTlf^G@42X zSVrlCKKA`T(mUhjvl~$=?IxVx{n>Bx4+B03!G6*(2N~-x$lK zMq@%l+9P#Qi@qcq2C+9+S6oEb^hl<@zwzHW+2GoRMfn2DVNvvysaAy$&8;yepR9$H z130W48T(Z|D60mX(-r<10+0u1Oi66?GKjg~ECNV$kjnGsF4{H$ZfmXVwdwZ~r151~ zg>JqPjT#Z+jIuw9iN8C%J@>hoa3AWDfznLgNb@MQg-DV5FH#I_0AQzh`rajK+b51l4XuA82Mn4gb?fl3dKMc_#xFVJR>HoCX3+L z%gF}FLS%mf*#d&QxH0Kov@aahqHb(*3%6rL{JY1FXa&C?Yx z<(;8mi)ikxw?0@mkBTqT)2;Hj-#XP&CQfQH7^d06D zMqX#f4MV=;@aTJ~NKhZZcg{^_dYlI$f2ng@2XD&GHwEvKJBsktwX2)#{JBT|GW1BG zBRuw;j@VR-B@R|Yyjab1|Fr(;tw#?XMzeFVbjx_`1E1l`=EP0>Hi6x+@Bt61BM%ftxXmA|nZArN==N_NLepaPiwQ#QzLQi}$ zfOWoXO)s;lZ^D)8S>Ry_`iw{ZqEt*Fm>F)E9ERkD?PH9)LrBNGxBiT3ORMk6Ov4^A z3X`jWej?lN$xY-}d4v$H!qe86k#q+o8RQQNNFA3)FOp1Y12Stgsf8RqSS=D@_K&V0 zYSFx%m5iE12@UEOg*avJggv`>WKc|5vKMh-ok{qAwen^d1qd_B76!ETj z(2q&J115O-sAFMP(83AS)&MLx%6aGKdh^iCR7EAc`7l<{49@-!65lKtpBYGgymYbn z%#Yc~4Stuyx2s2xb2lKj+2H@Wpb^t2PpxN5YBMkSYo7*BRD{%A;HG7I0*{$Av#kEV*+8v$aGjD&jBC#4AFn$(g$M(vwII3M?$Mk4Ri^f;;0R7z;CzFz1rhMRxK zC*^;1FE{+LQsFrq$wQ>y^9#V5umwlw``dN~R=Mx1!Tpbyk+gYp_21_N%CHy0_&Z$)IkuYR_c4B|8%FdA29dY3Kjlg3dSV~_#D(jY6Yx_ z&Oj?=Z*Ol?n4e$AB*vz3x}%;(fB~`ui;+@5UguS9&R&F@vf{OpQDI#V&Cp9d1b#qlnr|muYoz=z(X^u zVqoWJLs##-0>>@zB7BNskqFx&V4MnEOBx%k0U)WIWxxekkmGCpjFBZ9PR>CvEIWQiN@@X&LrqOg29uA<{Rbp>-7E=!5L5~_q3g5V zX5e6qI?XwXvv#JOR=RWN4){r-R3Lbp294Q2vu+I4tU|mSm!b||c^y_br0iDX+Ur)> zd_p)ro@S1S?9W_Z+WgMr0!UKti2FF4d#11)s5y8m$NyyG$9eB_i>-ImHeNRW^JgML;!5{W6JWwg zp(d!h+l2e4-kNi5;mH`|TMIJ~4mLK_?hx(r06RQ1kfK^nrYL1!)L*I~-Lntem1?g4 zXuC*a^S|@4vO`BdfBCZH;{H5o)@SMR>n}&o_MFfNTFMAgvlE=1y}7cSnV4@c-o8Bf zHX}Qr?(*Mv3=qJD-3mvU<1dvrq==q7`jBR&KPqP}I=<6A&OM-cG%xdIbd5>%?89@S zbyr?zb}W0cChEq;7rXZIghxxx?s>@ET>7#52E7`CvmP_KBKzKK*nVN+r+nTMNuN;j zyTt3CUb9LRNZoO7iDBK@omHp+M?>Hx!GmdDkMLU$8k10`SJbyJO8H{r{zgTi|6u^L zKwxO}y>dN?RG}d|iN#NbJ~=Ongu7;6FT8duFVZ4w6}JcbLdAf2C^_+%tF27Q+uaCn z=5oZF9bz#U;K=*7Q7QIIZh$qP71-se+}Q07nUO&CEaIs#(C?ohnAI0cN&6WkTuxp6rdasPaA>X8<4B_?ozS>^htWTF~O^N+HtgU0y}t?c*r z8cu!sD~J3!2JsF2U|(B;`>bcPjX~cFj0-Zis9v%QNA;tDANk-AV{QZEoU+)4xOK6#DCv0?(C$#A9xuv|9xC=du+PyBuliOVZsF^xwX1eBAk>rG|-A?>P zdR&}q^mk9XB05~AY#BBFByxhH6k8cdp;(_{g87K03znRjoJ_?W!pEY=y)|LOdgC2! zs6ML7=hHZI=1kDkEi%R^WsW#1#j_`BkR-$xcsK8h&c}egvN>Jz-5J>wBcc;kCCO=E zj>r?|<<#Lj$yRS+A!Nx>9jNsMeev=M<(RMlq`Zkrk^TJ6 zT%P3%kJO0BNKlVgfVLkks^~_Nay;tkL6c`IVg<=z0*+2_s;M-XG$FathG>MV^`x1#TgymW8)m20>Uz+O0x zX~_CdIBFZMm-3WQtS(wtnQJ^G;{7#!kuz^7GW5;tc<}>NVSsBFBkJJ?X+0dW=rJ!D zyy*e{d2{a8f71?}JZaR(axR-o7k{Rb=`KNj@?AsNf9EG90(I*lvi+hhO9q~B_HxLe zg3sJf6Bo=3kvt-*1Sc(FE1pnU|l? z@|6a9vOrzgR9SEp06g`qQ6OY>?l8Fz!U>7w{#nxRo`d}52)GhUBJIHjX+T|74%ns;n#rGc4hJU*&g?B0;Ty9R|^2f0gC+nM?{L?VWSZr#7v)+q-#+OKt zcVZB`{5{x6i4Wn8lU#jKO1`S?%RUK``qpSZ-)3k3W)_kBgA9)uh0ZvZ9^-jc165&XZ1GHglUo5#G!@9=iJ8_qz%c>zP2tBA$T!NVOiF>;=2x z-@p6ygxO-B(ctJ8HNHzDMwmIa*y+!)qwjd1-4Wyz#*DP=HYQeD zZU?*kh`g`eK{&p)ayMd^EW{YWdNaT=jm`(~UGJ23cX0TgTtQ1iQ&Jvh-G)gveL|Ar zh74B^P-QE>85^C=qL8|HPq5_v%psg1fPM+Y56 zyed6)N3lPT#)h-vK6B?t0{dpA&c~SigpU`|%H39xKy)-d>^-AN;SfSH9b>oVwD>vV zh!K8R$d#`mt+TV*cLWKZ~g1 z7`(stPzVtW041)_2;%&Vi3-p#H%x!@gj$jr@N~yhX(Te6nw~KKPRo(NKu&t5PP{B{ zKHo~6zlOV`+gwK2J}tbL{(f%9p5H|!ULmEv1mla}3X16z!Z7tnx`4;;Ny6SF8!KyX z1T{N)+?TFepR>vsrGt9^tChEZ(U*4Q?GPjz)!_A$&84ullF)k+SwLx5Gpp+5%-!*TjJ*{uCn99n_r-G8`E?UzxOD4(gC} zeq=YA}^TC^p4NEMz*A^lLA~l3px;{0Q0+D?SI`1V5pVv#+VuTO(Xksh#nN{3> zzn>!Kyx01>RU+jbmGcJU48dS;HopLpFaZYHlql zRBcr_EE*abz@N1ToXBhZ7Ki$ey6^357{!s{UWcqWG@@*LKvznk+)N#ieOBLFCe8=> zIw(%MT`mxIcZRKaGe1chzd*Sm5;WPdjizbr@f#wKCIF!m^6!w*>fT`RhFn8d7SJ7PxB3>~#6k4~wKISKJn&-v7QFu^X#AV^&e)gVZ%*ma z%$YR{Twht?i)+6+Y$bY7K(Kl{EN{V!Tosp>FGp>4U+ry+_gfBE<~d)YKy zAr}@mo3{=c7V9P!Sgf>Q$9@?Gr#v{TZ)))yb_EX^3Q?y7JV? zmT&kzo3u;RmJa7Ko!cMRHUXNTVmieHzxS%V8ux+leNK3t&-_Ce`O~WAsJZLjY+#JM zQnJ5`rvGqIs-k|`8(jf!jnbvA_PxFNw+0O1_6Du@9s-hBeW}c!2fG|9nmZ-;`lqM( z%80o^ktkKV7vSob*_^Ot&C4>7(I&*HRXC8X=_0S-KK&rULzT$hm4rI)0q(X$5kfpA z_2JUB`(|fkWQw5H=Kk(B37T&hU%FHpw*5HFKsM7~RuYkSGQrlUx)*6)djc$MME-xP z=Xf>Qbtc@^Gqp5ohDR+yd~sw>M}Y6g#QJm@$@J&He-ju{66F>JpkefMgqbLzW5bi{ zS$~Sa{`=NFB$>#4-r0rkyPv>ZJ^1`Wi3@Rhu${-oz)NDy@rovx?#g_QV;Wuib4u!*NFj`?Ix$~|m z+r(_$3G@yWAQt%0Uz^r6_>$O09JD1W5(!9IQiSUT;o6y^+C%WfWeNjM@@c$zT(1n3 z^X5-?gBVcSFaAHT_GkCLHu&0>npL0#$cZpgg*kj{rbrr2gsTo&PE^;;pM0Ae?7H-(R#bh49+-3)+#AOOX))IY*3XLIB-^7 z4!Cd$U0=zx!qd-BN%0ujEy{9Sd%%h-w=>!Lvxeb9j&K78MhEVoLvnia)bvs@TLCZ% z3AONspHzXKq6JCeI=XR7XS+J7J48NB0vE+nVHT9%04|_e0QOAE0r>MgYR2g zN}g8 zU<$BwA1+{fSb^*2)GHidh)+Q7E^W@Lh1QrF?aea(7aEP#d1XHz{po65QJQz2OwqMx zf00^3ScnM@R7v>)*lLHyr%$VdGf%OFwpJQx)u`%GXBzAA6x?_2@R#LCPF*&a$FT6N ztuRwI8L%cobqK9Gl5(|Ty=egET)DY12%66=S8DfL4x zjk|S-a;aOP94F1W<$p4sv1feNo$eym*q<;$LFMheKOw`14l=u-V-I_tI9%@Za3q_` z!9gh?$wR7iZS5K=_k_tXV^#K%;6&rNszHaNKTB8VNm_*XzIKwrov`Mg<&pP2z&%V1Hr0nU; zHZoHO36_U7J06lP7D4? zvc7WY&3y&;eJXuYZc8O+o0`pQB;B*I+_IH{|PE4?t+0N}MBb z7kFmjW;SV^jY4`5T*U*>1JS+-dr1!{z<|xwG8HF)NUX9qZu&Ci>Mx;*^)F7gGJtt0 zKYDf=O9$)C1|DA=E)ajI(<_)?JovP;bqUjytVlx-l8e zHWrp?Cvs)s$rzbblyi%p*kL2Q*Z{CEi||{ZnwOeLvkr{L9k>q|$hQC$haM>30N05M zY++8u{h+-?>k5E=OdDmePhc#~&*X{^cBd_4Z;AICfh6e+&P-CAjLygbt%~et+e6TV ziYUJxP1Xb6XXTk)otauXkWub&;(P+*D|^8Ovm)$oU06SQ%%d9DgM%7Xq|D8RY*uFv z0%$&^aUk*r=>m#Z;oum$Lo09odmX7y0*00T3ARU=pZS(P=H4e;qRREO<3sdxZ{uxg z-HBHJt#s!mG!>+~E{nTPAFJxOF=*xv<(=pcc4+d#6w6$vO+e>xEq{4UNg#Y#2{l89 zk}SVj*9AD^zQ4PTcJ)O=vGC7XMUtFZNrPj!6VK6tw`}W9T^;jj%+t-E`kt7NX=C#= zYPTh94!F}#J4m;jB6F+ot1F3y)t@^$@p$`((z8G>Q}2xlzzYG48B}%7l~HQL%ZNMz9;D| zY^{BRSuI1!M^S@NKb4e_N)<45^OPdCF;)g(xL3T7Lp_u7)-3j}&v8r7j z6SCX7k;X43#u}7fC!W4)S!3zW(u|{T)0Fl4vvS5MEhuO-JdHX3U3=`l&sM#B?{Km7 zU}xVR^;Os;_-~e%E(vV>35=}#gvaBuLU&yE;P`Kl-<_t_N<@9?VL0GLAV!RIk^RlO z#zCkS27mExrUp!mPF6zyo%>*k39x8Myii}^qbG+b1%P9Gd~jkSwC$S*7)$h>Dkxr9 zUiSa#QP@2@_Hxs1@tnW;LgJ$V9ffbKp$-0Z*#u2zwM24Pf{(15lO}WarHU^rv*XwN zK_iwrtAmm@8;MU}WQVoXx2@jrk{d0&vUi&EsMdVi!!F+S_O;0`)Xw8K_p^F!W6yh) zfa=muYrJh??2GSaX(mBUz&-!1E;Vs%;`_N7_dn~SFoUM{@k*^8vj|=)^{h_;icL_* zImo3~YVBKTZqCY0Ch)=pDedjGak!1%-Y2MH8)fx;p`~*X6?=i_{>ExEJR$I{%LgZa zA-XA7vRzL*%%OF{n|yhWgJzP41N9oWzH zPzhLPy0jizECGwRf)Hlp&!R2no&amx7v7)o@or%u$tpB50wDQKCCt{$>~@A)%YqHQuZ03rZnjUa%8_|U zY7=;&j*XV0{wp#U5{eEziu{?6nth@7LM*t=CymEoDLx*Ay2mm1eEQD1@gX<9$aLLw;RubU>0H#TDNU; zss{3*YH7EnMKv)@krkrTYgD#*f^N2@1T=diH=r?0v<3-Qv*)cNuRkbPIO}6v?x2IP zb|7S|HxWR$wFtg)!ziW9wKu00)G9YI4ZPQGk#zPsOGY5R+#y4eJ|tsyWev2=5zID) zB5^Z`l~;=IHYeb_K~7Au!c^*}fxNl`R2+ofo+rFO6Up3x1Y);yayOROb^OIu&BA^* zcJ_XBY+h%koEfU9qNqJ6rfY$x*dPS;~6CzvA z&nUFJ`{j)IcW=qnQnd7>;qFSySvP@VSeWBn8MCdj5Cyn-Gkc7qnlLAtmSA$&!Er;Ulj5*sT> za<&GQ|DGaOJmvB+Ot7aFU53-r%IPPq-fY!WSd53Ywze+Okm2Gt-mk9{Sj3hzhGMvd zh`mK+?V5qJ{>m@xUZ|1i4;P>xO|V9;%dZp8J$ZnjsEWpxG)D(jJV4DtLGFh;w2siI z5aGM|p`=UbzF#(KOQw!|i%JU z302!R_fC1}Lp8+}R(H?%_@eJ2q)q(^iHs(;r!BEqz`#fXp?4}@W+AX=~v?@Q1Dzrhhmvk+a-Uo1D+@vyscMhTlqSO+1{{C5< zjM)&CA}|4Jeq#9%(L#>!BjVxY{;=T@y3e)LIC1!3#UC(QGIu~>wiRQ78PTS!bUt%h z;Lt4h!%k)^VMwyIE}4j?c5^5)jsx$?3~o_MGCa_PfOMf9aq{AxX$yLMjN&mD>B%ST z&j%PIUp;(hN(sPVF-p1!`6Rt4`P{iL3W)lneTAf<@qeMi5lRc6xu*vG;w({p z&8!r>Ek-DY0xMQ3PS+F7_I9Nq{}=(obzB~ixrN@Cy54PSd}vQ{1>1rSED$s2T10}- zee;pA3P^7uHV8dp1rh(&dx`DMNl+^>#o*H2mA3g#;`H z|H6m0FL|jU4L&LNYn=cK_k65rxPk~C0aFJ$BqU!TON;qTHD+WzjpsA()Zo+S7JwZ= z?q<35HsGlVG+Y945$8OL{IoC)CrtA)?IS-?6+e9N0%fH&koixrbk0t-4@Ha1O&(a- zl%v8kUe_b=soP~Ba@QCJxo8IHM?6i;3-Xm}FhrHCAo*I2y4F7cpp#0cBjr|r#hs>6 z;uy2OcUZdNJV**_oPEt<4MLwtXHgMI^|Wtq?CXXrAg@tKj6XebJ2Ttt5{D988v7(s=mfqd!P9~df{(J53acvzsN6z^zSd4Fx*!R3?s8Sg+5k>^Venw+Zy&DW@Ez2NLb<; z-nkJ?oY7<#D{yK(#Exr#J&EWZ!^}y{`&zEac=0Qqh2zYc{{>=n3?~5z7CUG^rFUeqPSeSHa#4fR_E_V5N92gd-YSBPNDRcES%npaSNI^^?ya{Z$v6^Hujwg}xuC`WQ1EDoXgEP_9bC z9c^oK@)^)2LPQvmz1?YaLXO_KwY$8<4GmY~<#)g?Z0vV| zbc2Qh02(HWHD998A%;3Mplse(>1Fe4Q5^sj7M`kSDyQ3B|k-vX7dm0FN#DBmB0?`^)-YP4$Ws^mf&_+Xb)PWo!1u^?Vq6SYg-XPQ)n1|KW%1HvANm~kKx6^M0(82>MpgVxZH4`{c(l;z3Ul4Wsgy3g z@aP?Sc1bTdW@B+!j_xe37}D)WOZyGHt}D^^nexT)N92R+wFpvy+5sZwWs3#fezF|( z9n$fe3lrbKn6Zpxr{j%NVe`Avt@UNjvCNqHND<+UF+r1bnSgBhfu}l3t-kJiJYU4B zC+5AyNGg`L?m#WZb+K=_2;3cjZVuFTcVEokKYIR&{I$8BV|&xIVLd3W_kue}gIUAq z*t7*uxc9P8tdf%4xzI3sex(;$2|_t#eboU4xp{fP0O`Lc;!*nyjmDIMlGm@ttEw~Q zv)yOM3QL5gczgoN?L6kcegNQD9yrmx{PW?ryq&st_R(f)_n9lq%D;HcKK}ai+nrPD ziK3TB@`nn{LOmLlKilX0B}0$;TALeR6UuKAo}5q2Z)X-UT`ao+ z9gHu~FWsF{3iV6R`>z%t*e_%*AM4{>>eu3NsaupJE1O%)vvKI&l#l@!Z5&rCE!LML@T=KarjXDAGK%UB{G{BfJs)^1dnMDQNimU&13 zp|L9Xf-TL|As^)0GH6Gx9Dm6t4u8jm0tH|-vr|snM@V^$A>^r15773hOuOy7Kq5KSD!g@O0fSgQ+a9?FR|$*j06o2yuUNdE#uebpfjQBM~+ z^@uOrpx&n4GAG93b$OAS-zlxPZp*_!Tj5k~x0aYFJ<@=sv8T{UJ2)&MOuSl}^}dL= zIF&-tI@KHt!dYEE$%g|Uw^B;5m_akNKIQyQ6Y=C?^Om6x?4@DGZAHUb*b0obhcabl zw=z$HQDPW#Vi>LwYS$1z8Gz4b;BqwL*6mHL*uRvjC-iR5GItQ#0~y<67QMx%ePvct zoFjdrzMb(4>Hd2`Nyd7*`{3Kqz=tnE97uB-m7zpjQr1`Rx?nCe?)_!aQ7I#~Ip89vE|-D+I7ti|Zz+q>SX7(v`%gIZ`%wLO zgcJKpgu8-Xs+Cjyy@SxOF1blTOTbMG!#Ub0H)E1%H*zX@u53E^F8Czz}r) zyVOUsh%*r7wE#Oj1&QH*`shGM15`c7Nh&qUbR6yK`Pk~;WnlNfwocf+3unkrRGfVd z&2PvR72F)i4_W0NS0JpsNppoH{gZjR_1DV#QZ^ZU2Mve2AJW#e;4%KYw+&tw%%(&s z_5t%TbCBq|xezvTP5g#96vuFD3$T2@0SE7p_p;!N}dw*m_x^#FdG%LI7QzBae+VD8+X*K2#GPo%t6C0 z$hx(ANn1;A$JE(r+J}4ImvVg$dKx~uSd! zW`cbn3pZQy(!{p2tjJh4-Nfu|8iNc2tZ*|(9U`tX;i$F{8#44@wLQOf`R}R0u>Bzx z5DHum`g$4_nUpj^QzIw?`9GD+3eta3F;OvqVieHl~6n3jecP;pS)<>4DK*lRyhY)E7t7%|VS{@wked z#rYfU9Rc2hY(oTU0F}_skE~W^1wuD3Wn~3C9ncVj!Ip5gSChZcv8zBxGbaW(p*WAe zBHC&v1fI2*X}Ds*{@FdMa@h|*I!J;=1?)?GaIbi)&q?J9-C&@>_mVHBe8b3&+v`q48}tx4 z^r^?A93Q`(xH5^&S#!ou=UZzqk1Ie77WS4RIUJdAuEnfCr`oCG?rGt}EijP8SB5QF z69v+MRW=VEAe3}IB?DH3JRQOjpHnTo@srkH)(qe!Ktomuc9&w-?Xq|mG61qExH!-uTMml}V(3-VIP zh`H~YgJ{MQ=XB@yyMFjqILXmsciOSqf{X{NFT`-5`==QprjkPbQZnHJ`Q%j9kw z#OP=a)p96lL4V7X4;ph}!n-NdUiW{UA{crFkB>9n|KPj3bq$?{gEo4h2CL}!`FSx6 zBRQpF8<=xc(&Io`e)ApZjbdU2J0oiXqnsnJN7=%~vZ1yFX~k18f)KE)N`cwAWiVO% zB5JuF$E8q?>cS`YmIM6ILB?o_J1<_o1XUZHx^(OiKA7hHIyv~OB!|Nj4UKqQ+GQYw z?B&a{82aQ}f@#OH!5HpZi2_yT1N05K9GaEJtYf%v^vA)J;r|beTw#!KV*ZWw84c90 zK9h<`G2u5abk%~^I=qPr&ntcCM_U@^BdHAmI_a#s5{wO@WTgwd%E6vT#6yewPs%=@&76j~e=dJ&RdVn;%Wbfa8zy`^+mJUyx$#`u` zmBpg}=i~R8mcWg%hR}V7uCBBKDh-GWn|=ZALc!S${!5`}!-Sj*JIiQz?I2IUs>K)o zox66aJ6kC0&*M6=B=*0JPwUi<6h=>yb~L{BDY(ABqUy8Mc=h45#`(C%6<^w&^)`21 z&YjR08ztO+)u{;>fj{fdFBsyw-o8C1a39bq%otV=gSo@LK&=9p+j)czCq=8(mbGpV zcXoDoHK1Anybtu@C+5S@Fc^fD{YwTMCyd?QOPAKPUV3pC0^$fM&vaKXgEiFLO$Za` zz4FQ<{9?T0xZ`6YIJYr*IPe9;dA-EU zkWb+PHu%1Z^8r%ef4F-`nrRG|N@VU2$A&;DNs10Wu!^(!+pvI|_4JEjtI11cw9Xf~_d*n*g7;maI!P%;1HyZa%=02yh zYg$9Fk!6mOlKL3Ak);bIr%URizG?diB>Zk7s~{tGQk#8F+*<3_lT_O=?Qve6K9A0~ zviAd8bv;`D@_^}E<_hinM~&(m3A<8O z1jAAf!5^h7Q z7;Bs;&p58T;eU*~J;g>3_vNEythr4hZ%nQEbdsawjflE|*n2KwDF`o5Uyf`*5w@p` z0&kw^ZX|@HA~9z?Fhi%hF!}eXYX}90;_j*%>CSCrN`yuUrv3ZGEoK{2q=jB>Zq13( z+hdIwD>FG8VUVbg&u?626eh=enj-Yxk~K(v@;bpT`nc%U&IC;2P-axoM(7W_KIp?{ zv=ZBB+mhIL$a1<-J8oplKfHShXMs`2pB6=;G>DjRAvXnb=WMYL?y1{5Ak>~mcoBwF zx_c*5P1&^*@0wPS$y+dU(!?fSe?eFntGVmI!b`s4c_bq=d(Ku<&Jvnn}2;MBf~O6|T(4cd&xg8^+1APaf}QBTMfUl;xnu zJ4y0<@VGU;qE^$o0eO9mw{egyy6jzZM?1Pj??K=3=-+WIABlaZl z-}rR7KLYQ>v8^BgU1b0H=33D%!g2iJ68b3~x|{ABK|S9V)p9?@XuD_3c?-5t{3Ner zp>kRIll$o#x_oojZ+iOv4z`s})3-&LZ(R&OZ7%;=o@@u(J#m+zu}9Cb957shqF(@9ET&q(~t4Z17m`JjbLg zHCE-xoFjX16<_t;#cXvg>)m$P=}uk^^}2&tP)(6?FBf-k#bSFBZd)5&ItJX16mZ7a z2j&L!SwtTpGBi=+^KIDw=Q2HJ&pq8tfgfpMo*K#FMefs^V_1aYzjSEqGvr6?j~=;2 zhwi%~3HfZ=%*>E}^?Wth#39dPsI)jdRP$h$=B!=wS3lH6{_9f)W4_ss&j^Q4G6mlL zOUS7s!f$RNy5U)i9vFUDnDEP@N%;}K@8L(R6t7)7jSkNF3ObQys5wrp1wKelzRM(R zYV#qQ$sCU1Ik7006t^!=w!=IKu{ZcNn(Afb9Eg)}`Fe#xcdq92( zBiC(=XL@`3tFoQ|_IXUxmhwm}fA91I8yeFs1_Q=56IFpXR;RLV%&`96+~kp$KMEz; z78vxq3ROfRIJ%@KLe_Osv_tBnYRZO~JP4iD5NH(3JL6!FX86f%%RNzwJt}?f7Sdu~ zrY@NZ+bGXP)lkSkpk_*0&$*uS!EdeN&F{MkSW!rX(btY+!pYiJnfFXg#BFT_)?XHV z15#)68kfUD!oq7HE`R4CWxxFS<;W|)(m8#&O;RD`EFoToOn+r$pZ#_6S)70jnHawy zrJ^Ro=)RVsuHd}&WGkPwGUE+6Vbnp&M#Cw47Yy2Sa&i!8>EZH_VmEzeN%FWoydrFG zF>rUS^_Y(pV*)>{xb3n{9x7hsCAoPvsT3lCVR47Acl=Du!BCG$5uKT$f|tVcX?(?5 zYL2agf9bn=oyX{HD7SqD4vlHKkr0Z!8?wpkJp_LmCux&D-Gx^8@87@M%ywmtfP&63 ztn1g0b~QO)9=5xUrE*c{yF_5 zL3vgcuwn&fjobnPfyb+$B?T?MBq}&Bjny{qJF*o+f!o&kzI^s@mpfO?%-A;sx6MFd z@IdC*Q^fL>|M^U=;%etn5k({FS0`JJ?cE5f_ZP`h7^uE9#*QF9KEL$EM7;*O<9X$m zmE`0m`pE_|m0u2@&#m8Hxw1DLTHYOAo+J+|@$%`_XWa2GoGx=d zwln~GBJcSs%MmTRD5vZm?~`)Gs2@Fo(^n4?Wc@t45wWvvWvV}sCP3+(t z4>CMlf@Cme^UBcEJNjRT!DGH&(pa<9HSIC~H-!usixz->Be5Lqxr1sX>@6SF0c6C-e^>2)Ifi3aRyb6sPVK3o{t-D z1&$6TT+sAmN8@n%eGg3B)iw@AQEYGFEoyuQLG7`)20cWx)lcH0fje-TE6X;akf<$l z#pj&>cz0JdIZ47+G*&JKXWYurfmfNU?c&$3u2r&aNr^nVcKF`gDw34)@T+)bM(Pk} z4dwjn)6{sWE<&Cb)5sl>Tt5$nzSv(@F9?PJ5j!Xp_gv?Ep5#qZzV5wy4hB;&$!uY` z4y#~Lx!AtxxIN}Fl_rpAPb*{w`4x)G=(CppIG}ZoQFP&^9%64vzvOxa%J>x&)2%3I)=*_ zW#xn>Gfwy#xyjkvdXEyEUI3m#U*kO{+|Fs}x1&@PbO{o~n9u*bB~3@iL9No%3uvSd zM&RX@O8lQI;vM>oqX)ys!{Bf#?|8?@{p#=pl7V-#rg+XmHSO>H0<%aYjr7}GJACM{ z3fsZUG+m_NL$1OCGm{eSqDkVNzFkNwpBj77vW0}(m+wOcQIT+KPzsX&ZkmUVCt4B` zu6xF1L!4s>ERt6g(r`&46w}7evlLQ^vvEiNNgJCib8UDqhRMe=^)r|a8XAXVm`7$Ksa%* z!%`sOakjsREC09TYBWay+F3G4CGs=ZU6+nBFqG z{-k>S7xUr5{wPImZf+hPHc?i^Sm6}JL5uqer8LGpTbvNz?YF~whu8MH;OUSz=~wZV z{wD`ouB3e==DuQ0BMiA|6PlZX*bDRF!-uUI>BcC46|~Tnf5F{d7&Kp^C#~OdK%-=9 zX~kP1S#2J+{1$m>2;|1qBvN?>zB2iCu#&YAH>O!aZ>?ZDu6=~G%=O>mUKFk*v!?BO zmHB|A^`i2Q4Lv8K5;45^L8ANI9q8MA-`Z*q+>v>mCntEX$3tV^VfUf33r0o^&#SAe z>^@M8Ecy2yW9ao0WWVwfxmymlFwb?=UndjW7+^2q=CAWbo!IJbsg`-C!kzYmnpl7O z=dBBg`fbcMY{anbJ3TH0Hy#nml6NrwrW^^$6QoL+mF6d1yu8#xLe5D;T2)Bd;1JW}vz$_ z7KOI%)PoBX22lV5D-`KM>o7%VBUf>8@!Zd!ci-9Ip<}cPY>nFYGoYvx??0#POG5Z; z9$P1ywzgLmaxVFih%ATU%h#oGxlP}iUXw_O@x|F6t$EM7n)my+yg4tCu1=-^{EH_D z&b3fpwejS)t>hb(>&=%tZ>`nGoR@QTzuIsvg_x*bAf1bY{uU^+xzT-U4&*%6Y$NP96OW+m&n5NEik0guI z`hD3}c*+p}L6HZXpTAIV{mu%H@&46y+5Vku3oaG5#jTsM54KWNB#f)ib=LO{;#Utn z{?YLCQ})}IH+^>f`+ITX-nPwLbW5vpiu&X?diWK#mM6b=)bA*znG9>;A|J2SBe>Mv zDoEbOkj3P(b(+btCl>v;d6i<5-(7vvWpbkb}*ndP@3>7?N9Fcoz)(ZOG^{@}xl3zI?oG0y~?3 z$&9x6HT}FTeglZ?3%V3AYH+e?M8LlV2*qT%_hoeC|DcEzMfjO%NU5dWLD_!FMo45p z(clN$smohqDPcfVuTi>U!tV7}raA9DQo3f)bagZgPj>IoBsoYB4)|R`&68yG*cg)Q z32h|cAA?K$&6<*))W$y^u>`Av>KD9ANZHbPD*x|%BKC&ZGIDXv=S)HM=eY+9BEthm zDuG!etbAB+C1z^DcrD!)#9~~#hplNB9l~&x9kF?PUwAC=KdX~RT-!Z;AVpIEhEbc) z`dj;$sR|4fSkPx5qA!B#pbjATmdXh?@DOZl&%^~B;5VrC4tIM z)CtfEZX%YpCq!TWT-G9s?OfLtd`TmS?V2rk%5D8IatYMbeZt_GeNj7e2kTi0iSBLG z&(ubG_?%H2LUf|dwP+tEB!LQ`VFlgr!!}UknM-tY)3vOcBw}gnbwEM)F=5)kV>}h} zHjT}8#!j;4M2QUM;1dh_kL!)co8*dIQ`AG69L5FCvwlgVX7m-U~)Ab{@M9;aF0gLy`={@LkOpS>5{rCUIURdkH|S_B>iR2PXHJQ zQwdPU)tb=J&C+7hT4pFMx_} zr4ts5{~H6S=F=(NVU(rvpqiPYorwX&e8@D+Mdyal;Dmnr*RlQGAP_((O_K|IJ$jJiB0XX3vCr`s|id~;KjW_j`HJnLbvC}Qyf_~z-$srsyI6;i7 zm~Mi9t_!p-2#0I}Z5!JK{hs9o*_a~6g>#q4)$YN*C{Tu?tj?>U-m27pB$H3_&(76u zO|ITUcOM6iOpG~-OYbhqob`VIR$lfOK^J0~=zs2M=j}MM`{u)O=M8lV zIzpT0qt-HcgV^cZz-VFUNijC(Pd54X(kd?UYZ#o+5cXyo`~=pFZ%aV?fL~yw=yi&3 z_|S#`lsA3bLz$KOaQow&xD&ttMZz*n(4@XiZEM26)o5Fpg`ast>Vmi#n$1FUp_N2pKEM@Gl{p;&-Rv;?nO zdoPL7RpB`zuayX$u0rq1L>+Dp;>Xu|*rhWqLTcxX{Lmyq5y-80~a{SrQLGU86o ztb7x#KiKRLT1IXV)f_U=McTvHoPJrx1+=|Py*0|3WsgzHE;~czszL1CvPM|Zo?dYDs{)!~)d=2$ z-ne$6Hy-|@H%|q2H<4#@#(ks9Dq|qw+mOHtkVAOOKo64D8s_Q;yxqj?=Kf z1Yb(P=?YT_Src`)9DIKl8p~5%-qXK(mklT-iR5o6{Xqp*=qx2Y#wdpW*eNsZ>~#VD zN*GkAU3wq=0?qP{mmVIj6vbasiSAe@zQ>Mb{)~C(U)-HdgT7 z+*rj=#RA7Gpc-E>-ev#&TC&RCA=hpCp0-%eMdfu-G77&7wArYHjY>XJ9NWQDm;HRE^S(NT? z5aYGpsj_VaAKpuhCh8r)#o7$$biQob9#cQK_=X@|nFd4;+j(F#koRY5gJnKFEk`FA zJ?Vl{Vp~*oUi5`7^3XHHR^dQb;9UF%0CA>vzSAqk-V9e(>t|4zi)kHXub_A<@%Y}L-=N;ZdXKV~koNSA z#~o;<%014{=cN|sVgw~uyT$euG&st4Jf;G^W#-LQ&Ht8vd3fCj7co_T7!rGa_v6ye zQ0al>t3`s&E$>|)o%X@ZuDPe}t->{QUxqlN*Ov5ARnd>5IkUh3;`Vp(gDftF7TnkaY$ zt`=~^b2swBOrG6b@@V2zHr$nE(WZNG#WOQB#lLsT+WOK}3V%wHXkjtY{aT=kP?IG4 zx)#e*fLrwllWP(s_5lZ?1C&@_${R`_f%;}o>}1%zOHHv%$oHN=_R)hdpd(P8` zmt81|y*M6ifpSU8yi%F-vawLUjPe`d$<4S@lx(P8mH?K+ z`uqtp!O})vPPnbnKR)iKY|#n|#&W?%Zt8VS^~Ye^ z+V0CvOFSLymXnh`VGf9X246y+Q@;8PIMMVfP4~=F=!tzdGd*I2wTkD)ll97XqaOAT z_R1~9=J(%!e?Qi-1lS6=w!-3WS_&?OW1E@hMhT#C-DV#+(_3-)>;t zA*eWJca}Ua-v`5U%CTa~{kf+T|JouA7L~yPSsA(w;OEg?Mo9)72;qQSfCG2sS>h?m z>-ML#lZZeia#F(>0-}cQ?ap9V>pvgktRmaeEJ}glzXEL1{Z?KK$ywC)2XH}aZ2;Z! z$t-RO4LGkvfl?pDR*h>Rl6EmBF8ixGW5RyYYi-jHAAL7)N!3TQu~VneQ)i+ly~XGTRr zh%occ!x2Eu8LT8XvlE?3C*BVA3$f5*Xy;96dxvKz7ZC{U&on4qKR_;u60CXpJy#Re$}1jV`@mD&#WjX zp!?6$&k%Lu0yiW7LODLxEF92(H)WnQ%N1K$LTT7AYsiD?fB==dQJt^7bwMc3ME-*W zzynuWL1+doL*VUWsC$mFAwV|{yX=94^ZsEmb=2qLR#5Y9VXjDTMjx$egL;GpIUV6b zFEE1?{@vTvrHS)Nr#?zZL{5t-5h%{@L<6@eKzVXYTKON2hX6Ep$I;d)PtZxn*@~mj zc=3rwP>ZQ_WPfwsE6)9$Dk7vI%kb!W0P4z+;GbN*H)OWr*RbAo>Fz5)Am71ehKP(j z>Q~GIE{Oq4?EGPQ)%tp6DSkWq)n|9b& z+9HYL-87)9BKRG#qc6W150y&I$3?_DXBb&yZ($8IoY@JpCIE&h0|=kb?|s_)Q`I@J zAN#)w3!AsK9Yvq`%M3Z$-od_0B|NXQ*pECJnulBK&@hu&%5{U$$@smM=bB%!$!bBDo6-2R~aUa<5!HI0m}O%Xsq;x_4HS)0*f4f zvcwGN1_|qVQ}YF-{i(}4d{@Ynd3$m5j~Tu`NAe$6Xu@z)Yng~a?4f3dh=qma)FthLhKRG5)`NL{u%!72KSH$Y*W8@Nf3Z2KdY`?` zZqnzR>}kucQ)7!3oF9hPk3KyIC`E6eXAn|b=vWgcMDlw&K|Yk?wueaddQMdW;?Ht> zmbu%Mk`A*lDs@eJGKQXNz3chj?w4fy?#cN8MN-qw=ck-;4|eUYb45%Gmj`~TezVB= zMdEUe0pF?HGFP#}IRzo+2EjR1Omv{u>+EbC$R$S*2p?7qM~#daA`Me5kr0iWQmOO% ze?{ULQu>dMx=$hgP1#yZSJqu(((KZI_6EVNdfkqHd^A~ab3Rwsd&HoY!N+cx32FFY zL7#k+)6vel;IK1)#s_27l~l89(T4lf+wfqAfvHGHtNHA@#Y1zV)il9JKf4%-B*)F? zhKThSHOHm`QeP7Nd1$^k)O){t{`Z5G67QAfoZ3x6ns|svn?rAO#J^(PmlnuYzG8>R zN@rArGKXc*GoXc*_uPYmVsNnqd};37L1j-oQq8Hmx__*=Us8{kL=F^ zHWPPI_Q}+sftC$(q8Jj=tJhC3o9X|Bfdb6N1sD%O_5{JwS3#!u2#)weUu}0H1O1d( z^YV!LU@mQ4A zvA+|tJ~>Mlbwt0g+Tf|d zh=$SjXz@hLLSX??G(_UbKuFaKK}}2$NZmtxzH>FkeKN+b9NXjzNr-vA@F9*2ZS~` znlDnX>&spf6zGj4RV;nFZK8b~M>7*Fs-!!r@!vO>6`Hd3ecF|~jQ{Fp1~;|{W1e`< z=R9|jjh(igQwZEDe!`SUO!y}*b=B6L_nTPA8TvsA-)|X>c7Hx@TvuWF8jp2mh7x3G zo_{-1a%5==)p7(URzMTj62A2Xo|_-IToKxRh<xMUIYW(u^{ZCKS70_iH6u^nEV$LD?*xd%SWtHR!BGSye@(~GAAijiT z%~ijNnNae?hX}Ek!z_09Bw28+peq*qGs*t->m}mP{KkdE<;VOP8n&;wv2Fgh8+)aN z%!WR@N+zFji>(32)Y8Gmp^3Qn4J4=?-0l2!k}M%d`OKPsn%C7a>Ewd?E8TmIv8QlW(g^J=ARzvi6p;F4!9pfyb~I)Ex>Og zUPBB{kfTOdlP7T`Ks2{I<2>B0D+_wo8xPk#Lp?zddS6I9@)G+XveywE9+1UURZge9 zL(xr!(jo#k;5FTkOOH~QS7aVbVELtL25{%Ax#?1}fW+CSdeYvCVz2q(L)w)pjxNW3 zQeFbAOb}%$uG3e2NQKH=dT7xn1AB-5HeLH!IH_Wpac&Vd;=^ts-rBNp?Zr=U?EZx) zQ2~&u!acGjnP*g+oEjT68Yvm9njjBXvq1Nh&GVwY0*mom$+LCcIT<27qmTV0AK@VD ztp)O`zY^Fj^&GtHw#|`-ysL^OMWt%aa*KbQ+|MEbEA?`=Z(tf_B_zWAvn`zYu3{zD z{rHeUnJIjQT8wHe7`1N2oC<0cY!e!#P60R*B^KT#weR%YlwR~qFSY%1?hl%SxOUxI zg~6dD8KPg%+tqH=0#~$y)F+6+q<%hD>#l*p)x{BYc;1gft~HR_R(NTgAr)(29?hvU zVnor@O|6y1mP{kYSDh4Li>x(ov-EjN_Ne7J>49Pf-H+Eka^!eYtHwa073*>ZhUdJwlc+$mc4+A8wI}6AK z?u4-<7a#St2ky#m*)(>O#l1jvET!3938Rm9yVwoax6RBJb?skx;>QCYY=$xF4iTxi zFwPJoLH};IsgXXj`6e6aowN9!4Kq7tplr^F5kbUG!2HaUj%HaAT!7lTF4p*ev3m%FbF?z*?rAq@!}q!U(m-ry+8$=RZj z`kO(^Yxd1N)k2V)5XrVM=;buF0f;iGp z`iArhp^{wE-St1>0R;Ba%16yn?He!N*z%El>8&Bm!t#>GciPSUtT>_wn>3iPp zHCO6G*qd8PP4T8rJS@1va>AFQNh2%X(4C9dppa?-92Cy(k z)PXKWFAY9(S19?!L!H&-*0E0QPqb>gQ>aV86!8|nb!PJ0V5Heo)`NmY%9r9U(ry4* zP9hx5#ko0{^uY>}m(mvwxboxLrFsY<+Osn^8l5UER$gJ$V9~Z6L2C1plP6YEuk{D+ zZD+-Y&*o#p6uwP;DA^s$j#ts2*jg4Un?d|}qr2utjCNL@_o(w@q1+h#5g@g~Cn8?$;xTYIW@XDo2gzICM1 zwZwf1B`$6H4Ca=K4<<;#&S?D7XjRGijS8{5O6$GpV4G5=Q>dnjcK3`R<+^w=RbsO5 z?XlRQ!mr_JTtzunb@fzwu){dWZf9<@aL*`{aOP(NzjAV9y{rc1|7hsb+t0^%&W+Ki zV@vv2o=NbRyP~kUy}wsek5{TZEYbrVODDc($a+DA@_L)p5vEJ(SX@0_l0-Sa7z&n` zhsu`Tr|UsyUw@_WpDSsr+Ty*MSkw&`7Oj)Vmb2lc;nrG@=6uc`mHx(VeercSJ%IYW z9e*S#m-*kj)H~XYvcYY^UF)$J-HUSPc7C-Pq6b_H zXCiNG1ZFN|d3XqHxXhKE2{w7=($kl~2Wd*kg(DZ_rTeH5j> zNxbXDXN7H?Ug->}m+X?`UMqnm@7__Tk5!y?te2|c@nTh$<}s>%**RDCdnokA@E}iB zA!LM3z!VD{WO=lj3KthwN9_pHJ#*PHZnr<9x!4)!ecmKL&4Ma- zRd&Iy`cn|>Ltp(OD>m8RXeWv>q6M~K2l^9Q>~F82Ih-$jHkE63ry?_DnR`y(z5U|! zL_eGB`bzk1I6$mg@$FB}%#KHc-b>M^-T2xrn<+~Z#~wp#B2Gsayph0l^~zPQI{MuV(h3vM5AA%kuh_<#o8XED3q6*-IKXUQN!hvbxR;7>p_IpiQhO@CA4g+Wo zw`fA?w13(BF~8$SbmCrQX!~>Y_!2JJb_G#k*Jz2JfirWC4ihK(>(k&yy{l6oaEQEi z>cFppjv zqr2&6$*yx_niAiO;*4C$n&s%QKjDN8=i?xjAd18MZC9_COZ>qU!itmNXOCg7`kDIl z$|Olf4E=DV8c$RJ3r*GBB@I(6Z(@i_)Ld&CRFo^3D0@yE;L7h^weAq5eJlvv54aTf z+WkZ6r|v0Ya@(W0EY>Fj#NsUZQDC+@T%V>a&76kD8|Rs&xTQkCFacWaeNGHEs4C*_ zV5L{#TS{L@CO*YDspXUeoz)yntZ03mXU`)O?*=2|97~9z!;}soQf{B@N(|L2CjBoP zTZ!l7Lj^2(`|mJc=-4&XCQaXwqpG}e_jEb0VE`mvtr-i4?gJ*RFBrXH!d%6VnkAW+ z67B?DiHxB) zcrz94ru~_Mr0;9Cmbld2bI3$wsAmRLAkcMBv?E9F3bB!OPN@-=s;}^dWnC{FVZz<# zwZq9ctUc7|bz$t!6oRq|kWHWN^pOxi8wEXi96RdmpUZxomC+UKz3od7^TjGq!p-sw z&0oVLR{k*S^{1a6Ktu@e!1u3$>#!RrQSMMSSy(a%M(|e7)F(^yVqSy&R@;uZ50`-C zU;w;M`Md6+J#Sy{%g1{B6ZMWYxGJS|Cla6E=nslB#K&8l)U7PEVll6qQH3rpqHdEm4wj!$CzXu8^A%@@)RCkgxs@-ukt_=PFV_%1?-RDSb@((&v!oU1^ItB& zE^yFzq2N14=<)meITo~bSY0xEb{at3yIs{*FF><4>Uo0!XJ$tqvnEjc*@#Epv5Jl$ zUEl{SE#9vS$7uA)-sg%oV%X&B%f>H%XGKf{0AUu*wzJ)OxIV5%0^yHgzi>RW`!eJn zyniYA-?T?3$J`wfl=f3cp@^|U5)2(9{gTXTs1t<99J z%Il}e>*mFG?i%=)^v@>5-Nsoq^rC8Q8$`9=&hkE)dOeJQhx7*aZY(lPRQdatm&JX3 zcswpZ&n|wbi8O)!FH|EXHXNphmOO>DM2g+!U*C?Tv(rE8&se z=u<{31LK1p%D0#yKsv5k@A7kssi%7G&DT%c<@c5stXq^6L%t=7Makuj=ZuHn%PogiL%&u$3GSXTIEZE~ej#=M!vwkW-p?7;q5a zP3yVontj=f;T2;iiVjk$2FHD%#F@}>td_7^Oy7AQjOl|TdX|R}pX^47jD?}X?Q=dW zyT(tq+!{HRy_o&@wKbkUwyZN_il7N(odjpR>A+}hQrDB?=-#oQ{R_V~cRzJJSd!L^ zLQ=YA+vdhj)AvOk{eFWx>?vQ9pQlFXDcdBcrH1IW=O(|1J6p2|-%5;KDd2H1JJ2#(wXMphH#HfpS@>DeIu6zn4pUt= zBac;*MZ-!)w!m?{dmrev-#zQGt>cxCwL{?mhZbKQQKjb%t>; zkdfmI_fQ1)lV}Q7Rn#MI7+e9jx9G+!>Qw-S z^P2jE<-_apXV%a78!gGTbsX%E4yx>%_9F*s0=GbEEF1sf8hLL;5$)y~1e=~s0_0lU z>OuASOt;G+)vNUi#>9rp=oE53!fswTlk zaRc#tY;UGQeqBjEo3~C&)%Gq;&Xx*D!}{5g09ua&Uf^n)zS6POg=1%2c~^9%{Ma|D z*G~?UU=!6G9qc(-9vSan-!&UR&&-3S`nydReIxOzTD|}T4&uM2cCisiC$qWle#~a* zWxnY}I*&hI8Tj6d?N%0aTyI0zPF1$?dMf1U75grZo?@HucCQjk(mD@&-lL2Ry5sMf z(IV|Ou%#i-Gb&TB#@|wyao=;+!G}e>@XfyQ@PI6qGqY8{VAp!m=;NcVdvrkjF0UTy z4)q4{iV-zXD5I7Q3m!iXc zc-6D{(Ji|tawsXT<4eceml`5OCJpo&IJ?b2D`m>EWNPdx_+nNGlQm0f3W?V@3no+{^@+nS^NG_@!a-<8N}G>Emp$} zqg?k>U?8OK@qDVFXq#fWjev=t^3vew(@Jg7xTro+{)bz9DmjeTa<0joFN`#aa~7pi zUi@JsxrX`-sEsrgn5^W=@wzvqmCROG2|3@lA_=!~KnYC{4^~bVLH1{3wyBkYu!!!O zN5KIIUeAMKhp^VuPL*cEkX}nvDb7fs}23pE6w1* zA`M?Q_I3w941O-ZuZ&$;zYqOt*7k9qPHeu(i+$NxRW9Y!xV-`o?Zbi7LWJbxW7Fc z2bCZvC&!WqB^-r33@TD_l;R`3k5R+)y*K1A%>lUm;`*z*khtD3>)zc>lT^tSxpZ|M<{Py}%ya-A*)r@II=`7`!l)a+{W^lSJ#8&kK z2O6thcUH&ZFK~gK>EWrnNFHaqV(ZkH)h5}xFy;tMgGon9P`c`K>J7EWaQSQ!5f_hN zmV>;?L@(D|r%n4C#2X(u@OVMNj9lKIoK$Qdt~W?;p>d6t&SM?gyqjPzJ=N)3chnhM ztgg0AcaO-7NC?t~EPdT)&CEcP613jHb6BY}G^NW7W6kAcPmin2nJe7NoA?|S7N+FT z+A_#@`7-{_J-mbfNayBMqGl0>M4xBa-q^mP?&rY1gx3oSnL_GF&3I23iQ3k{cdZzp zGZ;K5C%Q+P%$#sIGF7f%oo9j2F%%`9Hmp~_I7^7LUUY|J!{;uwp)ce6cY+5UAM|#s zHz?#n(T$dq!`zE!^^xp&++`)=3`KLmpsfQx0EL$&RZ}?Vaf5^H#acm@-gUpBXTD!rxoj1;#hcV@?d&TWZ0_f0a$r33O#T+D^WS5ts~ zBXwLBy;!#Q^z`!X^=`L<7g_;lz`?q2#C8O@L9jy5%+-#f=j%D~R>htvycLJDlL6gDHofo7NZ{Q!~*4`c4 zcIVuT5&#ANPR~Eia_^GZNj-=(IgojFS8sdI=f1gl=JJloVHMhNELz(-4=s1B@o@24 zDe*iByiTnTS*$_Me%sJUU^xost!*J2q=g$ct%&e9o2qX%~^vRC`=HKK-V1|D7_p@BO zM0~Fl0u-n~pg#f;Px9-I0~sSoWxhSZQ+6Z3#HFU5&gvF^y9V>@qV5YUivNGP<9DTO zPW2|y&fHbzf-c4k|K2CYhN{%ypXAMQ?x+e4%ov1(FukY^Z30PjXPgZTF zrCRvC_r1U0vwgWy`T~;Ffg9OEp6gun8Q4BD0>i$)BYk~%rGq}b&*q_mRZCmj{9t!u zvIv@WpP{;JDDC0lv1O82PVWy(3~!Qe91}oqz41{*fdj)VE-$s5}RE`uh8) zCmI9a=H^~kQzMCwk2kFt#k^6B2!kIiONeI{GwuFW~GCd_mA?8s=MZzp{4Q# zW%66MZZ(bjE&k+66>(MYYTs)SXE+00dV^qCC@k$#M8Aj@Jv}`X@FdxjFut2xbw0i2 zqT{ojw{N3;NFi^c$&%k@bIAMQ!^?Z#qy)y^@Uy#C z9BH&NWME*h0r`zAJUlD$epWC&L_tS~il3i9)0d~8;#+-W(f0oRoN=CO=NG37h27@K zK=t(W^dQm{G5Z`aeD3R>&-AT2p)K2VD#K97jHy9ari+bg1uJ3bxkq9(Q@yMo&<8 zXWhH)3+f%`Rl#kY-7^iON3#|%gGBcSgb8=?##*bmbR7WT&ebp~J$#e}#7)pEq zjkdpNNNj2CE&<_}n8ahiUZh(hQwKe`XUfaV!`jBj$9bhTK2dCD%18d%`#slzwm-jH zKhE3V*Y_pz98WwWH09pg9(GC+vLA46NjG_K8Gc%Nb$R);pTqX@AU`s=v=vgnjb0e} z1vxW{2Q&(qnVB-i&|XO#y3nup`yTyH?~s?5--Mp&oPN+aznXg%+PAmEyryS5MaDc* zJ0q?qSMSZ&>3Cm|X6IyM6PLuT$2<0|iHcex zn@}XqyLSXnFT%oN@w0I0Aqe2Fo}OC|V3@$>8NXfG$`2p3#z4f2k87%`L?1vW^-W-v zFYy{xX#uq1V`AjwiD;YI~o!Co5=Nt8WY)4GgukSnHt(>_ye~DSXn;IXO8eU!U~s*_ZvDwWPax&tS6Ag9qK2 z@|#~0FU>#=))(mPn{+VJTX+b`y>VEdA&Z09&*}Ey7w|TSJBH`hHX<3p+Em z^#++FJ1+1ot1BysSf61pi2T%uW2!SNDs;w5tzvwBs0p<@)gP9_V=QkyoPique>Ti# z{rZEMFNyE>k1IUauHPKhp=+p5AtP7tNF?_xlTkhANr;n9h)K}3AU`jpghfcKD|myP z`cw#BddasB@;^T)P-ncBjEf_Rc#MPl)BiOqw&bl`&^6+p^n${opN0B|dH=;P^{jbM zOh8cm%jalexpvHC`_IVu;i2!`SnY=oVbQ0gVd7Z^Z0Buzdv3188+p08C3yo-0h*H& zJlmP30!Iq6niSE7O``Nx-%7@79wbanO>thn9R2hL8yLo$hK6)2BbB8(E6-_%t7&=h z5^^6XYHX^@X=!Q6A@ebQ2U*1qkkF6g-`rtd&jjs_UjN%6>H)Rv}qP4Bcn!RV`Fx=^*jvrJ4X-NNlZECmKpa| z;}jwyqOh>HA3r8%-{Kmr?ti2vlhy5}ACs~;PQ`1S&2{^=rn58O-rv7HFD1PHN)?Cx z>@VAg{Bc5rUgW;qfZ+F~{;|$>d<*(7xRBrIkqMW8A+=tvh!xh}r$T9}Cu_RqlsJhmnzy z?LU6p-YwA1&wcwg3>q{kdhe{VF=s+2tM8x_h8G54{>*((PmVNoMHu)&Uas++UtX7! z+}-dktZe(t{m2*G7uzdQdPx>XU8lAe=HN=o|i_!NHFTMOpN)m3%dvKL2i>?m%wO;Eu~+{lSHezbK5 zCeTrqQ86;|suno9#u}2!PGO8I9;SFQ|2Q1g-&D8!D>|HT#6U?Y zx3I9#GzYPm)DQaa?zF&B;W11A8`(ZQoC4ZFDOW4L(ey%QbVc^PgqU(Rms?7aE@T{FI82AzF+GiA1u z>h|0-wt0uK@UWgAKhWiZRmiQWNe%maansRl_A9m7bti7b7xpjEwdqNEPvl28dC4sUA z!j=TUva$N_UTa7lYVCSvWMpJ(=i}Fs{dx8Et7?X>SKpj}?Du&~y*w_9QK z+r-4gg%4&UYGxNoZL^Crpy8xnY-n{uZKA!w* zst5(lEk0LPVcAYzU|0!ZO&Gl>ovW1zpS+Pn-W+-cA9<^F@o7@h8LxuZf7sTHwmen3 z#7yBF{W$Xd`^V_l^XX01p8%YMU}&Thy<)+i~s*%{3w@Wq%C7#$$CP zyu4g+832##?APmX2+7IGHFb12K3J{#Tyb~Q3#j|`s4bfQ`+^(5duauQkm6CAO;QAO zkerMRHoL}4ZhP;UL>*@sz=|}0p>u{iR!tKv`%wb(hvZ@D;rpDSBXZ3~D=kvM;c@#x zQ9%J(CQM0$Yy5D5ZD4Tl`N4zavbLjFF!+4}fJ4(9j7pHQwzHFk?mMevwUGcLV81OE zdU7(SRijp5QlDx}Yh)wDPfw264y+7|m>4Z6=h%)JPA)FYHi$Trgao}D>w*1GFLmAc zSFbLxva*JiKmU@ETlS>Vb&SS*O=WCpX$cjS)Kofd@M!K=8h&)ty}vv*HYN;+8Mz=N zLWGUNsdJRWk^EcXv04RTKCT!H-T=`mS`O`n>AT zFoYwwySodz=k(;{Bquj_P)Z7|`b%*_Cnu+ay)Hima8XI=g#Cwx^u_mnB@~yH1%v-k zo8CkTR?@mmzh-;L4sNUw=CHE-RrB_0wE&blzjUf-mfpdoV!|3|!@3bvoGGpfS zX}nX=Df5Y0b4W&6+1YX5eV#uLYm3L*+VDYFSvxqiz$`pIkQ3kI)tHFDz*9F=RIWG- z32-Lf=nig~hs0tfb#(9^`)aD z`1+QwQOoMuTFjq6Zb$=+)7e-uIev&_E>y;`C}u*@9@^u;!GQ<$Jz+lZ1Ps_JuBd2( z`uv*)2E>Usg8cxiBK~J2cM%YA$aa_0)_(R@LQG6d9Oid~MMegyzi9^R*A0yd7H_jC zJee=AKl5~Kwzm4napA$*4UND1kT!9Km4jpYa~umaS4gwQwk9d*@9!smz$ob>Z13Q3 z)5r+bXsqn)%p4r}fDbF%Q&f#w3VUrKa2gDvkXcx$&Ux7&qOh<9pOmiQ{oRgNS{FAK z-lP-BU(UG|J~3hH_3&ZSj~^6(ozIpHCj4%>^+#~+&Ye5EU{stB|GIy+_%6dW9^ncu6VqC&9^fCH3Hs;D~=I3+SL_qN@0L66y0#3%N@(?Ls13!eFtr%x$Q zGfL7)9jphErL(n3LSITcNuTpDUB>SU=o~LEuZi3@FJ&G)5FsKaM!Qymx|)Q+=gsX2 z983xAny{$gX)NJ(`5RgEtj@;#+9y*8nz!qC%4L9T%*3@afxefrb> zT7686)Y18?_r6NY%eTl@)I<09RACqFhqVlV=e&;g=JqAKK#2Sg?zyC+tj!GgWVAC#j^Q!S6jc0qq)Ep#{C#YH7%Tgt$07ILWJNaYq{)%<2J;;YTw5=jfRh zzWpV!HSpPcp*{Es0I}7DF z82t_~a;!DdhtLM@{DAZ~KuF)VcF;rQKQmW*Nn&A5CF!!fu5NNWYE`n86Ht->MhQ-X zDNPoCz)wdShsMgL8&do({Sio#W9D0`p3A_CXf#G9JSo1_rdLX=zvPeLW9{>(}foD4x=Bi}O;x z;;^JHOMN`xk-aJ|CWCWtY;4?Qehfps);BgV^hqyXM8G#O1H`hiu>t1f6X;F(V|GqX z0mynRamM1$X34-h;TXii)ISp7}u+xC*Us3;QX=*)9aP?V8@1?+~`(z}Z9 zBO_0sv7RY-_0SN7>~HQyyFU!&H6N>akhu-I01JUFYWmd!2atkYRa#k@5T?5 zav|G-YojPksunry9)HK=p`oDxe(N+ijG*0`KZR4kQDYLH>4i%G22)KDT*(-MX%+Gy zoM`txKfhzeKP&sKKg)EP{dDZts3{+TM)3Ll{ia~z zVDk&6^bdq);IFV?a>~l#gHtA+1xx^E(&3y$L`HV^^|60dUOT>&E@4JeBBcai61w$S z+uMHv>ZW^OfZ)}16)0#7EiExX0AN)kqpw~oH@JT+B(yevd9C%$qtepSxM$BoV9v8? zQxIWO83ut?$x$#rA>-r6-Hf@Qz;-QC#%DQgW5YE&L2PZSb913+G5stQGF_<2Y}<{1 z3UFUqlsXCzzpklC=CM406{Qe(aNq+xtZY$#7EmrfUsoPIn)*a&KU#GbFAxjj4gi^& zF1g{UIQ6drpguvEC5PZ4wPwhM2GO!KOca{%xT|;KO82$H^MED9tu!Rn(b1tidzR_) zWl|<4rhyMO6vn=LcVP-(762D;4%Ywtk<-;Z?>JUt^Il?ldI7zsCr>^B2A!Om!cJ4) zQF@8Exw!#nKnY+w5++_uf+O1usA>a7VlV((0FtF6GgsT zmwxv3O#u^@L>5|6Ap{-;86DkeB(p7%=Z?HQE)1=@;=ZV*#7eHj8n3SL$!*6~9qi}u zFs7z~+eK-2Q;h0LFrr48k=BunPLB1oA@V6crCNedh72pj>FNBF5`lk zRDA!pZ#iMy9H#zYhSz0%;<{OP3iL7pj>8l@m*lHfq;NIx6C)2{>I%g+nD)l8F$RF; zKEGVV3jm@3E7b&J&L@_kTRN?f$Hmo@9>(cr=|#1%7)B{luG_vN$CZ{=rCktrY^2}x1Ycw94!0Jc0RBW>m&1a+9XZ(Z5sXDW8nuW zuvl(2Re1jEFNLSQiK344wezodEbGe2%QrSRpK_|(V+We;D8-!h?zBe6E{~x}i6QR>zhOnohHO zHO$O)5Cq;KE8{YJ5w zaYPb!!)LRd!`a2l*8M)80jlh@e{h$juL`Xd=H)fLy_tnJmm+DZ zSoE8nrI+fjIW}}2Q`-j=yAFhOMBjH%ld9n{O^_E{R3pJxYGZAHTtFy zKJ>2s*Xx2oE?BYuUE1PF2@_oXe=hZe#0 Date: Mon, 27 Jul 2020 11:58:11 +0200 Subject: [PATCH 30/96] Remove flaky note from gauge tests (#73240) --- test/functional/apps/visualize/_gauge_chart.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/functional/apps/visualize/_gauge_chart.js b/test/functional/apps/visualize/_gauge_chart.js index aa94e596319c2..0f870b1fb545f 100644 --- a/test/functional/apps/visualize/_gauge_chart.js +++ b/test/functional/apps/visualize/_gauge_chart.js @@ -26,7 +26,6 @@ export default function ({ getService, getPageObjects }) { const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); - // FLAKY: https://github.com/elastic/kibana/issues/45089 describe('gauge chart', function indexPatternCreation() { async function initGaugeVis() { log.debug('navigateToApp visualize'); From 9d5b1bf20b5c5f48f9b9c6e4b8bfaee426e6f364 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 27 Jul 2020 12:10:16 +0100 Subject: [PATCH 31/96] simplified buffer tests to reduce flakyness (#73024) Co-authored-by: Elastic Machine --- .../server/lib/bulk_operation_buffer.test.ts | 80 ++++++++----------- 1 file changed, 32 insertions(+), 48 deletions(-) diff --git a/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.test.ts b/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.test.ts index 3a21f622cec17..f32a755515a95 100644 --- a/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.test.ts +++ b/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.test.ts @@ -33,8 +33,7 @@ function errorAttempts(task: TaskInstance): Err { +describe('Bulk Operation Buffer', () => { describe('createBuffer()', () => { test('batches up multiple Operation calls', async () => { const bulkUpdate: jest.Mocked> = jest.fn( @@ -67,8 +66,6 @@ describe.skip('Bulk Operation Buffer', () => { const task2 = createTask(); const task3 = createTask(); const task4 = createTask(); - const task5 = createTask(); - const task6 = createTask(); return new Promise((resolve) => { Promise.all([bufferedUpdate(task1), bufferedUpdate(task2)]).then((_) => { @@ -79,22 +76,18 @@ describe.skip('Bulk Operation Buffer', () => { setTimeout(() => { // on next tick - setTimeout(() => { - // on next tick - expect(bulkUpdate).toHaveBeenCalledTimes(2); - Promise.all([bufferedUpdate(task5), bufferedUpdate(task6)]).then((_) => { - expect(bulkUpdate).toHaveBeenCalledTimes(3); - expect(bulkUpdate).toHaveBeenCalledWith([task5, task6]); - resolve(); - }); - }, bufferMaxDuration + 1); - expect(bulkUpdate).toHaveBeenCalledTimes(1); Promise.all([bufferedUpdate(task3), bufferedUpdate(task4)]).then((_) => { expect(bulkUpdate).toHaveBeenCalledTimes(2); expect(bulkUpdate).toHaveBeenCalledWith([task3, task4]); }); - }, bufferMaxDuration + 1); + + setTimeout(() => { + // on next tick + expect(bulkUpdate).toHaveBeenCalledTimes(2); + resolve(); + }, bufferMaxDuration * 1.1); + }, bufferMaxDuration * 1.1); }); }); @@ -103,8 +96,9 @@ describe.skip('Bulk Operation Buffer', () => { return Promise.resolve(tasks.map(incrementAttempts)); }); + const bufferMaxDuration = 1000; const bufferedUpdate = createBuffer(bulkUpdate, { - bufferMaxDuration: 100, + bufferMaxDuration, bufferMaxOperations: 2, }); @@ -114,26 +108,19 @@ describe.skip('Bulk Operation Buffer', () => { const task4 = createTask(); const task5 = createTask(); - return new Promise((resolve) => { - bufferedUpdate(task1); - bufferedUpdate(task2); - bufferedUpdate(task3); - bufferedUpdate(task4); - - setTimeout(() => { - expect(bulkUpdate).toHaveBeenCalledTimes(2); - expect(bulkUpdate).toHaveBeenCalledWith([task1, task2]); - expect(bulkUpdate).toHaveBeenCalledWith([task3, task4]); - - setTimeout(() => { - expect(bulkUpdate).toHaveBeenCalledTimes(2); - bufferedUpdate(task5).then((_) => { - expect(bulkUpdate).toHaveBeenCalledTimes(3); - expect(bulkUpdate).toHaveBeenCalledWith([task5]); - resolve(); - }); - }, 50); - }, 50); + return Promise.all([ + bufferedUpdate(task1), + bufferedUpdate(task2), + bufferedUpdate(task3), + bufferedUpdate(task4), + ]).then(() => { + expect(bulkUpdate).toHaveBeenCalledTimes(2); + expect(bulkUpdate).toHaveBeenCalledWith([task1, task2]); + expect(bulkUpdate).toHaveBeenCalledWith([task3, task4]); + return bufferedUpdate(task5).then((_) => { + expect(bulkUpdate).toHaveBeenCalledTimes(3); + expect(bulkUpdate).toHaveBeenCalledWith([task5]); + }); }); }); @@ -153,29 +140,26 @@ describe.skip('Bulk Operation Buffer', () => { const task3 = createTask(); const task4 = createTask(); - return new Promise((resolve) => { - bufferedUpdate(task1); - bufferedUpdate(task2); - - setTimeout(() => { - expect(bulkUpdate).toHaveBeenCalledTimes(1); - expect(bulkUpdate).toHaveBeenCalledWith([task1, task2]); + return Promise.all([bufferedUpdate(task1), bufferedUpdate(task2)]).then(() => { + expect(bulkUpdate).toHaveBeenCalledTimes(1); + expect(bulkUpdate).toHaveBeenCalledWith([task1, task2]); - bufferedUpdate(task3); - bufferedUpdate(task4); + return new Promise((resolve) => { + const futureUpdates = Promise.all([bufferedUpdate(task3), bufferedUpdate(task4)]); setTimeout(() => { expect(bulkUpdate).toHaveBeenCalledTimes(1); - setTimeout(() => { + futureUpdates.then(() => { expect(bulkUpdate).toHaveBeenCalledTimes(2); expect(bulkUpdate).toHaveBeenCalledWith([task3, task4]); resolve(); - }, bufferMaxDuration / 2); + }); }, bufferMaxDuration / 2); - }, bufferMaxDuration + 1); + }); }); }); + test('handles both resolutions and rejections at individual task level', async (done) => { const bulkUpdate: jest.Mocked> = jest.fn( ([task1, task2, task3]) => { From d3ddcd2027442dd11136b4307f65ba4e693654de Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Mon, 27 Jul 2020 08:18:36 -0500 Subject: [PATCH 32/96] [APM] APM & Observability plugin lint improvements (#72702) * [APM] APM & Observability plugin lint improvements This is a large change, but most of it is automatic `eslint --fix` changes. * Apply the same ESLint ovderrides in APM and Observability plugins. * Remove the `no-unused-vars` rule. We can turn on the TypeScript check if needed. * Check both JS and TS files. * Add a rule for react function component definitions * Upgrade eslint-plugin-react to include that rule --- .eslintrc.js | 21 +-- package.json | 2 +- .../views/data/components/data_view.test.tsx | 4 +- .../plugins/apm/e2e/cypress/plugins/index.js | 2 + .../plugins/apm/public/application/index.tsx | 10 +- .../app/ErrorGroupOverview/List/index.tsx | 4 +- .../app/ErrorGroupOverview/index.tsx | 4 +- .../Breakdowns/BreakdownFilter.tsx | 6 +- .../Breakdowns/BreakdownGroup.tsx | 6 +- .../app/RumDashboard/ChartWrapper/index.tsx | 9 +- .../RumDashboard/Charts/PageLoadDistChart.tsx | 1 + .../Charts/VisitorBreakdownChart.tsx | 4 +- .../PageLoadDistribution/BreakdownSeries.tsx | 8 +- .../PercentileAnnotations.tsx | 10 +- .../PageLoadDistribution/index.tsx | 4 +- .../app/RumDashboard/PageViewsTrend/index.tsx | 4 +- .../app/RumDashboard/RumDashboard.tsx | 4 +- .../app/RumDashboard/RumHeader/index.tsx | 24 ++-- .../RumDashboard/VisitorBreakdown/index.tsx | 4 +- .../app/ServiceMap/EmptyBanner.test.tsx | 36 +++-- .../app/ServiceMap/LoadingOverlay.tsx | 48 +++---- .../app/ServiceMap/Popover/Contents.tsx | 11 +- .../components/app/ServiceMap/index.test.tsx | 6 +- .../components/app/ServiceMap/index.tsx | 4 +- .../app/ServiceNodeOverview/index.tsx | 4 +- .../AgentConfigurations/List/index.tsx | 4 +- .../CustomLink/CreateCustomLinkButton.tsx | 22 ++- .../CustomLinkFlyout/Documentation.tsx | 16 ++- .../CustomLinkFlyout/FiltersSection.tsx | 38 ++--- .../CustomLinkFlyout/FlyoutFooter.tsx | 6 +- .../CustomLinkFlyout/LinkPreview.tsx | 4 +- .../CustomLinkFlyout/LinkSection.tsx | 9 +- .../CustomLink/CustomLinkFlyout/index.tsx | 6 +- .../CustomLink/CustomLinkTable.tsx | 39 +++--- .../CustomizeUI/CustomLink/EmptyPrompt.tsx | 6 +- .../Settings/CustomizeUI/CustomLink/Title.tsx | 62 +++++---- .../Settings/CustomizeUI/CustomLink/index.tsx | 4 +- .../app/Settings/CustomizeUI/index.tsx | 4 +- .../anomaly_detection/add_environments.tsx | 6 +- .../app/Settings/anomaly_detection/index.tsx | 4 +- .../Settings/anomaly_detection/jobs_list.tsx | 4 +- .../public/components/app/Settings/index.tsx | 6 +- .../public/components/app/TraceLink/index.tsx | 4 +- .../TransactionDetails/Distribution/index.tsx | 10 +- .../WaterfallWithSummmary/ErrorCount.tsx | 38 ++--- .../SpanFlyout/TruncateHeightSection.tsx | 10 +- .../Waterfall/WaterfallFlyout.tsx | 7 +- .../Waterfall/WaterfallItem.tsx | 10 +- .../WaterfallContainer/Waterfall/index.tsx | 10 +- .../WaterfallWithSummmary/index.tsx | 6 +- .../components/shared/ApmHeader/index.tsx | 42 +++--- .../DatePicker/__test__/DatePicker.test.tsx | 30 ++-- .../public/components/shared/EmptyMessage.tsx | 6 +- .../shared/EnvironmentBadge/index.tsx | 4 +- .../shared/EnvironmentFilter/index.tsx | 4 +- .../public/components/shared/EuiTabLink.tsx | 4 +- .../shared/HeightRetainer/index.tsx | 9 +- .../components/shared/KueryBar/index.tsx | 1 - .../components/shared/LicensePrompt/index.tsx | 4 +- .../Links/DiscoverLinks/DiscoverErrorLink.tsx | 13 +- .../Links/DiscoverLinks/DiscoverSpanLink.tsx | 12 +- .../DiscoverLinks/DiscoverTransactionLink.tsx | 12 +- .../Links/MachineLearningLinks/MLJobLink.tsx | 9 +- .../shared/Links/apm/ErrorDetailLink.tsx | 4 +- .../shared/Links/apm/ErrorOverviewLink.tsx | 4 +- .../components/shared/Links/apm/HomeLink.tsx | 4 +- .../shared/Links/apm/MetricOverviewLink.tsx | 4 +- .../shared/Links/apm/ServiceMapLink.tsx | 4 +- .../apm/ServiceNodeMetricOverviewLink.tsx | 6 +- .../Links/apm/ServiceNodeOverviewLink.tsx | 4 +- .../shared/Links/apm/ServiceOverviewLink.tsx | 4 +- .../shared/Links/apm/SettingsLink.tsx | 4 +- .../shared/Links/apm/TraceOverviewLink.tsx | 4 +- .../Links/apm/TransactionDetailLink.tsx | 6 +- .../Links/apm/TransactionOverviewLink.tsx | 4 +- .../LocalUIFilters/Filter/FilterBadgeList.tsx | 40 +++--- .../Filter/FilterTitleButton.tsx | 4 +- .../shared/LocalUIFilters/Filter/index.tsx | 11 +- .../ServiceNameFilter/index.tsx | 4 +- .../TransactionTypeFilter/index.tsx | 4 +- .../shared/LocalUIFilters/index.tsx | 6 +- .../components/shared/MetadataTable/index.tsx | 32 +++-- .../shared/SelectWithPlaceholder/index.tsx | 1 + .../PopoverExpression/index.tsx | 4 +- .../shared/Stacktrace/FrameHeading.tsx | 4 +- .../shared/Stacktrace/Variables.tsx | 4 +- .../shared/Summary/DurationSummaryItem.tsx | 8 +- .../Summary/ErrorCountSummaryItemBadge.tsx | 4 +- .../shared/Summary/TransactionSummary.tsx | 12 +- .../components/shared/Summary/index.tsx | 4 +- .../CustomLink/CustomLinkPopover.tsx | 6 +- .../CustomLink/CustomLinkSection.tsx | 42 +++--- .../CustomLink/ManageCustomLink.tsx | 74 +++++----- .../CustomLink/index.tsx | 6 +- .../TransactionActionMenu.tsx | 34 ++--- .../TransactionBreakdownGraph/index.tsx | 4 +- .../TransactionBreakdownKpiList.tsx | 11 +- .../shared/TransactionBreakdown/index.tsx | 4 +- .../charts/CustomPlot/AnnotationsPlot.tsx | 4 +- .../ErroneousTransactionsRateChart/index.tsx | 4 +- .../components/shared/charts/Legend/index.tsx | 6 +- .../charts/Timeline/Marker/AgentMarker.tsx | 4 +- .../charts/Timeline/Marker/ErrorMarker.tsx | 4 +- .../shared/charts/Timeline/Marker/index.tsx | 4 +- .../shared/charts/Timeline/TimelineAxis.tsx | 6 +- .../shared/charts/Timeline/VerticalLines.tsx | 6 +- .../ChoroplethMap/ChoroplethToolTip.tsx | 10 +- .../TransactionCharts/ChoroplethMap/index.tsx | 4 +- .../DurationByCountryMap/index.tsx | 4 +- .../TransactionLineChart/index.tsx | 4 +- .../apm/public/context/ChartsSyncContext.tsx | 6 +- .../MockUrlParamsContextProvider.tsx | 6 +- .../public/utils/getRangeFromTimeSeries.ts | 4 +- .../plugins/apm/public/utils/testHelpers.tsx | 8 +- .../public/application/index.tsx | 4 +- .../components/app/chart_container/index.tsx | 6 +- .../components/app/empty_section/index.tsx | 4 +- .../public/components/app/header/index.tsx | 6 +- .../app/ingest_manager_panel/index.tsx | 4 +- .../components/app/layout/with_header.tsx | 30 ++-- .../public/components/app/news_feed/index.tsx | 8 +- .../public/components/app/resources/index.tsx | 4 +- .../components/app/section/alerts/index.tsx | 4 +- .../components/app/section/apm/index.tsx | 6 +- .../app/section/error_panel/index.tsx | 4 +- .../public/components/app/section/index.tsx | 4 +- .../components/app/section/logs/index.tsx | 6 +- .../components/app/section/metrics/index.tsx | 12 +- .../components/app/section/uptime/index.tsx | 12 +- .../components/app/styled_stat/index.tsx | 4 +- .../components/shared/action_menu/index.tsx | 74 +++++----- .../components/shared/data_picker/index.tsx | 4 +- .../observability/public/pages/home/index.tsx | 4 +- .../public/pages/landing/index.tsx | 4 +- .../public/pages/overview/index.tsx | 8 +- .../pages/overview/loading_observability.tsx | 4 +- .../public/typings/eui_styled_components.tsx | 24 ++-- .../components/rules/mitre/index.tsx | 2 +- .../tags_filter_popover.tsx | 1 + yarn.lock | 130 +++++++++++------- 140 files changed, 824 insertions(+), 733 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index e2674e8d7b407..c9f9d96f9ddae 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -771,19 +771,22 @@ module.exports = { }, /** - * APM overrides + * APM and Observability overrides */ { - files: ['x-pack/plugins/apm/**/*.js'], + files: [ + 'x-pack/plugins/apm/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/observability/**/*.{js,mjs,ts,tsx}', + ], rules: { - 'no-unused-vars': ['error', { ignoreRestSiblings: true }], 'no-console': ['warn', { allow: ['error'] }], - }, - }, - { - plugins: ['react-hooks'], - files: ['x-pack/plugins/apm/**/*.{ts,tsx}'], - rules: { + 'react/function-component-definition': [ + 'warn', + { + namedComponents: 'function-declaration', + unnamedComponents: 'arrow-function', + }, + ], 'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks 'react-hooks/exhaustive-deps': ['error', { additionalHooks: '^useFetcher$' }], }, diff --git a/package.json b/package.json index 594f0ce583987..ee91c59a8fda6 100644 --- a/package.json +++ b/package.json @@ -435,7 +435,7 @@ "eslint-plugin-node": "^11.0.0", "eslint-plugin-prefer-object-spread": "^1.2.1", "eslint-plugin-prettier": "^3.1.3", - "eslint-plugin-react": "^7.17.0", + "eslint-plugin-react": "^7.20.3", "eslint-plugin-react-hooks": "^4.0.4", "eslint-plugin-react-perf": "^3.2.3", "exit-hook": "^2.2.0", diff --git a/src/plugins/inspector/public/views/data/components/data_view.test.tsx b/src/plugins/inspector/public/views/data/components/data_view.test.tsx index 2772069d36877..bd78bca42c479 100644 --- a/src/plugins/inspector/public/views/data/components/data_view.test.tsx +++ b/src/plugins/inspector/public/views/data/components/data_view.test.tsx @@ -51,13 +51,13 @@ describe('Inspector Data View', () => { }); it('should render loading state', () => { - const component = mountWithIntl(); + const component = mountWithIntl(); // eslint-disable-line react/jsx-pascal-case expect(component).toMatchSnapshot(); }); it('should render empty state', async () => { - const component = mountWithIntl(); + const component = mountWithIntl(); // eslint-disable-line react/jsx-pascal-case const tabularLoader = Promise.resolve(null); adapters.data.setTabularLoader(() => tabularLoader); await tabularLoader; diff --git a/x-pack/plugins/apm/e2e/cypress/plugins/index.js b/x-pack/plugins/apm/e2e/cypress/plugins/index.js index 540b887d55df5..c5529c747adcd 100644 --- a/x-pack/plugins/apm/e2e/cypress/plugins/index.js +++ b/x-pack/plugins/apm/e2e/cypress/plugins/index.js @@ -29,6 +29,8 @@ module.exports = (on) => { // readFileMaybe on('task', { + // ESLint thinks this is a react component for some reason. + // eslint-disable-next-line react/function-component-definition readFileMaybe(filename) { if (fs.existsSync(filename)) { return fs.readFileSync(filename, 'utf8'); diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx index c39afe6da215e..0c9c6eb86225b 100644 --- a/x-pack/plugins/apm/public/application/index.tsx +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -37,7 +37,7 @@ const MainContainer = styled.div` height: 100%; `; -const App = () => { +function App() { const [darkMode] = useUiSetting$('theme:darkMode'); return ( @@ -59,9 +59,9 @@ const App = () => { ); -}; +} -const ApmAppRoot = ({ +function ApmAppRoot({ core, deps, routerHistory, @@ -71,7 +71,7 @@ const ApmAppRoot = ({ deps: ApmPluginSetupDeps; routerHistory: typeof history; config: ConfigSchema; -}) => { +}) { const i18nCore = core.i18n; const plugins = deps; const apmPluginContextValue = { @@ -111,7 +111,7 @@ const ApmAppRoot = ({ ); -}; +} /** * This module is rendered asynchronously in the Kibana platform. diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx index 1096c0c77db30..5c16bf0f324be 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx @@ -53,7 +53,7 @@ interface Props { items: ErrorGroupListAPIResponse; } -const ErrorGroupList: React.FC = (props) => { +function ErrorGroupList(props: Props) { const { items } = props; const { urlParams } = useUrlParams(); const { serviceName } = urlParams; @@ -213,6 +213,6 @@ const ErrorGroupList: React.FC = (props) => { sortItems={false} /> ); -}; +} export { ErrorGroupList }; diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx index b9a28c1c1841f..fe2303d645ec9 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx @@ -22,7 +22,7 @@ import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { ErrorDistribution } from '../ErrorGroupDetails/Distribution'; import { ErrorGroupList } from './List'; -const ErrorGroupOverview: React.FC = () => { +function ErrorGroupOverview() { const { urlParams, uiFilters } = useUrlParams(); const { serviceName, start, end, sortField, sortDirection } = urlParams; @@ -123,6 +123,6 @@ const ErrorGroupOverview: React.FC = () => { ); -}; +} export { ErrorGroupOverview }; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownFilter.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownFilter.tsx index 332cf40a465f9..7e5e7cdc53c55 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownFilter.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownFilter.tsx @@ -20,11 +20,11 @@ interface Props { onBreakdownChange: (values: BreakdownItem[]) => void; } -export const BreakdownFilter = ({ +export function BreakdownFilter({ id, selectedBreakdowns, onBreakdownChange, -}: Props) => { +}: Props) { const categories: BreakdownItem[] = [ { name: 'Browser', @@ -65,4 +65,4 @@ export const BreakdownFilter = ({ }} /> ); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownGroup.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownGroup.tsx index 5bf84b6c918c5..d4f80667ce98b 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownGroup.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownGroup.tsx @@ -22,12 +22,12 @@ export interface BreakdownGroupProps { onChange: (values: BreakdownItem[]) => void; } -export const BreakdownGroup = ({ +export function BreakdownGroup({ id, disabled, onChange, items, -}: BreakdownGroupProps) => { +}: BreakdownGroupProps) { const [isOpen, setIsOpen] = useState(false); const [activeItems, setActiveItems] = useState(items); @@ -97,4 +97,4 @@ export const BreakdownGroup = ({ ); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ChartWrapper/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ChartWrapper/index.tsx index a3cfbb28abee2..970365779a0a2 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ChartWrapper/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ChartWrapper/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, HTMLAttributes } from 'react'; +import React, { HTMLAttributes, ReactNode } from 'react'; import { EuiErrorBoundary, EuiFlexGroup, @@ -13,6 +13,7 @@ import { } from '@elastic/eui'; interface Props { + children?: ReactNode; /** * Height for the chart */ @@ -27,12 +28,12 @@ interface Props { 'aria-label'?: string; } -export const ChartWrapper: FC = ({ +export function ChartWrapper({ loading = false, height = '100%', children, ...rest -}) => { +}: Props) { const opacity = loading === true ? 0.3 : 1; return ( @@ -60,4 +61,4 @@ export const ChartWrapper: FC = ({ )} ); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx index 6c5b539fcecfa..b2b5e66d06ac6 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx @@ -70,6 +70,7 @@ export function PageLoadDistChart({ onPercentileChange(minX, maxX); }; + // eslint-disable-next-line react/function-component-definition const headerFormatter: TooltipValueFormatter = (tooltip: TooltipValue) => { return (
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx index 1e28fde4aa2b4..9f9ffdf7168b8 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx @@ -29,7 +29,7 @@ interface Props { }>; } -export const VisitorBreakdownChart = ({ options }: Props) => { +export function VisitorBreakdownChart({ options }: Props) { const [darkMode] = useUiSetting$('theme:darkMode'); return ( @@ -93,4 +93,4 @@ export const VisitorBreakdownChart = ({ options }: Props) => { ); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/BreakdownSeries.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/BreakdownSeries.tsx index 0c47ad24128ef..475a235ef5eed 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/BreakdownSeries.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/BreakdownSeries.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useEffect } from 'react'; import { CurveType, LineSeries, ScaleType } from '@elastic/charts'; +import React, { useEffect } from 'react'; import { PercentileRange } from './index'; import { useBreakdowns } from './use_breakdowns'; @@ -16,12 +16,12 @@ interface Props { onLoadingChange: (loading: boolean) => void; } -export const BreakdownSeries: FC = ({ +export function BreakdownSeries({ field, value, percentileRange, onLoadingChange, -}) => { +}: Props) { const { data, status } = useBreakdowns({ field, value, @@ -47,4 +47,4 @@ export const BreakdownSeries: FC = ({ ))} ); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/PercentileAnnotations.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/PercentileAnnotations.tsx index 9066dd73159b1..407ec42f03ff5 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/PercentileAnnotations.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/PercentileAnnotations.tsx @@ -33,7 +33,7 @@ const PercentileMarker = styled.span` bottom: 205px; `; -export const PercentileAnnotations = ({ percentiles }: Props) => { +export function PercentileAnnotations({ percentiles }: Props) { const dataValues = generateAnnotationData(percentiles) ?? []; const style: Partial = { @@ -44,17 +44,17 @@ export const PercentileAnnotations = ({ percentiles }: Props) => { }, }; - const PercentileTooltip = ({ + function PercentileTooltip({ annotation, }: { annotation: LineAnnotationDatum; - }) => { + }) { return ( {annotation.details}th Percentile ); - }; + } return ( <> @@ -82,4 +82,4 @@ export const PercentileAnnotations = ({ percentiles }: Props) => { ))} ); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx index adeff2b31fd93..c7545ff9a2764 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx @@ -24,7 +24,7 @@ export interface PercentileRange { max?: number | null; } -export const PageLoadDistribution = () => { +export function PageLoadDistribution() { const { urlParams, uiFilters } = useUrlParams(); const { start, end, serviceName } = urlParams; @@ -115,4 +115,4 @@ export const PageLoadDistribution = () => { />
); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx index c6ef319f8a666..0f43c0ddf540d 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx @@ -13,7 +13,7 @@ import { BreakdownFilter } from '../Breakdowns/BreakdownFilter'; import { PageViewsChart } from '../Charts/PageViewsChart'; import { BreakdownItem } from '../../../../../typings/ui_filters'; -export const PageViewsTrend = () => { +export function PageViewsTrend() { const { urlParams, uiFilters } = useUrlParams(); const { start, end, serviceName } = urlParams; @@ -68,4 +68,4 @@ export const PageViewsTrend = () => {
); -}; +} 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 2eb79257334d7..8c8164972328f 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx @@ -18,7 +18,7 @@ import { PageLoadDistribution } from './PageLoadDistribution'; import { I18LABELS } from './translations'; import { VisitorBreakdown } from './VisitorBreakdown'; -export const RumDashboard = () => { +export function RumDashboard() { return ( @@ -54,4 +54,4 @@ export const RumDashboard = () => { ); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHeader/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHeader/index.tsx index b1ff38fdd2d79..6b3fcb3b03466 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHeader/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHeader/index.tsx @@ -5,16 +5,18 @@ */ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React from 'react'; +import React, { ReactNode } from 'react'; import { DatePicker } from '../../../shared/DatePicker'; -export const RumHeader: React.FC = ({ children }) => ( - <> - - {children} - - - - - -); +export function RumHeader({ children }: { children: ReactNode }) { + return ( + <> + + {children} + + + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx index 2e17e27587b63..5c68ebb1667ab 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx @@ -11,7 +11,7 @@ import { VisitorBreakdownLabel } from '../translations'; import { useFetcher } from '../../../../hooks/useFetcher'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -export const VisitorBreakdown = () => { +export function VisitorBreakdown() { const { urlParams, uiFilters } = useUrlParams(); const { start, end, serviceName } = urlParams; @@ -62,4 +62,4 @@ export const VisitorBreakdown = () => { ); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.test.tsx index b330129f83785..f314fbbb1fba0 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.test.tsx @@ -4,32 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FunctionComponent } from 'react'; import { act, wait } from '@testing-library/react'; import cytoscape from 'cytoscape'; -import { CytoscapeContext } from './Cytoscape'; -import { EmptyBanner } from './EmptyBanner'; +import React, { ReactNode } from 'react'; import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; import { renderWithTheme } from '../../../utils/testHelpers'; +import { CytoscapeContext } from './Cytoscape'; +import { EmptyBanner } from './EmptyBanner'; const cy = cytoscape({}); -const wrapper: FunctionComponent = ({ children }) => ( - - {children} - -); +function wrapper({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ); +} describe('EmptyBanner', () => { describe('when cy is undefined', () => { it('renders null', () => { - const noCytoscapeWrapper: FunctionComponent = ({ children }) => ( - - - {children} - - - ); + function noCytoscapeWrapper({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ); + } const component = renderWithTheme(, { wrapper: noCytoscapeWrapper, }); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/LoadingOverlay.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/LoadingOverlay.tsx index 9e805058e8cb5..8557c3f0c0798 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/LoadingOverlay.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/LoadingOverlay.tsx @@ -34,26 +34,28 @@ interface Props { percentageLoaded: number; } -export const LoadingOverlay = ({ isLoading, percentageLoaded }: Props) => ( - - {isLoading && ( - - - - - - - {i18n.translate('xpack.apm.loadingServiceMap', { - defaultMessage: - 'Loading service map... This might take a short while.', - })} - - - )} - -); +export function LoadingOverlay({ isLoading, percentageLoaded }: Props) { + return ( + + {isLoading && ( + + + + + + + {i18n.translate('xpack.apm.loadingServiceMap', { + defaultMessage: + 'Loading service map... This might take a short while.', + })} + + + )} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx index 78466b2659bb7..4911d7f147d7c 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx @@ -34,20 +34,21 @@ interface ContentsProps { // @ts-ignore `documentMode` is not recognized as a valid property of `document`. const isIE11 = !!window.MSInputMethodContext && !!document.documentMode; -const FlexColumnGroup = (props: { +function FlexColumnGroup(props: { children: React.ReactNode; style: React.CSSProperties; direction: 'column'; gutterSize: 's'; -}) => { +}) { if (isIE11) { const { direction, gutterSize, ...rest } = props; return
; } return ; -}; -const FlexColumnItem = (props: { children: React.ReactNode }) => - isIE11 ?
: ; +} +function FlexColumnItem(props: { children: React.ReactNode }) { + return isIE11 ?
: ; +} export function Contents({ selectedNodeData, diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx index f36b94f2971cd..4a56f75b05de9 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx @@ -5,7 +5,7 @@ */ import { render } from '@testing-library/react'; -import React, { FunctionComponent } from 'react'; +import React, { ReactNode } from 'react'; import { License } from '../../../../../licensing/common/license'; import { LicenseContext } from '../../../context/LicenseContext'; import { ServiceMap } from './'; @@ -22,13 +22,13 @@ const expiredLicense = new License({ }, }); -const Wrapper: FunctionComponent = ({ children }) => { +function Wrapper({ children }: { children?: ReactNode }) { return ( {children} ); -}; +} describe('ServiceMap', () => { describe('with an inactive license', () => { diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index 7f3d25efa6f44..d4be4da2ae1c5 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -29,7 +29,7 @@ interface ServiceMapProps { serviceName?: string; } -export const ServiceMap = ({ serviceName }: ServiceMapProps) => { +export function ServiceMap({ serviceName }: ServiceMapProps) { const theme = useTheme(); const license = useLicense(); const { urlParams } = useUrlParams(); @@ -101,4 +101,4 @@ export const ServiceMap = ({ serviceName }: ServiceMapProps) => { ); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx index 62ea3bc42860a..5537a73d228e8 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx @@ -36,7 +36,7 @@ const ServiceNodeName = styled.div` ${truncate(px(8 * unit))} `; -const ServiceNodeOverview = () => { +function ServiceNodeOverview() { const { uiFilters, urlParams } = useUrlParams(); const { serviceName, start, end } = urlParams; @@ -182,6 +182,6 @@ const ServiceNodeOverview = () => { ); -}; +} export { ServiceNodeOverview }; diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx index 0f23e230733b4..ce325a57426f5 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx @@ -38,7 +38,7 @@ interface Props { refetch: () => void; } -export const AgentConfigurationList = ({ status, data, refetch }: Props) => { +export function AgentConfigurationList({ status, data, refetch }: Props) { const theme = useTheme(); const [configToBeDeleted, setConfigToBeDeleted] = useState( null @@ -219,4 +219,4 @@ export const AgentConfigurationList = ({ status, data, refetch }: Props) => { /> ); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx index 919cc4debe4d8..2e860ebe22c0f 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx @@ -7,15 +7,13 @@ import React from 'react'; import { EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -export const CreateCustomLinkButton = ({ - onClick, -}: { - onClick: () => void; -}) => ( - - {i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.createCustomLink', - { defaultMessage: 'Create custom link' } - )} - -); +export function CreateCustomLinkButton({ onClick }: { onClick: () => void }) { + return ( + + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.createCustomLink', + { defaultMessage: 'Create custom link' } + )} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/Documentation.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/Documentation.tsx index 48a0288f11ae5..262d22be25272 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/Documentation.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/Documentation.tsx @@ -9,8 +9,14 @@ import { ElasticDocsLink } from '../../../../../shared/Links/ElasticDocsLink'; interface Props { label: string; } -export const Documentation = ({ label }: Props) => ( - - {label} - -); +export function Documentation({ label }: Props) { + return ( + + {label} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FiltersSection.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FiltersSection.tsx index daadc1bace9c4..8cf0f03175fc2 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FiltersSection.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FiltersSection.tsx @@ -26,13 +26,13 @@ import { getSelectOptions, } from './helper'; -export const FiltersSection = ({ +export function FiltersSection({ filters, onChangeFilters, }: { filters: Filter[]; onChangeFilters: (filters: Filter[]) => void; -}) => { +}) { const onChangeFilter = ( key: Filter['key'], value: Filter['value'], @@ -147,25 +147,27 @@ export const FiltersSection = ({ /> ); -}; +} -const AddFilterButton = ({ +function AddFilterButton({ onClick, isDisabled, }: { onClick: () => void; isDisabled: boolean; -}) => ( - - {i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.flyout.filters.addAnotherFilter', - { - defaultMessage: 'Add another filter', - } - )} - -); +}) { + return ( + + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyout.filters.addAnotherFilter', + { + defaultMessage: 'Add another filter', + } + )} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx index 4fde75602990c..17c3fb265bca5 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx @@ -14,7 +14,7 @@ import { import { i18n } from '@kbn/i18n'; import { DeleteButton } from './DeleteButton'; -export const FlyoutFooter = ({ +export function FlyoutFooter({ onClose, isSaving, onDelete, @@ -26,7 +26,7 @@ export const FlyoutFooter = ({ onDelete: () => void; customLinkId?: string; isSaveButtonEnabled: boolean; -}) => { +}) { return ( @@ -61,4 +61,4 @@ export const FlyoutFooter = ({ ); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.tsx index b229157d1b1a8..b7250bda30966 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.tsx @@ -41,7 +41,7 @@ const fetchTransaction = debounce( const getTextColor = (value?: string) => (value ? 'default' : 'subdued'); -export const LinkPreview = ({ label, url, filters }: Props) => { +export function LinkPreview({ label, url, filters }: Props) { const [transaction, setTransaction] = useState(); useEffect(() => { @@ -128,4 +128,4 @@ export const LinkPreview = ({ label, url, filters }: Props) => { ); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkSection.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkSection.tsx index 6a31752d11705..49307cbb8efba 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkSection.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkSection.tsx @@ -31,12 +31,7 @@ interface Props { onChangeUrl: (url: string) => void; } -export const LinkSection = ({ - label, - onChangeLabel, - url, - onChangeUrl, -}: Props) => { +export function LinkSection({ label, onChangeLabel, url, onChangeUrl }: Props) { const inputFields: InputField[] = [ { name: 'label', @@ -145,4 +140,4 @@ export const LinkSection = ({ })} ); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx index ccd98bd005666..9687846d6c520 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx @@ -37,13 +37,13 @@ interface Props { const filtersEmptyState: Filter[] = [{ key: '', value: '' }]; -export const CustomLinkFlyout = ({ +export function CustomLinkFlyout({ onClose, onSave, onDelete, defaults, customLinkId, -}: Props) => { +}: Props) { const { toasts } = useApmPluginContext().core.notifications; const [isSaving, setIsSaving] = useState(false); @@ -139,4 +139,4 @@ export const CustomLinkFlyout = ({ ); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx index f2aabc878bf2d..d512ea19c7892 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx @@ -24,10 +24,7 @@ interface Props { onCustomLinkSelected: (customLink: CustomLink) => void; } -export const CustomLinkTable = ({ - items = [], - onCustomLinkSelected, -}: Props) => { +export function CustomLinkTable({ items = [], onCustomLinkSelected }: Props) { const [searchTerm, setSearchTerm] = useState(''); const columns = [ @@ -121,20 +118,22 @@ export const CustomLinkTable = ({ /> ); -}; +} -const NoResultFound = ({ value }: { value: string }) => ( - - - - {i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.table.noResultFound', - { - defaultMessage: `No results for "{value}".`, - values: { value }, - } - )} - - - -); +function NoResultFound({ value }: { value: string }) { + return ( + + + + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.table.noResultFound', + { + defaultMessage: `No results for "{value}".`, + values: { value }, + } + )} + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/EmptyPrompt.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/EmptyPrompt.tsx index ee9350e320e1a..9411043c0b716 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/EmptyPrompt.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/EmptyPrompt.tsx @@ -8,11 +8,11 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { CreateCustomLinkButton } from './CreateCustomLinkButton'; -export const EmptyPrompt = ({ +export function EmptyPrompt({ onCreateCustomLinkClick, }: { onCreateCustomLinkClick: () => void; -}) => { +}) { return ( } /> ); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx index 95b8adb403981..22d8749d78834 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx @@ -7,34 +7,36 @@ import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -export const Title = () => ( - - - - - -

- {i18n.translate('xpack.apm.settings.customizeUI.customLink', { - defaultMessage: 'Custom Links', - })} -

-
+export function Title() { + return ( + + + + + +

+ {i18n.translate('xpack.apm.settings.customizeUI.customLink', { + defaultMessage: 'Custom Links', + })} +

+
- - - -
-
-
-
-); + + + +
+
+
+
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx index b4acc783d08ed..aa34515ea460a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx @@ -18,7 +18,7 @@ import { Title } from './Title'; import { CreateCustomLinkButton } from './CreateCustomLinkButton'; import { LicensePrompt } from '../../../../shared/LicensePrompt'; -export const CustomLinkOverview = () => { +export function CustomLinkOverview() { const license = useLicense(); const hasValidLicense = license?.isActive && license?.hasAtLeast('gold'); @@ -107,4 +107,4 @@ export const CustomLinkOverview = () => { ); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx index c88eba1c87b57..84408a7624403 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx @@ -9,7 +9,7 @@ import { EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { CustomLinkOverview } from './CustomLink'; -export const CustomizeUI = () => { +export function CustomizeUI() { return ( <> @@ -23,4 +23,4 @@ export const CustomizeUI = () => { ); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx index c9328c4988e5f..cb2090d1cbe2b 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx @@ -31,11 +31,11 @@ interface Props { onCreateJobSuccess: () => void; onCancel: () => void; } -export const AddEnvironments = ({ +export function AddEnvironments({ currentEnvironments, onCreateJobSuccess, onCancel, -}: Props) => { +}: Props) { const { notifications, application } = useApmPluginContext().core; const canCreateJob = !!application.capabilities.ml.canCreateJob; const { toasts } = notifications; @@ -175,4 +175,4 @@ export const AddEnvironments = ({ ); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx index abbe1e2c83c7b..dab30761c6ebe 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx @@ -28,7 +28,7 @@ const DEFAULT_VALUE: AnomalyDetectionApiResponse = { errorCode: undefined, }; -export const AnomalyDetection = () => { +export function AnomalyDetection() { const plugin = useApmPluginContext(); const canGetJobs = !!plugin.core.application.capabilities.ml.canGetJobs; const license = useLicense(); @@ -112,4 +112,4 @@ export const AnomalyDetection = () => { )} ); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx index f3b8822010f59..8494004ae5639 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx @@ -65,7 +65,7 @@ interface Props { status: FETCH_STATUS; onAddEnvironments: () => void; } -export const JobsList = ({ data, status, onAddEnvironments }: Props) => { +export function JobsList({ data, status, onAddEnvironments }: Props) { const { jobs, hasLegacyJobs, errorCode } = data; return ( @@ -127,7 +127,7 @@ export const JobsList = ({ data, status, onAddEnvironments }: Props) => { {hasLegacyJobs && } ); -}; +} function getNoItemsMessage({ status, diff --git a/x-pack/plugins/apm/public/components/app/Settings/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/index.tsx index 6d8571bf57767..bd2ea706e492d 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { ReactNode } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty, @@ -17,7 +17,7 @@ import { HomeLink } from '../../shared/Links/apm/HomeLink'; import { useLocation } from '../../../hooks/useLocation'; import { getAPMHref } from '../../shared/Links/apm/APMLink'; -export const Settings: React.FC = (props) => { +export function Settings(props: { children: ReactNode }) { const { search, pathname } = useLocation(); return ( <> @@ -84,4 +84,4 @@ export const Settings: React.FC = (props) => { ); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx index 3eb5a855ee3b4..55ab275002b4e 100644 --- a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx @@ -58,7 +58,7 @@ const redirectToTracePage = ({ }, }); -export const TraceLink = () => { +export function TraceLink() { const { urlParams } = useUrlParams(); const { traceIdLink: traceId, rangeFrom, rangeTo } = urlParams; @@ -93,4 +93,4 @@ export const TraceLink = () => { Fetching trace...} /> ); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx index 1244dd01a3b43..90bbe0a5a2135 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx @@ -7,19 +7,19 @@ import { EuiIconTip, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import d3 from 'd3'; -import React, { FunctionComponent, useCallback } from 'react'; import { isEmpty } from 'lodash'; +import React, { useCallback } from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { IBucket } from '../../../../../server/lib/transactions/distribution/get_buckets/transform'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { getDurationFormatter } from '../../../../utils/formatters'; +import { history } from '../../../../utils/history'; // @ts-ignore import Histogram from '../../../shared/charts/Histogram'; import { EmptyMessage } from '../../../shared/EmptyMessage'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; -import { history } from '../../../../utils/history'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; interface IChartPoint { @@ -99,9 +99,7 @@ interface Props { bucketIndex: number; } -export const TransactionDistribution: FunctionComponent = ( - props: Props -) => { +export function TransactionDistribution(props: Props) { const { distribution, urlParams: { transactionType }, @@ -211,4 +209,4 @@ export const TransactionDistribution: FunctionComponent = ( />
); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.tsx index 89757b227f8fd..20f93bce29ca8 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.tsx @@ -12,21 +12,23 @@ interface Props { count: number; } -export const ErrorCount = ({ count }: Props) => ( - -

- { - e.stopPropagation(); - }} - > - {i18n.translate('xpack.apm.transactionDetails.errorCount', { - defaultMessage: - '{errorCount, number} {errorCount, plural, one {Error} other {Errors}}', - values: { errorCount: count }, - })} - -

-
-); +export function ErrorCount({ count }: Props) { + return ( + +

+ { + e.stopPropagation(); + }} + > + {i18n.translate('xpack.apm.transactionDetails.errorCount', { + defaultMessage: + '{errorCount, number} {errorCount, plural, one {Error} other {Errors}}', + values: { errorCount: count }, + })} + +

+
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx index 64e20cf10d8aa..4f32df2b3115e 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx @@ -6,7 +6,7 @@ import { EuiIcon, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { Fragment, useEffect, useRef, useState } from 'react'; +import React, { Fragment, ReactNode, useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; import { px, units } from '../../../../../../../style/variables'; @@ -16,13 +16,11 @@ const ToggleButtonContainer = styled.div` `; interface Props { + children: ReactNode; previewHeight: number; } -export const TruncateHeightSection: React.FC = ({ - children, - previewHeight, -}) => { +export function TruncateHeightSection({ children, previewHeight }: Props) { const contentContainerEl = useRef(null); const [showToggle, setShowToggle] = useState(true); @@ -73,4 +71,4 @@ export const TruncateHeightSection: React.FC = ({ ) : null} ); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx index f0150e5a1b758..7e1dbddf56025 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx @@ -15,12 +15,13 @@ interface Props { location: Location; toggleFlyout: ({ location }: { location: Location }) => void; } -export const WaterfallFlyout: React.FC = ({ + +export function WaterfallFlyout({ waterfallItemId, waterfall, location, toggleFlyout, -}) => { +}: Props) { const currentItem = waterfall.items.find( (item) => item.id === waterfallItemId ); @@ -58,4 +59,4 @@ export const WaterfallFlyout: React.FC = ({ default: return null; } -}; +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx index a25ae71947f21..a4d42bcf51d01 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { ReactNode } from 'react'; import styled from 'styled-components'; import { EuiIcon, EuiText, EuiTitle, EuiToolTip } from '@elastic/eui'; @@ -109,13 +109,11 @@ function PrefixIcon({ item }: { item: IWaterfallItem }) { } interface SpanActionToolTipProps { + children: ReactNode; item?: IWaterfallItem; } -const SpanActionToolTip: React.FC = ({ - item, - children, -}) => { +function SpanActionToolTip({ item, children }: SpanActionToolTipProps) { if (item?.docType === 'span') { return ( @@ -124,7 +122,7 @@ const SpanActionToolTip: React.FC = ({ ); } return <>{children}; -}; +} function Duration({ item }: { item: IWaterfallItem }) { return ( diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx index 78235594f40ec..1fd0ec761b1ae 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx @@ -67,12 +67,12 @@ interface Props { exceedsMax: boolean; } -export const Waterfall: React.FC = ({ +export function Waterfall({ waterfall, exceedsMax, waterfallItemId, location, -}) => { +}: Props) { const itemContainerHeight = 58; // TODO: This is a nasty way to calculate the height of the svg element. A better approach should be found const waterfallHeight = itemContainerHeight * waterfall.items.length; @@ -81,7 +81,7 @@ export const Waterfall: React.FC = ({ const agentMarks = getAgentMarks(waterfall.entryTransaction); const errorMarks = getErrorMarks(waterfall.errorItems, serviceColors); - const renderWaterfallItem = (item: IWaterfallItem) => { + function renderWaterfallItem(item: IWaterfallItem) { const errorCount = item.docType === 'transaction' ? waterfall.errorsPerTransaction[item.doc.transaction.id] @@ -99,7 +99,7 @@ export const Waterfall: React.FC = ({ onClick={() => toggleFlyout({ item, location })} /> ); - }; + } return ( @@ -134,4 +134,4 @@ export const Waterfall: React.FC = ({ /> ); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx index beb0c03f37f8f..12676b7c15f1c 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx @@ -37,14 +37,14 @@ interface Props { traceSamples: IBucket['samples']; } -export const WaterfallWithSummmary: React.FC = ({ +export function WaterfallWithSummmary({ urlParams, location, waterfall, exceedsMax, isLoading, traceSamples, -}) => { +}: Props) { const [sampleActivePage, setSampleActivePage] = useState(0); useEffect(() => { @@ -135,4 +135,4 @@ export const WaterfallWithSummmary: React.FC = ({ /> ); -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx b/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx index cccbdc8d86d91..4ffd422801816 100644 --- a/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx @@ -5,29 +5,31 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import React from 'react'; +import React, { ReactNode } from 'react'; import { KueryBar } from '../KueryBar'; import { DatePicker } from '../DatePicker'; import { EnvironmentFilter } from '../EnvironmentFilter'; -export const ApmHeader: React.FC = ({ children }) => ( - <> - - {children} - - - - +export function ApmHeader({ children }: { children: ReactNode }) { + return ( + <> + + {children} + + + + - + - - - - - - - - - -); + + + + + + + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx b/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx index 215e97aebf646..36e33fba89fbb 100644 --- a/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { ReactNode } from 'react'; import { LocationProvider } from '../../../../context/LocationContext'; import { UrlParamsContext, @@ -21,18 +21,24 @@ import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContex const mockHistoryPush = jest.spyOn(history, 'push'); const mockRefreshTimeRange = jest.fn(); -const MockUrlParamsProvider: React.FC<{ +function MockUrlParamsProvider({ + params = {}, + children, +}: { + children: ReactNode; params?: IUrlParams; -}> = ({ params = {}, children }) => ( - -); +}) { + return ( + + ); +} function mountDatePicker(params?: IUrlParams) { return mount( diff --git a/x-pack/plugins/apm/public/components/shared/EmptyMessage.tsx b/x-pack/plugins/apm/public/components/shared/EmptyMessage.tsx index f300ed9d65aac..296df901d309e 100644 --- a/x-pack/plugins/apm/public/components/shared/EmptyMessage.tsx +++ b/x-pack/plugins/apm/public/components/shared/EmptyMessage.tsx @@ -14,7 +14,7 @@ interface Props { hideSubheading?: boolean; } -const EmptyMessage: React.FC = ({ +function EmptyMessage({ heading = i18n.translate('xpack.apm.emptyMessage.noDataFoundLabel', { defaultMessage: 'No data found.', }), @@ -22,7 +22,7 @@ const EmptyMessage: React.FC = ({ defaultMessage: 'Try another time range or reset the search filter.', }), hideSubheading = false, -}) => { +}: Props) { return ( = ({ body={!hideSubheading && subheading} /> ); -}; +} export { EmptyMessage }; diff --git a/x-pack/plugins/apm/public/components/shared/EnvironmentBadge/index.tsx b/x-pack/plugins/apm/public/components/shared/EnvironmentBadge/index.tsx index 47e52285b6851..a430eea1cf40c 100644 --- a/x-pack/plugins/apm/public/components/shared/EnvironmentBadge/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/EnvironmentBadge/index.tsx @@ -11,7 +11,7 @@ import { EuiBadge, EuiToolTip } from '@elastic/eui'; interface Props { environments: string[]; } -export const EnvironmentBadge: React.FC = ({ environments = [] }) => { +export function EnvironmentBadge({ environments = [] }: Props) { if (environments.length < 3) { return ( <> @@ -42,4 +42,4 @@ export const EnvironmentBadge: React.FC = ({ environments = [] }) => { ); -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx index 28dd5e7a5a363..1490ca42679b9 100644 --- a/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx @@ -65,7 +65,7 @@ function getOptions(environments: string[]) { ]; } -export const EnvironmentFilter: React.FC = () => { +export function EnvironmentFilter() { const location = useLocation(); const { uiFilters, urlParams } = useUrlParams(); @@ -90,4 +90,4 @@ export const EnvironmentFilter: React.FC = () => { isLoading={status === 'loading'} /> ); -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/EuiTabLink.tsx b/x-pack/plugins/apm/public/components/shared/EuiTabLink.tsx index 8538ea6a510ce..d29ccd8abcd42 100644 --- a/x-pack/plugins/apm/public/components/shared/EuiTabLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/EuiTabLink.tsx @@ -32,7 +32,7 @@ const Wrapper = styled.div<{ isSelected: boolean }>` } `; -const EuiTabLink = (props: Props) => { +function EuiTabLink(props: Props) { const { isSelected, children } = props; const className = cls('euiTab', { @@ -44,6 +44,6 @@ const EuiTabLink = (props: Props) => { {children} ); -}; +} export { EuiTabLink }; diff --git a/x-pack/plugins/apm/public/components/shared/HeightRetainer/index.tsx b/x-pack/plugins/apm/public/components/shared/HeightRetainer/index.tsx index be8ff87617c80..5c8755f9f586f 100644 --- a/x-pack/plugins/apm/public/components/shared/HeightRetainer/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/HeightRetainer/index.tsx @@ -6,7 +6,12 @@ import React, { useEffect, useRef } from 'react'; -export const HeightRetainer: React.FC = (props) => { +export function HeightRetainer( + props: React.DetailedHTMLProps< + React.HTMLAttributes, + HTMLDivElement + > +) { const containerElement = useRef(null); const minHeight = useRef(0); @@ -26,4 +31,4 @@ export const HeightRetainer: React.FC = (props) => { style={{ minHeight: minHeight.current }} /> ); -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx index 6ddc4eecba7ed..502f5f0034b5f 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -112,7 +112,6 @@ export function KueryBar() { setState({ ...state, suggestions, isLoadingSuggestions: false }); } catch (e) { - // eslint-disable-next-line no-console console.error('Error while fetching suggestions', e); } } diff --git a/x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx b/x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx index d8464fdfa8481..50be268d9ccd0 100644 --- a/x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx @@ -14,7 +14,7 @@ interface Props { showBetaBadge?: boolean; } -export const LicensePrompt = ({ text, showBetaBadge = false }: Props) => { +export function LicensePrompt({ text, showBetaBadge = false }: Props) { const licensePageUrl = useKibanaUrl( '/app/kibana', '/management/stack/license_management/home' @@ -60,4 +60,4 @@ export const LicensePrompt = ({ text, showBetaBadge = false }: Props) => { ); return <>{showBetaBadge ? renderWithBetaBadge : renderLicenseBody}; -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.tsx index 5679e31a9898b..d83f10cf1975f 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { ReactNode } from 'react'; import { ERROR_GROUP_ID, SERVICE_NAME, @@ -32,13 +32,18 @@ function getDiscoverQuery(error: APMError, kuery?: string) { }; } -const DiscoverErrorLink: React.FC<{ +function DiscoverErrorLink({ + error, + kuery, + children, +}: { + children?: ReactNode; readonly error: APMError; readonly kuery?: string; -}> = ({ error, kuery, children }) => { +}) { return ( ); -}; +} export { DiscoverErrorLink }; diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverSpanLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverSpanLink.tsx index 5fce3e842d8da..d7751c43b5943 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverSpanLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverSpanLink.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { ReactNode } from 'react'; import { SPAN_ID } from '../../../../../common/elasticsearch_fieldnames'; import { Span } from '../../../../../typings/es_schemas/ui/span'; import { DiscoverLink } from './DiscoverLink'; @@ -22,8 +22,12 @@ function getDiscoverQuery(span: Span) { }; } -export const DiscoverSpanLink: React.FC<{ +export function DiscoverSpanLink({ + span, + children, +}: { readonly span: Span; -}> = ({ span, children }) => { + children?: ReactNode; +}) { return ; -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.tsx index e2500617155c1..223fabbdb0d6f 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { ReactNode } from 'react'; import { PROCESSOR_EVENT, TRACE_ID, @@ -32,10 +32,14 @@ export function getDiscoverQuery(transaction: Transaction) { }; } -export const DiscoverTransactionLink: React.FC<{ +export function DiscoverTransactionLink({ + transaction, + children, +}: { readonly transaction: Transaction; -}> = ({ transaction, children }) => { + children?: ReactNode; +}) { return ( ); -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx index f3c5b49287293..887ac2ff6bbb9 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx @@ -4,24 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { ReactNode } from 'react'; import { EuiLink } from '@elastic/eui'; import { useTimeSeriesExplorerHref } from './useTimeSeriesExplorerHref'; interface Props { + children?: ReactNode; jobId: string; external?: boolean; serviceName?: string; transactionType?: string; } -export const MLJobLink: React.FC = ({ +export function MLJobLink({ jobId, serviceName, transactionType, external, children, -}) => { +}: Props) { const href = useTimeSeriesExplorerHref({ jobId, serviceName, @@ -36,4 +37,4 @@ export const MLJobLink: React.FC = ({ target={external ? '_blank' : undefined} /> ); -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorDetailLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorDetailLink.tsx index c788da6a0d240..1ff32b17f3245 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorDetailLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorDetailLink.tsx @@ -11,13 +11,13 @@ interface Props extends APMLinkExtendProps { errorGroupId: string; } -const ErrorDetailLink = ({ serviceName, errorGroupId, ...rest }: Props) => { +function ErrorDetailLink({ serviceName, errorGroupId, ...rest }: Props) { return ( ); -}; +} export { ErrorDetailLink }; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx index 684531d50897c..862b1ac649648 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx @@ -14,7 +14,7 @@ interface Props extends APMLinkExtendProps { query?: APMQueryParams; } -const ErrorOverviewLink = ({ serviceName, query, ...rest }: Props) => { +function ErrorOverviewLink({ serviceName, query, ...rest }: Props) { const { urlParams } = useUrlParams(); const persistedFilters = pickKeys( @@ -35,6 +35,6 @@ const ErrorOverviewLink = ({ serviceName, query, ...rest }: Props) => { {...rest} /> ); -}; +} export { ErrorOverviewLink }; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/HomeLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/HomeLink.tsx index 92ff3164880e8..724b9536dfaa3 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/HomeLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/HomeLink.tsx @@ -6,8 +6,8 @@ import React from 'react'; import { APMLink, APMLinkExtendProps } from './APMLink'; -const HomeLink = (props: APMLinkExtendProps) => { +function HomeLink(props: APMLinkExtendProps) { return ; -}; +} export { HomeLink }; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx index bd3e3b36a8601..35ba5db68d507 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx @@ -12,7 +12,7 @@ interface Props extends APMLinkExtendProps { serviceName: string; } -const MetricOverviewLink = ({ serviceName, ...rest }: Props) => { +function MetricOverviewLink({ serviceName, ...rest }: Props) { const { urlParams } = useUrlParams(); const persistedFilters = pickKeys( @@ -30,6 +30,6 @@ const MetricOverviewLink = ({ serviceName, ...rest }: Props) => { {...rest} /> ); -}; +} export { MetricOverviewLink }; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceMapLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceMapLink.tsx index 36c108160bdb2..ff8b1354daeb5 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceMapLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceMapLink.tsx @@ -16,11 +16,11 @@ interface ServiceMapLinkProps extends APMLinkExtendProps { serviceName?: string; } -const ServiceMapLink = ({ serviceName, ...rest }: ServiceMapLinkProps) => { +function ServiceMapLink({ serviceName, ...rest }: ServiceMapLinkProps) { const path = serviceName ? `/services/${serviceName}/service-map` : '/service-map'; return ; -}; +} export { ServiceMapLink }; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx index 1473221cca2be..2553ec4353194 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx @@ -13,11 +13,11 @@ interface Props extends APMLinkExtendProps { serviceNodeName: string; } -const ServiceNodeMetricOverviewLink = ({ +function ServiceNodeMetricOverviewLink({ serviceName, serviceNodeName, ...rest -}: Props) => { +}: Props) { const { urlParams } = useUrlParams(); const persistedFilters = pickKeys( @@ -37,6 +37,6 @@ const ServiceNodeMetricOverviewLink = ({ {...rest} /> ); -}; +} export { ServiceNodeMetricOverviewLink }; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx index b479ab77e1127..111c2391cd54f 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx @@ -12,7 +12,7 @@ interface Props extends APMLinkExtendProps { serviceName: string; } -const ServiceNodeOverviewLink = ({ serviceName, ...rest }: Props) => { +function ServiceNodeOverviewLink({ serviceName, ...rest }: Props) { const { urlParams } = useUrlParams(); const persistedFilters = pickKeys( @@ -30,6 +30,6 @@ const ServiceNodeOverviewLink = ({ serviceName, ...rest }: Props) => { {...rest} /> ); -}; +} export { ServiceNodeOverviewLink }; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceOverviewLink.tsx index 577209a26e46b..2081fc4767903 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceOverviewLink.tsx @@ -14,12 +14,12 @@ import { APMLink, APMLinkExtendProps } from './APMLink'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { pickKeys } from '../../../../../common/utils/pick_keys'; -const ServiceOverviewLink = (props: APMLinkExtendProps) => { +function ServiceOverviewLink(props: APMLinkExtendProps) { const { urlParams } = useUrlParams(); const persistedFilters = pickKeys(urlParams, 'host', 'agentName'); return ; -}; +} export { ServiceOverviewLink }; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/SettingsLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/SettingsLink.tsx index 853972f4df402..80f3053b86f93 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/SettingsLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/SettingsLink.tsx @@ -6,8 +6,8 @@ import React from 'react'; import { APMLink, APMLinkExtendProps } from './APMLink'; -const SettingsLink = (props: APMLinkExtendProps) => { +function SettingsLink(props: APMLinkExtendProps) { return ; -}; +} export { SettingsLink }; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx index dc4519365cbc2..8f3ea191fab1a 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx @@ -14,7 +14,7 @@ import { APMLink, APMLinkExtendProps } from './APMLink'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { pickKeys } from '../../../../../common/utils/pick_keys'; -const TraceOverviewLink = (props: APMLinkExtendProps) => { +function TraceOverviewLink(props: APMLinkExtendProps) { const { urlParams } = useUrlParams(); const persistedFilters = pickKeys( @@ -26,6 +26,6 @@ const TraceOverviewLink = (props: APMLinkExtendProps) => { ); return ; -}; +} export { TraceOverviewLink }; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx index c7eba1984472e..2ca3dce5da9ce 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx @@ -17,14 +17,14 @@ interface Props extends APMLinkExtendProps { transactionType: string; } -export const TransactionDetailLink = ({ +export function TransactionDetailLink({ serviceName, traceId, transactionId, transactionName, transactionType, ...rest -}: Props) => { +}: Props) { const { urlParams } = useUrlParams(); const persistedFilters = pickKeys( @@ -46,4 +46,4 @@ export const TransactionDetailLink = ({ {...rest} /> ); -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx index ccef83ee73fb8..adc64f5a2d3dc 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx @@ -12,7 +12,7 @@ interface Props extends APMLinkExtendProps { serviceName: string; } -const TransactionOverviewLink = ({ serviceName, ...rest }: Props) => { +function TransactionOverviewLink({ serviceName, ...rest }: Props) { const { urlParams } = useUrlParams(); const persistedFilters = pickKeys( @@ -31,6 +31,6 @@ const TransactionOverviewLink = ({ serviceName, ...rest }: Props) => { {...rest} /> ); -}; +} export { TransactionOverviewLink }; diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx index 9191f4e797637..2090a92bf0de4 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx @@ -20,24 +20,26 @@ interface Props { onRemove: (val: string) => void; } -const FilterBadgeList = ({ onRemove, value }: Props) => ( - - {value.map((val) => ( - - - - ))} - -); +function FilterBadgeList({ onRemove, value }: Props) { + return ( + + {value.map((val) => ( + + + + ))} + + ); +} export { FilterBadgeList }; diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx index 26125ab0f5343..0d306f5133716 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx @@ -24,7 +24,7 @@ const Button = styled(EuiButtonEmpty).attrs(() => ({ type Props = React.ComponentProps; -export const FilterTitleButton = (props: Props) => { +export function FilterTitleButton(props: Props) { return ( ); -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx index 167574f9aa00d..c13439a3c5928 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx @@ -66,14 +66,7 @@ interface Props { type Option = EuiSelectable['props']['options'][0]; -const Filter = ({ - name, - title, - options, - onChange, - value, - showCount, -}: Props) => { +function Filter({ name, title, options, onChange, value, showCount }: Props) { const [showPopover, setShowPopover] = useState(false); const toggleShowPopover = () => setShowPopover((show) => !show); @@ -176,6 +169,6 @@ const Filter = ({ ) : null} ); -}; +} export { Filter }; diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx index 405a4cacae714..99656b05db450 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx @@ -21,7 +21,7 @@ interface Props { loading: boolean; } -const ServiceNameFilter = ({ loading, serviceNames }: Props) => { +function ServiceNameFilter({ loading, serviceNames }: Props) { const { urlParams: { serviceName }, } = useUrlParams(); @@ -72,6 +72,6 @@ const ServiceNameFilter = ({ loading, serviceNames }: Props) => { /> ); -}; +} export { ServiceNameFilter }; diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx index 0e6b1c5904fc5..afd2d023d16ba 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx @@ -20,7 +20,7 @@ interface Props { transactionTypes: string[]; } -const TransactionTypeFilter = ({ transactionTypes }: Props) => { +function TransactionTypeFilter({ transactionTypes }: Props) { const { urlParams: { transactionType }, } = useUrlParams(); @@ -59,6 +59,6 @@ const TransactionTypeFilter = ({ transactionTypes }: Props) => { /> ); -}; +} export { TransactionTypeFilter }; diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx index 020b7481c68ea..fedf96b4cc4ea 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx @@ -31,13 +31,13 @@ const ButtonWrapper = styled.div` display: inline-block; `; -const LocalUIFilters = ({ +function LocalUIFilters({ projection, params, filterNames, children, showCount = true, -}: Props) => { +}: Props) { const { filters, setFilterValue, clearValues } = useLocalUIFilters({ filterNames, projection, @@ -91,6 +91,6 @@ const LocalUIFilters = ({ ) : null} ); -}; +} export { LocalUIFilters }; diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx index eace3035a3555..8dfb1e0ce960d 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx @@ -91,18 +91,20 @@ export function MetadataTable({ sections }: Props) { ); } -const NoResultFound = ({ value }: { value: string }) => ( - - - - {i18n.translate( - 'xpack.apm.propertiesTable.agentFeature.noResultFound', - { - defaultMessage: `No results for "{value}".`, - values: { value }, - } - )} - - - -); +function NoResultFound({ value }: { value: string }) { + return ( + + + + {i18n.translate( + 'xpack.apm.propertiesTable.agentFeature.noResultFound', + { + defaultMessage: `No results for "{value}".`, + values: { value }, + } + )} + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/SelectWithPlaceholder/index.tsx b/x-pack/plugins/apm/public/components/shared/SelectWithPlaceholder/index.tsx index e0da91fae2ba7..02939b18401fe 100644 --- a/x-pack/plugins/apm/public/components/shared/SelectWithPlaceholder/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/SelectWithPlaceholder/index.tsx @@ -19,6 +19,7 @@ const DEFAULT_PLACEHOLDER = i18n.translate('xpack.apm.selectPlaceholder', { * with `hasNoInitialSelection`. It uses the `placeholder` prop to populate * the first option as the initial, not selected option. */ +// eslint-disable-next-line react/function-component-definition export const SelectWithPlaceholder: typeof EuiSelect = (props) => { const placeholder = props.placeholder || DEFAULT_PLACEHOLDER; return ( diff --git a/x-pack/plugins/apm/public/components/shared/ServiceAlertTrigger/PopoverExpression/index.tsx b/x-pack/plugins/apm/public/components/shared/ServiceAlertTrigger/PopoverExpression/index.tsx index 1abdb94c8313e..b07672eeaee06 100644 --- a/x-pack/plugins/apm/public/components/shared/ServiceAlertTrigger/PopoverExpression/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ServiceAlertTrigger/PopoverExpression/index.tsx @@ -13,7 +13,7 @@ interface Props { children?: React.ReactNode; } -export const PopoverExpression = (props: Props) => { +export function PopoverExpression(props: Props) { const { title, value, children } = props; const [popoverOpen, setPopoverOpen] = useState(false); @@ -36,4 +36,4 @@ export const PopoverExpression = (props: Props) => { {children} ); -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx index 48580146c6fe1..5891895629318 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx @@ -29,7 +29,7 @@ interface Props { isLibraryFrame: boolean; } -const FrameHeading: React.FC = ({ stackframe, isLibraryFrame }) => { +function FrameHeading({ stackframe, isLibraryFrame }: Props) { const FileDetail = isLibraryFrame ? LibraryFrameFileDetail : AppFrameFileDetail; @@ -50,6 +50,6 @@ const FrameHeading: React.FC = ({ stackframe, isLibraryFrame }) => { )} ); -}; +} export { FrameHeading }; diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx index 4bd6d361d6714..07b5ed6868df5 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx @@ -23,7 +23,7 @@ interface Props { vars: IStackframe['vars']; } -export const Variables = ({ vars }: Props) => { +export function Variables({ vars }: Props) { if (!vars) { return null; } @@ -46,4 +46,4 @@ export const Variables = ({ vars }: Props) => { ); -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx b/x-pack/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx index 831f72e3925af..7858bebead408 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx @@ -15,11 +15,7 @@ interface Props { parentType: 'trace' | 'transaction'; } -const DurationSummaryItem = ({ - duration, - totalDuration, - parentType, -}: Props) => { +function DurationSummaryItem({ duration, totalDuration, parentType }: Props) { const calculatedTotalDuration = totalDuration === undefined ? duration : totalDuration; @@ -41,6 +37,6 @@ const DurationSummaryItem = ({ ); -}; +} export { DurationSummaryItem }; diff --git a/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx b/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx index b6ea6a714017d..ed33c59af36f4 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx @@ -19,7 +19,7 @@ const Badge = (styled(EuiBadge)` margin-top: ${px(units.eighth)}; ` as unknown) as typeof EuiBadge; -export const ErrorCountSummaryItemBadge = ({ count }: Props) => { +export function ErrorCountSummaryItemBadge({ count }: Props) { const theme = useTheme(); return ( @@ -31,4 +31,4 @@ export const ErrorCountSummaryItemBadge = ({ count }: Props) => { })} ); -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx b/x-pack/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx index 86b42844f1fa7..98543ffaa9218 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx @@ -20,7 +20,7 @@ interface Props { errorCount: number; } -const getTransactionResultSummaryItem = (transaction: Transaction) => { +function getTransactionResultSummaryItem(transaction: Transaction) { const result = transaction.transaction.result; const isRumAgent = isRumAgentName(transaction.agent.name); const url = isRumAgent @@ -39,13 +39,9 @@ const getTransactionResultSummaryItem = (transaction: Transaction) => { } return null; -}; +} -const TransactionSummary = ({ - transaction, - totalDuration, - errorCount, -}: Props) => { +function TransactionSummary({ transaction, totalDuration, errorCount }: Props) { const items = [ , ; -}; +} export { TransactionSummary }; diff --git a/x-pack/plugins/apm/public/components/shared/Summary/index.tsx b/x-pack/plugins/apm/public/components/shared/Summary/index.tsx index 55ac525d71192..aea62c88f5833 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/index.tsx @@ -26,7 +26,7 @@ const Item = styled(EuiFlexItem)` } `; -const Summary = ({ items }: Props) => { +function Summary({ items }: Props) { const filteredItems = items.filter(Boolean) as React.ReactElement[]; return ( @@ -38,6 +38,6 @@ const Summary = ({ items }: Props) => { ))} ); -}; +} export { Summary }; diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx index 00a839adc2fdd..27c6aa82ac674 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx @@ -24,7 +24,7 @@ const ScrollableContainer = styled.div` overflow: scroll; `; -export const CustomLinkPopover = ({ +export function CustomLinkPopover({ customLinks, onCreateCustomLinkClick, onClose, @@ -34,7 +34,7 @@ export const CustomLinkPopover = ({ onCreateCustomLinkClick: () => void; onClose: () => void; transaction: Transaction; -}) => { +}) { return ( <> @@ -71,4 +71,4 @@ export const CustomLinkPopover = ({ ); -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx index 40143b53f17c5..6b421bc370332 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx @@ -24,28 +24,30 @@ const TruncateText = styled(EuiText)` ${truncate(px(units.unit * 25))} `; -export const CustomLinkSection = ({ +export function CustomLinkSection({ customLinks, transaction, }: { customLinks: CustomLink[]; transaction: Transaction; -}) => ( -
    - {customLinks.map((link) => { - let href = link.url; - try { - href = Mustache.render(link.url, transaction); - } catch (e) { - // ignores any error that happens - } - return ( - - - {link.label} - - - ); - })} -
-); +}) { + return ( +
    + {customLinks.map((link) => { + let href = link.url; + try { + href = Mustache.render(link.url, transaction); + } catch (e) { + // ignores any error that happens + } + return ( + + + {link.label} + + + ); + })} +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.tsx index 9740a9f1ee847..09cdaa26004bb 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.tsx @@ -14,46 +14,48 @@ import { import { i18n } from '@kbn/i18n'; import { APMLink } from '../../Links/apm/APMLink'; -export const ManageCustomLink = ({ +export function ManageCustomLink({ onCreateCustomLinkClick, showCreateCustomLinkButton = true, }: { onCreateCustomLinkClick: () => void; showCreateCustomLinkButton?: boolean; -}) => ( - - - - - - - - - - - {showCreateCustomLinkButton && ( - - - {i18n.translate('xpack.apm.customLink.buttom.create.title', { - defaultMessage: 'Create', +}) { + return ( + + + + + + > + + + + - )} - - - -); + {showCreateCustomLinkButton && ( + + + {i18n.translate('xpack.apm.customLink.buttom.create.title', { + defaultMessage: 'Create', + })} + + + )} + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.tsx index 40ac3c31d1d43..d6484f52e84f9 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.tsx @@ -37,7 +37,7 @@ const SeeMoreButton = styled.button<{ show: boolean }>` } `; -export const CustomLink = ({ +export function CustomLink({ customLinks, status, onCreateCustomLinkClick, @@ -49,7 +49,7 @@ export const CustomLink = ({ onCreateCustomLinkClick: () => void; onSeeMoreClick: () => void; transaction: Transaction; -}) => { +}) { const renderEmptyPrompt = ( <> @@ -125,4 +125,4 @@ export const CustomLink = ({ )} ); -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx index 2507eca9ff663..77d70c626183f 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx @@ -6,10 +6,8 @@ import { EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { FunctionComponent, useMemo, useState, MouseEvent } from 'react'; +import React, { MouseEvent, useMemo, useState } from 'react'; import url from 'url'; -import { Filter } from '../../../../common/custom_link/custom_link_types'; -import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { ActionMenu, ActionMenuDivider, @@ -19,32 +17,34 @@ import { SectionSubtitle, SectionTitle, } from '../../../../../observability/public'; +import { Filter } from '../../../../common/custom_link/custom_link_types'; +import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { useFetcher } from '../../../hooks/useFetcher'; +import { useLicense } from '../../../hooks/useLicense'; import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { CustomLinkFlyout } from '../../app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout'; +import { convertFiltersToQuery } from '../../app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper'; import { CustomLink } from './CustomLink'; import { CustomLinkPopover } from './CustomLink/CustomLinkPopover'; import { getSections } from './sections'; -import { useLicense } from '../../../hooks/useLicense'; -import { convertFiltersToQuery } from '../../app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper'; interface Props { readonly transaction: Transaction; } -const ActionMenuButton = ({ onClick }: { onClick: () => void }) => ( - - {i18n.translate('xpack.apm.transactionActionMenu.actionsButtonLabel', { - defaultMessage: 'Actions', - })} - -); - -export const TransactionActionMenu: FunctionComponent = ({ - transaction, -}: Props) => { +function ActionMenuButton({ onClick }: { onClick: () => void }) { + return ( + + {i18n.translate('xpack.apm.transactionActionMenu.actionsButtonLabel', { + defaultMessage: 'Actions', + })} + + ); +} + +export function TransactionActionMenu({ transaction }: Props) { const license = useLicense(); const hasValidLicense = license?.isActive && license?.hasAtLeast('gold'); @@ -211,4 +211,4 @@ export const TransactionActionMenu: FunctionComponent = ({ ); -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx index 2cb3696f88002..209657971620b 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx @@ -29,7 +29,7 @@ const formatTooltipValue = (coordinate: Coordinate) => { : NOT_AVAILABLE_LABEL; }; -const TransactionBreakdownGraph: React.FC = (props) => { +function TransactionBreakdownGraph(props: Props) { const { timeseries } = props; const trackApmEvent = useUiTracker({ app: 'apm' }); const handleHover = useMemo( @@ -49,6 +49,6 @@ const TransactionBreakdownGraph: React.FC = (props) => { onHover={handleHover} /> ); -}; +} export { TransactionBreakdownGraph }; diff --git a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx index 3898679f83537..d3761cf0fe38e 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx @@ -31,10 +31,7 @@ const Description = styled.span` } `; -const KpiDescription: React.FC<{ - name: string; - color: string; -}> = ({ name, color }) => { +function KpiDescription({ name, color }: { name: string; color: string }) { return ( ); -}; +} -const TransactionBreakdownKpiList: React.FC = ({ kpis }) => { +function TransactionBreakdownKpiList({ kpis }: Props) { return ( {kpis.map((kpi) => ( @@ -73,6 +70,6 @@ const TransactionBreakdownKpiList: React.FC = ({ kpis }) => { ))} ); -}; +} export { TransactionBreakdownKpiList }; diff --git a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx index 51cad6bc65a85..80ed9163ec08d 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx @@ -21,7 +21,7 @@ const emptyMessage = i18n.translate('xpack.apm.transactionBreakdown.noData', { defaultMessage: 'No data within this time range.', }); -const TransactionBreakdown = () => { +function TransactionBreakdown() { const { data, status } = useTransactionBreakdown(); const { kpis, timeseries } = data; const noHits = data.kpis.length === 0 && status === FETCH_STATUS.SUCCESS; @@ -51,6 +51,6 @@ const TransactionBreakdown = () => { ); -}; +} export { TransactionBreakdown }; diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx index ed57692d70a65..d02c5a5d08927 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx @@ -26,7 +26,7 @@ interface Props { overlay: Maybe; } -export const AnnotationsPlot = ({ plotValues, annotations }: Props) => { +export function AnnotationsPlot({ plotValues, annotations }: Props) { const theme = useTheme(); const tickValues = annotations.map((annotation) => annotation['@timestamp']); @@ -70,4 +70,4 @@ export const AnnotationsPlot = ({ plotValues, annotations }: Props) => { ))} ); -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx index f87be32b43fc1..a433b0b507239 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx @@ -21,7 +21,7 @@ const tickFormatY = (y?: number) => { return asPercent(y || 0, 1); }; -export const ErroneousTransactionsRateChart = () => { +export function ErroneousTransactionsRateChart() { const { urlParams, uiFilters } = useUrlParams(); const syncedChartsProps = useChartsSync(); @@ -105,4 +105,4 @@ export const ErroneousTransactionsRateChart = () => { /> ); -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx index a00c46bcf324d..1a2a90c9fb3c3 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx @@ -60,7 +60,7 @@ interface Props { indicator?: () => React.ReactNode; } -export const Legend: React.FC = ({ +export function Legend({ onClick, text, color, @@ -71,7 +71,7 @@ export const Legend: React.FC = ({ shape = Shape.circle, indicator, ...rest -}) => { +}: Props) { const theme = useTheme(); const indicatorColor = color || theme.eui.euiColorVis1; @@ -96,4 +96,4 @@ export const Legend: React.FC = ({ {text} ); -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx index d2dea39b83d82..64e0fe33c982f 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx @@ -27,7 +27,7 @@ interface Props { mark: AgentMark; } -export const AgentMarker: React.FC = ({ mark }) => { +export function AgentMarker({ mark }: Props) { const theme = useTheme(); return ( @@ -46,4 +46,4 @@ export const AgentMarker: React.FC = ({ mark }) => { ); -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx index d8e056deb769a..4567bc3f0f0b7 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx @@ -53,7 +53,7 @@ function truncateMessage(errorMessage?: string) { } } -export const ErrorMarker: React.FC = ({ mark }) => { +export function ErrorMarker({ mark }: Props) { const theme = useTheme(); const { urlParams } = useUrlParams(); const [isPopoverOpen, showPopover] = useState(false); @@ -123,4 +123,4 @@ export const ErrorMarker: React.FC = ({ mark }) => { ); -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx index 03124952c3f88..71a1639af6dcc 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx @@ -22,7 +22,7 @@ const MarkerContainer = styled.div` bottom: 0; `; -export const Marker: React.FC = ({ mark, x }) => { +export function Marker({ mark, x }: Props) { const legendWidth = 11; return ( @@ -33,4 +33,4 @@ export const Marker: React.FC = ({ mark, x }) => { )} ); -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx index a9c36634381d4..5cbfcc695e012 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx @@ -42,11 +42,11 @@ interface TimelineAxisProps { topTraceDuration: number; } -export const TimelineAxis = ({ +export function TimelineAxis({ plotValues, marks = [], topTraceDuration, -}: TimelineAxisProps) => { +}: TimelineAxisProps) { const theme = useTheme(); const { margins, tickValues, width, xDomain, xMax, xScale } = plotValues; const tickFormatter = getDurationFormatter(xMax); @@ -107,4 +107,4 @@ export const TimelineAxis = ({ }} ); -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx index 0753cb318d3a4..5ea2e4cfedf18 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx @@ -16,11 +16,11 @@ interface VerticalLinesProps { topTraceDuration: number; } -export const VerticalLines = ({ +export function VerticalLines({ topTraceDuration, plotValues, marks = [], -}: VerticalLinesProps) => { +}: VerticalLinesProps) { const { width, height, margins, xDomain, tickValues } = plotValues; const markTimes = marks @@ -63,4 +63,4 @@ export const VerticalLines = ({
); -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx index 9d13b23904b36..69d4e8109dfbf 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx @@ -9,11 +9,15 @@ import { i18n } from '@kbn/i18n'; import { asDuration, asInteger } from '../../../../../utils/formatters'; import { fontSizes } from '../../../../../style/variables'; -export const ChoroplethToolTip: React.FC<{ +export function ChoroplethToolTip({ + name, + value, + docCount, +}: { name: string; value: number; docCount: number; -}> = ({ name, value, docCount }) => { +}) { return (
{name}
@@ -41,4 +45,4 @@ export const ChoroplethToolTip: React.FC<{
); -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/index.tsx index a9a9343dde6be..965cb2ae4f50a 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/index.tsx @@ -66,7 +66,7 @@ const getMin = (items: ChoroplethItem[]) => const getMax = (items: ChoroplethItem[]) => Math.max(...items.map((item) => item.value)); -export const ChoroplethMap: React.FC = (props) => { +export function ChoroplethMap(props: Props) { const theme = useTheme(); const { items } = props; const containerRef = useRef(null); @@ -267,4 +267,4 @@ export const ChoroplethMap: React.FC = (props) => { ); -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx index 61030679f45fd..2dd3d058e98b8 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx @@ -11,7 +11,7 @@ import { useAvgDurationByCountry } from '../../../../../hooks/useAvgDurationByCo import { ChoroplethMap } from '../ChoroplethMap'; -export const DurationByCountryMap: React.FC = () => { +export function DurationByCountryMap() { const { data } = useAvgDurationByCountry(); return ( @@ -30,4 +30,4 @@ export const DurationByCountryMap: React.FC = () => { ); -}; +} diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx index cee74c81325ba..eaad883d2f9f6 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx @@ -30,7 +30,7 @@ interface Props { onHover?: () => void; } -const TransactionLineChart: React.FC = (props: Props) => { +function TransactionLineChart(props: Props) { const { series, tickFormatY, @@ -68,6 +68,6 @@ const TransactionLineChart: React.FC = (props: Props) => { {...(stacked ? { stackBy: 'y' } : {})} /> ); -}; +} export { TransactionLineChart }; diff --git a/x-pack/plugins/apm/public/context/ChartsSyncContext.tsx b/x-pack/plugins/apm/public/context/ChartsSyncContext.tsx index c00fc95f1f4f2..f93b69a877057 100644 --- a/x-pack/plugins/apm/public/context/ChartsSyncContext.tsx +++ b/x-pack/plugins/apm/public/context/ChartsSyncContext.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo, useState } from 'react'; +import React, { ReactNode, useMemo, useState } from 'react'; import { toQuery, fromQuery } from '../components/shared/Links/url_helpers'; import { history } from '../utils/history'; import { useUrlParams } from '../hooks/useUrlParams'; @@ -17,7 +17,7 @@ const ChartsSyncContext = React.createContext<{ onSelectionEnd: (range: { start: number; end: number }) => void; } | null>(null); -const ChartsSyncContextProvider: React.FC = ({ children }) => { +function ChartsSyncContextProvider({ children }: { children: ReactNode }) { const [time, setTime] = useState(null); const { urlParams, uiFilters } = useUrlParams(); @@ -78,6 +78,6 @@ const ChartsSyncContextProvider: React.FC = ({ children }) => { }, [time, data.annotations]); return ; -}; +} export { ChartsSyncContext, ChartsSyncContextProvider }; diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/MockUrlParamsContextProvider.tsx b/x-pack/plugins/apm/public/context/UrlParamsContext/MockUrlParamsContextProvider.tsx index 4e4fbabf5571a..fd01e057ac3de 100644 --- a/x-pack/plugins/apm/public/context/UrlParamsContext/MockUrlParamsContextProvider.tsx +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/MockUrlParamsContextProvider.tsx @@ -21,11 +21,11 @@ interface Props { refreshTimeRange?: (time: any) => void; } -export const MockUrlParamsContextProvider = ({ +export function MockUrlParamsContextProvider({ params, children, refreshTimeRange = () => undefined, -}: Props) => { +}: Props) { const urlParams = { ...defaultUrlParams, ...params }; return ( ); -}; +} diff --git a/x-pack/plugins/apm/public/utils/getRangeFromTimeSeries.ts b/x-pack/plugins/apm/public/utils/getRangeFromTimeSeries.ts index 8ec81616ccff8..71024edc9815c 100644 --- a/x-pack/plugins/apm/public/utils/getRangeFromTimeSeries.ts +++ b/x-pack/plugins/apm/public/utils/getRangeFromTimeSeries.ts @@ -7,7 +7,7 @@ import { flatten } from 'lodash'; import { TimeSeries } from '../../typings/timeseries'; -export const getRangeFromTimeSeries = (timeseries: TimeSeries[]) => { +export function getRangeFromTimeSeries(timeseries: TimeSeries[]) { const dataPoints = flatten(timeseries.map((series) => series.data)); if (dataPoints.length) { @@ -18,4 +18,4 @@ export const getRangeFromTimeSeries = (timeseries: TimeSeries[]) => { } return null; -}; +} diff --git a/x-pack/plugins/apm/public/utils/testHelpers.tsx b/x-pack/plugins/apm/public/utils/testHelpers.tsx index 8e7f987966783..418312743c324 100644 --- a/x-pack/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/plugins/apm/public/utils/testHelpers.tsx @@ -197,9 +197,11 @@ export function mountWithTheme( tree: React.ReactElement, { darkMode = false } = {} ) { - const WrappingThemeProvider = (props: any) => ( - {props.children} - ); + function WrappingThemeProvider(props: any) { + return ( + {props.children} + ); + } return mount(tree, { wrappingComponent: WrappingThemeProvider, diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index d76c033a41756..b0134ed8b746b 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -29,7 +29,7 @@ function getTitleFromBreadCrumbs(breadcrumbs: Breadcrumbs) { .join(' | '); } -const App = () => { +function App() { return ( <> @@ -53,7 +53,7 @@ const App = () => { ); -}; +} export const renderApp = (core: CoreStart, { element }: AppMountParameters) => { const i18nCore = core.i18n; diff --git a/x-pack/plugins/observability/public/components/app/chart_container/index.tsx b/x-pack/plugins/observability/public/components/app/chart_container/index.tsx index 2a0c25773eae5..b68ddbd06c778 100644 --- a/x-pack/plugins/observability/public/components/app/chart_container/index.tsx +++ b/x-pack/plugins/observability/public/components/app/chart_container/index.tsx @@ -18,12 +18,12 @@ interface Props { const CHART_HEIGHT = 170; -export const ChartContainer = ({ +export function ChartContainer({ isInitialLoad, children, iconSize = 'xl', height = CHART_HEIGHT, -}: Props) => { +}: Props) { if (isInitialLoad) { return (
:first-child { - flex-basis: 40% !important; - } - > :nth-child(2) { - order: 3; - } - > :nth-child(3) { - flex-basis: 60% !important; - } - } - } + position: relative; `; export const MonitorListHeader: React.FC = () => { @@ -48,18 +38,12 @@ export const MonitorListHeader: React.FC = () => { - - -
- - - -
-
-
+ + + ); }; From e9fa2f35427926dde0421242e067c02ebcf96e10 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Mon, 27 Jul 2020 12:15:56 -0500 Subject: [PATCH 44/96] [build] fix chmod errors (#72768) * inherit data permisions * tmp add all-platforms flag * Revert "inherit data permisions" This reverts commit ce30dd7b3aae1ee92acb3a2b49cc4ce681d0975c. * silent chmod, move to configure sectino * simplify chown and fix babel cache * rm empty lines * Revert "tmp add all-platforms flag" This reverts commit f1ae815ca9966b873fc735fde03fd8bfdc256aa4. Co-authored-by: Elastic Machine --- .../package_scripts/post_install.sh | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/dev/build/tasks/os_packages/package_scripts/post_install.sh b/src/dev/build/tasks/os_packages/package_scripts/post_install.sh index 10f11ff51874e..c49b291d1a0c9 100644 --- a/src/dev/build/tasks/os_packages/package_scripts/post_install.sh +++ b/src/dev/build/tasks/os_packages/package_scripts/post_install.sh @@ -3,6 +3,22 @@ set -e export KBN_PATH_CONF=${KBN_PATH_CONF:-<%= configDir %>} +set_chmod() { + chmod -f 660 ${KBN_PATH_CONF}/kibana.yml || true + chmod -f 2750 <%= dataDir %> || true + chmod -f 2750 ${KBN_PATH_CONF} || true +} + +set_chown() { + chown -R <%= user %>:<%= group %> <%= dataDir %> + chown -R root:<%= group %> ${KBN_PATH_CONF} +} + +set_access() { + set_chmod + set_chown +} + case $1 in # Debian configure) @@ -14,6 +30,8 @@ case $1 in adduser --quiet --system --no-create-home --disabled-password \ --ingroup "<%= group %>" --shell /bin/false "<%= user %>" fi + + set_access ;; abort-deconfigure|abort-upgrade|abort-remove) ;; @@ -28,6 +46,8 @@ case $1 in useradd -r -g "<%= group %>" -M -s /sbin/nologin \ -c "kibana service user" "<%= user %>" fi + + set_access ;; *) @@ -35,12 +55,3 @@ case $1 in exit 1 ;; esac - -chown -R <%= user %>:<%= group %> <%= dataDir %> -chmod 2750 <%= dataDir %> -chmod -R 2755 <%= dataDir %>/* - -chown :<%= group %> ${KBN_PATH_CONF} -chown :<%= group %> ${KBN_PATH_CONF}/kibana.yml -chmod 2750 ${KBN_PATH_CONF} -chmod 660 ${KBN_PATH_CONF}/kibana.yml From 6631a296ef8ee2934a5d93e160c4465f47874bee Mon Sep 17 00:00:00 2001 From: Poff Poffenberger Date: Mon, 27 Jul 2020 12:18:33 -0500 Subject: [PATCH 45/96] [Canvas] Fix top left elements being automatically selected on workpad page loads (#72121) * Fix top left elements being automatically selected on workpad page loads * Remove unnecessary code Co-authored-by: Elastic Machine --- .../components/workpad_page/workpad_interactive_page/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js b/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js index 41f78165a7394..632ec1ad5e004 100644 --- a/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js +++ b/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js @@ -127,7 +127,7 @@ const componentLayoutState = ({ gestureState: aeroStore ? aeroStore.getCurrentState().currentScene.gestureState : { - cursor: { x: 0, y: 0 }, + cursor: { x: Infinity, y: Infinity }, mouseIsDown: false, mouseButtonState: { buttonState: 'up', downX: null, downY: null }, }, From d66cc3515c227cfe81def49d52dc2dda3ba20f30 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 27 Jul 2020 10:43:18 -0700 Subject: [PATCH 46/96] [canvas] migrate tests away from karma (#73121) Co-authored-by: spalger Co-authored-by: Elastic Machine --- .../components/loading/__tests__/loading.js | 25 -- .../components/loading/loading.test.tsx | 36 +++ .../public/functions/__tests__/asset.js | 28 -- .../lib/__tests__/find_expression_type.js | 95 ------- .../public/lib/__tests__/history_provider.js | 240 ------------------ .../public/lib/__tests__/modify_path.js | 34 --- .../get_pretty_shortcut.test.ts | 2 +- .../canvas/public/lib/modify_path.test.ts | 33 +++ .../lib/{modify_path.js => modify_path.ts} | 8 +- .../{__tests__ => }/readable_color.test.ts | 2 +- .../resolved_arg.js => resolved_arg.test.ts} | 22 +- .../lib/{resolved_arg.js => resolved_arg.ts} | 6 +- .../lib/{__tests__ => }/time_interval.test.ts | 2 +- .../__tests__/elements.get_sibling_context.js | 107 -------- .../public/state/actions/elements.test.js | 106 ++++++++ .../public/state/middleware/in_flight.ts | 1 - .../action_creator.js | 0 .../elements.js => elements.test.js} | 10 +- ...resolved_args.js => resolved_args.test.js} | 126 +++++---- ...resolved_args.js => resolved_args.test.js} | 22 +- .../public/state/selectors/resolved_args.ts | 2 - .../{__tests__/workpad.js => workpad.test.js} | 60 ++--- .../canvas/public/state/selectors/workpad.ts | 1 - 23 files changed, 311 insertions(+), 657 deletions(-) delete mode 100644 x-pack/plugins/canvas/public/components/loading/__tests__/loading.js create mode 100644 x-pack/plugins/canvas/public/components/loading/loading.test.tsx delete mode 100644 x-pack/plugins/canvas/public/functions/__tests__/asset.js delete mode 100644 x-pack/plugins/canvas/public/lib/__tests__/find_expression_type.js delete mode 100644 x-pack/plugins/canvas/public/lib/__tests__/history_provider.js delete mode 100644 x-pack/plugins/canvas/public/lib/__tests__/modify_path.js rename x-pack/plugins/canvas/public/lib/{__tests__ => }/get_pretty_shortcut.test.ts (98%) create mode 100644 x-pack/plugins/canvas/public/lib/modify_path.test.ts rename x-pack/plugins/canvas/public/lib/{modify_path.js => modify_path.ts} (61%) rename x-pack/plugins/canvas/public/lib/{__tests__ => }/readable_color.test.ts (94%) rename x-pack/plugins/canvas/public/lib/{__tests__/resolved_arg.js => resolved_arg.test.ts} (57%) rename x-pack/plugins/canvas/public/lib/{resolved_arg.js => resolved_arg.ts} (75%) rename x-pack/plugins/canvas/public/lib/{__tests__ => }/time_interval.test.ts (98%) delete mode 100644 x-pack/plugins/canvas/public/state/actions/__tests__/elements.get_sibling_context.js create mode 100644 x-pack/plugins/canvas/public/state/actions/elements.test.js rename x-pack/plugins/canvas/public/state/reducers/{__tests__/fixtures => __fixtures__}/action_creator.js (100%) rename x-pack/plugins/canvas/public/state/reducers/{__tests__/elements.js => elements.test.js} (78%) rename x-pack/plugins/canvas/public/state/reducers/{__tests__/resolved_args.js => resolved_args.test.js} (62%) rename x-pack/plugins/canvas/public/state/selectors/{__tests__/resolved_args.js => resolved_args.test.js} (55%) rename x-pack/plugins/canvas/public/state/selectors/{__tests__/workpad.js => workpad.test.js} (80%) diff --git a/x-pack/plugins/canvas/public/components/loading/__tests__/loading.js b/x-pack/plugins/canvas/public/components/loading/__tests__/loading.js deleted file mode 100644 index c159f478766ce..0000000000000 --- a/x-pack/plugins/canvas/public/components/loading/__tests__/loading.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import expect from '@kbn/expect'; -import { shallow } from 'enzyme'; -import { EuiLoadingSpinner, EuiIcon } from '@elastic/eui'; -import { Loading } from '../loading'; - -describe('', () => { - it('uses EuiIcon by default', () => { - const wrapper = shallow(); - expect(wrapper.contains()).to.be.ok; - expect(wrapper.contains()).to.not.be.ok; - }); - - it('uses EuiLoadingSpinner when animating', () => { - const wrapper = shallow(); - expect(wrapper.contains()).to.not.be.ok; - expect(wrapper.contains()).to.be.ok; - }); -}); diff --git a/x-pack/plugins/canvas/public/components/loading/loading.test.tsx b/x-pack/plugins/canvas/public/components/loading/loading.test.tsx new file mode 100644 index 0000000000000..004ecc19c42e2 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/loading/loading.test.tsx @@ -0,0 +1,36 @@ +/* + * 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 { shallow } from 'enzyme'; +import { Loading } from './loading'; + +describe('', () => { + it('uses EuiIcon by default', () => { + expect(shallow()).toMatchInlineSnapshot(` +
+ +
+ `); + }); + + it('uses EuiLoadingSpinner when animating', () => { + expect(shallow()).toMatchInlineSnapshot(` +
+ +
+ `); + }); +}); diff --git a/x-pack/plugins/canvas/public/functions/__tests__/asset.js b/x-pack/plugins/canvas/public/functions/__tests__/asset.js deleted file mode 100644 index c21faf9a2e227..0000000000000 --- a/x-pack/plugins/canvas/public/functions/__tests__/asset.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; -import { asset } from '../asset'; - -// TODO: restore this test -// will require the ability to mock the store, or somehow remove the function's dependency on getState -describe.skip('asset', () => { - const fn = functionWrapper(asset); - - it('throws if asset could not be retrieved by ID', () => { - const throwsErr = () => { - return fn(null, { id: 'boo' }); - }; - expect(throwsErr).to.throwException((err) => { - expect(err.message).to.be('Could not get the asset by ID: boo'); - }); - }); - - it('returns the asset for found asset ID', () => { - expect(fn(null, { id: 'yay' })).to.be('here is your image'); - }); -}); diff --git a/x-pack/plugins/canvas/public/lib/__tests__/find_expression_type.js b/x-pack/plugins/canvas/public/lib/__tests__/find_expression_type.js deleted file mode 100644 index 58f5c5eb303bd..0000000000000 --- a/x-pack/plugins/canvas/public/lib/__tests__/find_expression_type.js +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -// import expect from 'expect.js'; -// import proxyquire from 'proxyquire'; -// import { Registry } from '../../../common/lib/registry'; - -// const registries = { -// datasource: new Registry(), -// transform: new Registry(), -// model: new Registry(), -// view: new Registry(), -// }; - -// const { findExpressionType } = proxyquire.noCallThru().load('../find_expression_type', { -// '../expression_types/datasource': { -// datasourceRegistry: registries.datasource, -// }, -// '../expression_types/transform': { -// transformRegistry: registries.transform, -// }, -// '../expression_types/model': { -// modelRegistry: registries.model, -// }, -// '../expression_types/view': { -// viewRegistry: registries.view, -// }, -// }); - -// describe('findExpressionType', () => { -// let expTypes; - -// beforeEach(() => { -// expTypes = []; -// const keys = Object.keys(registries); -// keys.forEach(key => { -// const reg = registries[key]; -// reg.reset(); - -// const expObj = () => ({ -// name: `__test_${key}`, -// key, -// }); -// expTypes.push(expObj); -// reg.register(expObj); -// }); -// }); - -// describe('all types', () => { -// it('returns the matching item, by name', () => { -// const match = findExpressionType('__test_model'); -// expect(match).to.eql(expTypes[2]()); -// }); - -// it('returns null when nothing is found', () => { -// const match = findExpressionType('@@nope_nope_nope'); -// expect(match).to.equal(null); -// }); - -// it('throws with multiple matches', () => { -// const commonName = 'commonName'; -// registries.transform.register(() => ({ -// name: commonName, -// })); -// registries.model.register(() => ({ -// name: commonName, -// })); - -// const check = () => { -// findExpressionType(commonName); -// }; -// expect(check).to.throwException(/Found multiple expressions/i); -// }); -// }); - -// describe('specific type', () => { -// it('return the match item, by name and type', () => { -// const match = findExpressionType('__test_view', 'view'); -// expect(match).to.eql(expTypes[3]()); -// }); - -// it('returns null with no match by name and type', () => { -// const match = findExpressionType('__test_view', 'datasource'); -// expect(match).to.equal(null); -// }); -// }); -// }); - -// TODO: restore this test -// proxyquire can not be used to inject mock registries - -describe.skip('findExpressionType', () => {}); diff --git a/x-pack/plugins/canvas/public/lib/__tests__/history_provider.js b/x-pack/plugins/canvas/public/lib/__tests__/history_provider.js deleted file mode 100644 index 99d8305768240..0000000000000 --- a/x-pack/plugins/canvas/public/lib/__tests__/history_provider.js +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import lzString from 'lz-string'; -import { historyProvider } from '../history_provider'; - -function createState() { - return { - transient: { - selectedPage: 'page-f3ce-4bb7-86c8-0417606d6592', - selectedToplevelNodes: ['element-d88c-4bbd-9453-db22e949b92e'], - resolvedArgs: {}, - }, - persistent: { - schemaVersion: 0, - time: new Date().getTime(), - }, - }; -} - -describe.skip('historyProvider', () => { - let history; - let state; - - beforeEach(() => { - history = historyProvider(); - state = createState(); - }); - - describe('instances', () => { - it('should return the same instance for the same window object', () => { - expect(historyProvider()).to.equal(history); - }); - - it('should return different instance for different window object', () => { - const newWindow = {}; - expect(historyProvider(newWindow)).not.to.be(history); - }); - }); - - describe('push updates', () => { - beforeEach(() => { - history.push(state); - }); - - afterEach(() => { - // reset state back to initial after each test - history.undo(); - }); - - describe('push', () => { - it('should add state to location', () => { - expect(history.getLocation().state).to.eql(state); - }); - - it('should push compressed state into history', () => { - const hist = history.historyInstance; - expect(hist.location.state).to.equal(lzString.compress(JSON.stringify(state))); - }); - }); - - describe.skip('undo', () => { - it('should move history back', () => { - // pushed location has state value - expect(history.getLocation().state).to.eql(state); - - // back to initial location with null state - history.undo(); - expect(history.getLocation().state).to.be(null); - }); - }); - - describe.skip('redo', () => { - it('should move history forward', () => { - // back to initial location, with null state - history.undo(); - expect(history.getLocation().state).to.be(null); - - // return to pushed location, with state value - history.redo(); - expect(history.getLocation().state).to.eql(state); - }); - }); - }); - - describe.skip('replace updates', () => { - beforeEach(() => { - history.replace(state); - }); - - afterEach(() => { - // reset history to default after each test - history.replace(null); - }); - - describe('replace', () => { - it('should replace state in window history', () => { - expect(history.getLocation().state).to.eql(state); - }); - - it('should replace compressed state into history', () => { - const hist = history.historyInstance; - expect(hist.location.state).to.equal(lzString.compress(JSON.stringify(state))); - }); - }); - }); - - describe('onChange', () => { - const createOnceHandler = (history, done, fn) => { - const teardown = history.onChange((location, prevLocation) => { - if (typeof fn === 'function') { - fn(location, prevLocation); - } - teardown(); - done(); - }); - }; - - it('should return a method to remove the listener', () => { - const handler = () => 'hello world'; - const teardownFn = history.onChange(handler); - - expect(teardownFn).to.be.a('function'); - - // teardown the listener - teardownFn(); - }); - - it('should call handler on state change', (done) => { - createOnceHandler(history, done, (loc) => { - expect(loc).to.be.a('object'); - }); - - history.push({}); - }); - - it('should pass location object to handler', (done) => { - createOnceHandler(history, done, (location) => { - expect(location.pathname).to.be.a('string'); - expect(location.hash).to.be.a('string'); - expect(location.state).to.be.an('object'); - expect(location.action).to.equal('push'); - }); - - history.push(state); - }); - - it('should pass decompressed state to handler', (done) => { - createOnceHandler(history, done, ({ state: curState }) => { - expect(curState).to.eql(state); - }); - - history.push(state); - }); - - it('should pass in the previous location object to handler', (done) => { - createOnceHandler(history, done, (location, prevLocation) => { - expect(prevLocation.pathname).to.be.a('string'); - expect(prevLocation.hash).to.be.a('string'); - expect(prevLocation.state).to.be(null); - expect(prevLocation.action).to.equal('push'); - }); - - history.push(state); - }); - }); - - describe('resetOnChange', () => { - // the history onChange handler was made async and now there's no way to know when the handler was called - // TODO: restore these tests. - it.skip('removes listeners', () => { - const createHandler = () => { - let callCount = 0; - - function handlerFn() { - callCount += 1; - } - handlerFn.getCallCount = () => callCount; - - return handlerFn; - }; - - const handler1 = createHandler(); - const handler2 = createHandler(); - - // attach and test the first handler - history.onChange(handler1); - - expect(handler1.getCallCount()).to.equal(0); - history.push({}); - expect(handler1.getCallCount()).to.equal(1); - - // attach and test the second handler - history.onChange(handler2); - - expect(handler2.getCallCount()).to.equal(0); - history.push({}); - expect(handler1.getCallCount()).to.equal(2); - expect(handler2.getCallCount()).to.equal(1); - - // remove all handlers - history.resetOnChange(); - history.push({}); - expect(handler1.getCallCount()).to.equal(2); - expect(handler2.getCallCount()).to.equal(1); - }); - }); - - describe('parse', () => { - it('returns the decompressed object', () => { - history.push(state); - - const hist = history.historyInstance; - const rawState = hist.location.state; - - expect(rawState).to.be.a('string'); - expect(history.parse(rawState)).to.eql(state); - }); - - it('returns null with invalid JSON', () => { - expect(history.parse('hello')).to.be(null); - }); - }); - - describe('encode', () => { - it('returns the compressed string', () => { - history.push(state); - - const hist = history.historyInstance; - const rawState = hist.location.state; - - expect(rawState).to.be.a('string'); - expect(history.encode(state)).to.eql(rawState); - }); - }); -}); diff --git a/x-pack/plugins/canvas/public/lib/__tests__/modify_path.js b/x-pack/plugins/canvas/public/lib/__tests__/modify_path.js deleted file mode 100644 index 75454890f9717..0000000000000 --- a/x-pack/plugins/canvas/public/lib/__tests__/modify_path.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { prepend, append } from '../modify_path'; - -describe('modify paths', () => { - describe('prepend', () => { - it('prepends a string path', () => { - expect(prepend('a.b.c', '0')).to.eql([0, 'a', 'b', 'c']); - expect(prepend('a.b.c', ['0', '1'])).to.eql([0, 1, 'a', 'b', 'c']); - }); - - it('prepends an array path', () => { - expect(prepend(['a', 1, 'last'], '0')).to.eql([0, 'a', 1, 'last']); - expect(prepend(['a', 1, 'last'], [0, 1])).to.eql([0, 1, 'a', 1, 'last']); - }); - }); - - describe('append', () => { - it('appends to a string path', () => { - expect(append('one.2.3', 'zero')).to.eql(['one', 2, 3, 'zero']); - expect(append('one.2.3', ['zero', 'one'])).to.eql(['one', 2, 3, 'zero', 'one']); - }); - - it('appends to an array path', () => { - expect(append(['testString'], 'huzzah')).to.eql(['testString', 'huzzah']); - expect(append(['testString'], ['huzzah', 'yosh'])).to.eql(['testString', 'huzzah', 'yosh']); - }); - }); -}); diff --git a/x-pack/plugins/canvas/public/lib/__tests__/get_pretty_shortcut.test.ts b/x-pack/plugins/canvas/public/lib/get_pretty_shortcut.test.ts similarity index 98% rename from x-pack/plugins/canvas/public/lib/__tests__/get_pretty_shortcut.test.ts rename to x-pack/plugins/canvas/public/lib/get_pretty_shortcut.test.ts index 783b085f3da7e..95cffefde7b1c 100644 --- a/x-pack/plugins/canvas/public/lib/__tests__/get_pretty_shortcut.test.ts +++ b/x-pack/plugins/canvas/public/lib/get_pretty_shortcut.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getPrettyShortcut } from '../get_pretty_shortcut'; +import { getPrettyShortcut } from './get_pretty_shortcut'; describe('getPrettyShortcut', () => { test('uppercases shortcuts', () => { diff --git a/x-pack/plugins/canvas/public/lib/modify_path.test.ts b/x-pack/plugins/canvas/public/lib/modify_path.test.ts new file mode 100644 index 0000000000000..245b91ca4ccd4 --- /dev/null +++ b/x-pack/plugins/canvas/public/lib/modify_path.test.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. + */ + +import { prepend, append } from './modify_path'; + +describe('modify paths', () => { + describe('prepend', () => { + it('prepends a string path', () => { + expect(prepend('a.b.c', '0')).toEqual(['0', 'a', 'b', 'c']); + expect(prepend('a.b.c', ['0', '1'])).toEqual(['0', '1', 'a', 'b', 'c']); + }); + + it('prepends an array path', () => { + expect(prepend(['a', 1, 'last'], '0')).toEqual(['0', 'a', '1', 'last']); + expect(prepend(['a', 1, 'last'], [0, 1])).toEqual(['0', '1', 'a', '1', 'last']); + }); + }); + + describe('append', () => { + it('appends to a string path', () => { + expect(append('one.2.3', 'zero')).toEqual(['one', '2', '3', 'zero']); + expect(append('one.2.3', ['zero', 'one'])).toEqual(['one', '2', '3', 'zero', 'one']); + }); + + it('appends to an array path', () => { + expect(append(['testString'], 'huzzah')).toEqual(['testString', 'huzzah']); + expect(append(['testString'], ['huzzah', 'yosh'])).toEqual(['testString', 'huzzah', 'yosh']); + }); + }); +}); diff --git a/x-pack/plugins/canvas/public/lib/modify_path.js b/x-pack/plugins/canvas/public/lib/modify_path.ts similarity index 61% rename from x-pack/plugins/canvas/public/lib/modify_path.js rename to x-pack/plugins/canvas/public/lib/modify_path.ts index 714a616679bc9..a5b8f0316d23e 100644 --- a/x-pack/plugins/canvas/public/lib/modify_path.js +++ b/x-pack/plugins/canvas/public/lib/modify_path.ts @@ -6,14 +6,16 @@ import { toPath } from 'lodash'; -export function prepend(path, value) { +export type Path = Array; + +export function prepend(path: string | Path, value: string | Path): Path { return toPath(value).concat(toPath(path)); } -export function append(path, value) { +export function append(path: string | Path, value: string | Path): Path { return toPath(path).concat(toPath(value)); } -export function convert(path) { +export function convert(path: string | Path): Path { return toPath(path); } diff --git a/x-pack/plugins/canvas/public/lib/__tests__/readable_color.test.ts b/x-pack/plugins/canvas/public/lib/readable_color.test.ts similarity index 94% rename from x-pack/plugins/canvas/public/lib/__tests__/readable_color.test.ts rename to x-pack/plugins/canvas/public/lib/readable_color.test.ts index bd79655ca727b..ce7cf03c2889c 100644 --- a/x-pack/plugins/canvas/public/lib/__tests__/readable_color.test.ts +++ b/x-pack/plugins/canvas/public/lib/readable_color.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { readableColor } from '../readable_color'; +import { readableColor } from './readable_color'; describe('readableColor', () => { test('light', () => { diff --git a/x-pack/plugins/canvas/public/lib/__tests__/resolved_arg.js b/x-pack/plugins/canvas/public/lib/resolved_arg.test.ts similarity index 57% rename from x-pack/plugins/canvas/public/lib/__tests__/resolved_arg.js rename to x-pack/plugins/canvas/public/lib/resolved_arg.test.ts index 9e582ddd1858b..fac2023fb2ce5 100644 --- a/x-pack/plugins/canvas/public/lib/__tests__/resolved_arg.js +++ b/x-pack/plugins/canvas/public/lib/resolved_arg.test.ts @@ -4,39 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { getState, getValue, getError } from '../resolved_arg'; +import { getState, getValue, getError } from './resolved_arg'; describe('resolved arg helper', () => { describe('getState', () => { it('returns pending by default', () => { - expect(getState()).to.be(null); + expect(getState()).toBe(null); }); it('returns the state', () => { - expect(getState({ state: 'pending' })).to.equal('pending'); - expect(getState({ state: 'ready' })).to.equal('ready'); - expect(getState({ state: 'error' })).to.equal('error'); + expect(getState({ state: 'pending' })).toEqual('pending'); + expect(getState({ state: 'ready' })).toEqual('ready'); + expect(getState({ state: 'error' })).toEqual('error'); }); }); describe('getValue', () => { it('returns null by default', () => { - expect(getValue()).to.be(null); + expect(getValue()).toBe(null); }); it('returns the value', () => { - expect(getValue({ value: 'hello test' })).to.equal('hello test'); + expect(getValue({ value: 'hello test' })).toEqual('hello test'); }); }); describe('getError', () => { it('returns null by default', () => { - expect(getError()).to.be(null); + expect(getError()).toBe(null); }); it('returns null when state is not error', () => { - expect(getError({ state: 'pending', error: 'nope' })).to.be(null); + expect(getError({ state: 'pending', error: 'nope' })).toBe(null); }); it('returns the error', () => { @@ -46,8 +45,7 @@ describe('resolved arg helper', () => { error: new Error('i failed'), }; - expect(getError(arg)).to.be.an(Error); - expect(getError(arg).toString()).to.match(/i failed/); + expect(getError(arg)).toMatchInlineSnapshot(`[Error: i failed]`); }); }); }); diff --git a/x-pack/plugins/canvas/public/lib/resolved_arg.js b/x-pack/plugins/canvas/public/lib/resolved_arg.ts similarity index 75% rename from x-pack/plugins/canvas/public/lib/resolved_arg.js rename to x-pack/plugins/canvas/public/lib/resolved_arg.ts index 8a9da8e466f7e..77f8a6cf8e5e0 100644 --- a/x-pack/plugins/canvas/public/lib/resolved_arg.js +++ b/x-pack/plugins/canvas/public/lib/resolved_arg.ts @@ -6,15 +6,15 @@ import { get } from 'lodash'; -export function getState(resolvedArg) { +export function getState(resolvedArg?: any): any { return get(resolvedArg, 'state', null); } -export function getValue(resolvedArg) { +export function getValue(resolvedArg?: any): any { return get(resolvedArg, 'value', null); } -export function getError(resolvedArg) { +export function getError(resolvedArg?: any): any { if (getState(resolvedArg) !== 'error') { return null; } diff --git a/x-pack/plugins/canvas/public/lib/__tests__/time_interval.test.ts b/x-pack/plugins/canvas/public/lib/time_interval.test.ts similarity index 98% rename from x-pack/plugins/canvas/public/lib/__tests__/time_interval.test.ts rename to x-pack/plugins/canvas/public/lib/time_interval.test.ts index 2dab00631cce1..8a057793ead79 100644 --- a/x-pack/plugins/canvas/public/lib/__tests__/time_interval.test.ts +++ b/x-pack/plugins/canvas/public/lib/time_interval.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getTimeInterval, createTimeInterval, isValidTimeInterval } from '../time_interval'; +import { getTimeInterval, createTimeInterval, isValidTimeInterval } from './time_interval'; describe('time_interval', () => { test('getTimeInterval', () => { diff --git a/x-pack/plugins/canvas/public/state/actions/__tests__/elements.get_sibling_context.js b/x-pack/plugins/canvas/public/state/actions/__tests__/elements.get_sibling_context.js deleted file mode 100644 index 198ccb2ffc381..0000000000000 --- a/x-pack/plugins/canvas/public/state/actions/__tests__/elements.get_sibling_context.js +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { getSiblingContext } from '../elements'; - -const state = { - transient: { - resolvedArgs: { - 'element-foo': { - expressionContext: { - '0': { - state: 'ready', - value: { - type: 'datatable', - columns: [ - { name: 'project', type: 'string' }, - { name: 'cost', type: 'string' }, - { name: 'age', type: 'string' }, - ], - rows: [ - { project: 'pandas', cost: '500', age: '18' }, - { project: 'tigers', cost: '200', age: '12' }, - ], - }, - error: null, - }, - '1': { - state: 'ready', - value: { - type: 'datatable', - columns: [ - { name: 'project', type: 'string' }, - { name: 'cost', type: 'string' }, - { name: 'age', type: 'string' }, - ], - rows: [ - { project: 'tigers', cost: '200', age: '12' }, - { project: 'pandas', cost: '500', age: '18' }, - ], - }, - error: null, - }, - '2': { - state: 'ready', - value: { - type: 'pointseries', - columns: { - x: { type: 'string', role: 'dimension', expression: 'cost' }, - y: { type: 'string', role: 'dimension', expression: 'project' }, - color: { type: 'string', role: 'dimension', expression: 'project' }, - }, - rows: [ - { x: '200', y: 'tigers', color: 'tigers' }, - { x: '500', y: 'pandas', color: 'pandas' }, - ], - }, - error: null, - }, - }, - }, - }, - }, -}; - -describe('actions/elements getSiblingContext', () => { - it('should find context when a previous context value is found', () => { - // pointseries map - expect(getSiblingContext(state, 'element-foo', 2)).to.eql({ - index: 2, - context: { - type: 'pointseries', - columns: { - x: { type: 'string', role: 'dimension', expression: 'cost' }, - y: { type: 'string', role: 'dimension', expression: 'project' }, - color: { type: 'string', role: 'dimension', expression: 'project' }, - }, - rows: [ - { x: '200', y: 'tigers', color: 'tigers' }, - { x: '500', y: 'pandas', color: 'pandas' }, - ], - }, - }); - }); - - it('should find context when a previous context value is not found', () => { - // pointseries map - expect(getSiblingContext(state, 'element-foo', 1000)).to.eql({ - index: 2, - context: { - type: 'pointseries', - columns: { - x: { type: 'string', role: 'dimension', expression: 'cost' }, - y: { type: 'string', role: 'dimension', expression: 'project' }, - color: { type: 'string', role: 'dimension', expression: 'project' }, - }, - rows: [ - { x: '200', y: 'tigers', color: 'tigers' }, - { x: '500', y: 'pandas', color: 'pandas' }, - ], - }, - }); - }); -}); diff --git a/x-pack/plugins/canvas/public/state/actions/elements.test.js b/x-pack/plugins/canvas/public/state/actions/elements.test.js new file mode 100644 index 0000000000000..a790e81e65e25 --- /dev/null +++ b/x-pack/plugins/canvas/public/state/actions/elements.test.js @@ -0,0 +1,106 @@ +/* + * 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 { getSiblingContext } from './elements'; + +describe('getSiblingContext', () => { + const state = { + transient: { + resolvedArgs: { + 'element-foo': { + expressionContext: { + '0': { + state: 'ready', + value: { + type: 'datatable', + columns: [ + { name: 'project', type: 'string' }, + { name: 'cost', type: 'string' }, + { name: 'age', type: 'string' }, + ], + rows: [ + { project: 'pandas', cost: '500', age: '18' }, + { project: 'tigers', cost: '200', age: '12' }, + ], + }, + error: null, + }, + '1': { + state: 'ready', + value: { + type: 'datatable', + columns: [ + { name: 'project', type: 'string' }, + { name: 'cost', type: 'string' }, + { name: 'age', type: 'string' }, + ], + rows: [ + { project: 'tigers', cost: '200', age: '12' }, + { project: 'pandas', cost: '500', age: '18' }, + ], + }, + error: null, + }, + '2': { + state: 'ready', + value: { + type: 'pointseries', + columns: { + x: { type: 'string', role: 'dimension', expression: 'cost' }, + y: { type: 'string', role: 'dimension', expression: 'project' }, + color: { type: 'string', role: 'dimension', expression: 'project' }, + }, + rows: [ + { x: '200', y: 'tigers', color: 'tigers' }, + { x: '500', y: 'pandas', color: 'pandas' }, + ], + }, + error: null, + }, + }, + }, + }, + }, + }; + + it('should find context when a previous context value is found', () => { + // pointseries map + expect(getSiblingContext(state, 'element-foo', 2)).toEqual({ + index: 2, + context: { + type: 'pointseries', + columns: { + x: { type: 'string', role: 'dimension', expression: 'cost' }, + y: { type: 'string', role: 'dimension', expression: 'project' }, + color: { type: 'string', role: 'dimension', expression: 'project' }, + }, + rows: [ + { x: '200', y: 'tigers', color: 'tigers' }, + { x: '500', y: 'pandas', color: 'pandas' }, + ], + }, + }); + }); + + it('should find context when a previous context value is not found', () => { + // pointseries map + expect(getSiblingContext(state, 'element-foo', 1000)).toEqual({ + index: 2, + context: { + type: 'pointseries', + columns: { + x: { type: 'string', role: 'dimension', expression: 'cost' }, + y: { type: 'string', role: 'dimension', expression: 'project' }, + color: { type: 'string', role: 'dimension', expression: 'project' }, + }, + rows: [ + { x: '200', y: 'tigers', color: 'tigers' }, + { x: '500', y: 'pandas', color: 'pandas' }, + ], + }, + }); + }); +}); diff --git a/x-pack/plugins/canvas/public/state/middleware/in_flight.ts b/x-pack/plugins/canvas/public/state/middleware/in_flight.ts index 028b9f214133f..d564a44b0b5f7 100644 --- a/x-pack/plugins/canvas/public/state/middleware/in_flight.ts +++ b/x-pack/plugins/canvas/public/state/middleware/in_flight.ts @@ -9,7 +9,6 @@ import { loadingIndicator as defaultLoadingIndicator, LoadingIndicatorInterface, } from '../../lib/loading_indicator'; -// @ts-expect-error import { convert } from '../../lib/modify_path'; interface InFlightMiddlewareOptions { diff --git a/x-pack/plugins/canvas/public/state/reducers/__tests__/fixtures/action_creator.js b/x-pack/plugins/canvas/public/state/reducers/__fixtures__/action_creator.js similarity index 100% rename from x-pack/plugins/canvas/public/state/reducers/__tests__/fixtures/action_creator.js rename to x-pack/plugins/canvas/public/state/reducers/__fixtures__/action_creator.js diff --git a/x-pack/plugins/canvas/public/state/reducers/__tests__/elements.js b/x-pack/plugins/canvas/public/state/reducers/elements.test.js similarity index 78% rename from x-pack/plugins/canvas/public/state/reducers/__tests__/elements.js rename to x-pack/plugins/canvas/public/state/reducers/elements.test.js index e1f7509325a7a..23f684879ce06 100644 --- a/x-pack/plugins/canvas/public/state/reducers/__tests__/elements.js +++ b/x-pack/plugins/canvas/public/state/reducers/elements.test.js @@ -4,10 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { get } from 'lodash'; -import { elementsReducer } from '../elements'; -import { actionCreator } from './fixtures/action_creator'; +import { elementsReducer } from './elements'; +import { actionCreator } from './__fixtures__/action_creator'; describe('elements reducer', () => { let state; @@ -46,8 +44,8 @@ describe('elements reducer', () => { }); const newState = elementsReducer(state, action); - const newElement = get(newState, ['pages', 0, 'elements', 1]); + const newElement = newState?.pages?.[0]?.elements?.[1]; - expect(newElement).to.eql(expected); + expect(newElement).toEqual(expected); }); }); diff --git a/x-pack/plugins/canvas/public/state/reducers/__tests__/resolved_args.js b/x-pack/plugins/canvas/public/state/reducers/resolved_args.test.js similarity index 62% rename from x-pack/plugins/canvas/public/state/reducers/__tests__/resolved_args.js rename to x-pack/plugins/canvas/public/state/reducers/resolved_args.test.js index ee1d0fc1ca9ba..74f1544403e67 100644 --- a/x-pack/plugins/canvas/public/state/reducers/__tests__/resolved_args.js +++ b/x-pack/plugins/canvas/public/state/reducers/resolved_args.test.js @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import * as actions from '../../actions/resolved_args'; -import { flushContextAfterIndex } from '../../actions/elements'; -import { resolvedArgsReducer } from '../resolved_args'; -import { actionCreator } from './fixtures/action_creator'; +import * as actions from '../actions/resolved_args'; +import { flushContextAfterIndex } from '../actions/elements'; +import { resolvedArgsReducer } from './resolved_args'; +import { actionCreator } from './__fixtures__/action_creator'; describe('resolved args reducer', () => { let state; @@ -41,13 +40,15 @@ describe('resolved args reducer', () => { }); const newState = resolvedArgsReducer(state, action); - expect(newState.resolvedArgs['element-1']).to.eql([ - { - state: 'pending', - value: null, - error: null, - }, - ]); + expect(newState.resolvedArgs['element-1']).toMatchInlineSnapshot(` + Object { + "0": Object { + "error": null, + "state": "pending", + "value": null, + }, + } + `); }); it('sets state to loading, with array path', () => { @@ -56,13 +57,15 @@ describe('resolved args reducer', () => { }); const newState = resolvedArgsReducer(state, action); - expect(newState.resolvedArgs['element-1']).to.eql([ - { - state: 'pending', - value: null, - error: null, - }, - ]); + expect(newState.resolvedArgs['element-1']).toMatchInlineSnapshot(` + Object { + "0": Object { + "error": null, + "state": "pending", + "value": null, + }, + } + `); }); }); @@ -75,13 +78,15 @@ describe('resolved args reducer', () => { }); const newState = resolvedArgsReducer(state, action); - expect(newState.resolvedArgs['element-1']).to.eql([ - { - state: 'ready', - value, - error: null, - }, - ]); + expect(newState.resolvedArgs['element-1']).toMatchInlineSnapshot(` + Object { + "0": Object { + "error": null, + "state": "ready", + "value": "hello world", + }, + } + `); }); it('handles error values', () => { @@ -92,13 +97,15 @@ describe('resolved args reducer', () => { }); const newState = resolvedArgsReducer(state, action); - expect(newState.resolvedArgs['element-1']).to.eql([ - { - state: 'error', - value: null, - error: err, - }, - ]); + expect(newState.resolvedArgs['element-1']).toMatchInlineSnapshot(` + Object { + "0": Object { + "error": [Error: farewell world], + "state": "error", + "value": null, + }, + } + `); }); it('preserves old value on error', () => { @@ -109,11 +116,13 @@ describe('resolved args reducer', () => { }); const newState = resolvedArgsReducer(state, action); - expect(newState.resolvedArgs['element-0'][0]).to.eql({ - state: 'error', - value: 'testing', - error: err, - }); + expect(newState.resolvedArgs['element-0'][0]).toMatchInlineSnapshot(` + Object { + "error": [Error: farewell world], + "state": "error", + "value": "testing", + } + `); }); }); @@ -124,14 +133,15 @@ describe('resolved args reducer', () => { }); const newState = resolvedArgsReducer(state, action); - expect(newState.resolvedArgs['element-0']).to.have.length(1); - expect(newState.resolvedArgs['element-0']).to.eql([ - { - state: 'ready', - value: 'testing', - error: null, - }, - ]); + expect(newState.resolvedArgs['element-0']).toMatchInlineSnapshot(` + Array [ + Object { + "error": null, + "state": "ready", + "value": "testing", + }, + ] + `); }); it('deeply removes resolved values', () => { @@ -140,7 +150,7 @@ describe('resolved args reducer', () => { }); const newState = resolvedArgsReducer(state, action); - expect(newState.resolvedArgs['element-0']).to.be(undefined); + expect(newState.resolvedArgs['element-0']).toBe(undefined); }); }); @@ -183,12 +193,22 @@ describe('resolved args reducer', () => { }); const newState = resolvedArgsReducer(state, action); - expect(newState.resolvedArgs['element-1']).to.eql({ - expressionContext: { - '1': { state: 'ready', value: 'test-1', error: null }, - '2': { state: 'ready', value: 'test-2', error: null }, - }, - }); + expect(newState.resolvedArgs['element-1']).toMatchInlineSnapshot(` + Object { + "expressionContext": Object { + "1": Object { + "error": null, + "state": "ready", + "value": "test-1", + }, + "2": Object { + "error": null, + "state": "ready", + "value": "test-2", + }, + }, + } + `); }); }); }); diff --git a/x-pack/plugins/canvas/public/state/selectors/__tests__/resolved_args.js b/x-pack/plugins/canvas/public/state/selectors/resolved_args.test.js similarity index 55% rename from x-pack/plugins/canvas/public/state/selectors/__tests__/resolved_args.js rename to x-pack/plugins/canvas/public/state/selectors/resolved_args.test.js index 3157201927854..1710d6704f980 100644 --- a/x-pack/plugins/canvas/public/state/selectors/__tests__/resolved_args.js +++ b/x-pack/plugins/canvas/public/state/selectors/resolved_args.test.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import * as selector from '../resolved_args'; +import * as selector from './resolved_args'; describe('resolved args selector', () => { let state; @@ -35,21 +34,20 @@ describe('resolved args selector', () => { }); it('getValue returns the state', () => { - expect(selector.getState(state, 'test1')).to.equal('ready'); - expect(selector.getState(state, 'test2')).to.equal('pending'); - expect(selector.getState(state, 'test3')).to.equal('error'); + expect(selector.getState(state, 'test1')).toEqual('ready'); + expect(selector.getState(state, 'test2')).toEqual('pending'); + expect(selector.getState(state, 'test3')).toEqual('error'); }); it('getValue returns the value', () => { - expect(selector.getValue(state, 'test1')).to.equal('test value'); - expect(selector.getValue(state, 'test2')).to.equal(null); - expect(selector.getValue(state, 'test3')).to.equal('some old value'); + expect(selector.getValue(state, 'test1')).toEqual('test value'); + expect(selector.getValue(state, 'test2')).toEqual(null); + expect(selector.getValue(state, 'test3')).toEqual('some old value'); }); it('getError returns the error', () => { - expect(selector.getError(state, 'test1')).to.equal(null); - expect(selector.getError(state, 'test2')).to.equal(null); - expect(selector.getError(state, 'test3')).to.be.an(Error); - expect(selector.getError(state, 'test3').toString()).to.match(/i\ have\ failed$/); + expect(selector.getError(state, 'test1')).toEqual(null); + expect(selector.getError(state, 'test2')).toEqual(null); + expect(selector.getError(state, 'test3')).toMatchInlineSnapshot(`[Error: i have failed]`); }); }); diff --git a/x-pack/plugins/canvas/public/state/selectors/resolved_args.ts b/x-pack/plugins/canvas/public/state/selectors/resolved_args.ts index 770d4403f8587..b557ff04921ca 100644 --- a/x-pack/plugins/canvas/public/state/selectors/resolved_args.ts +++ b/x-pack/plugins/canvas/public/state/selectors/resolved_args.ts @@ -5,9 +5,7 @@ */ import { get } from 'lodash'; -// @ts-expect-error untyped local import * as argHelper from '../../lib/resolved_arg'; -// @ts-expect-error untyped local import { prepend } from '../../lib/modify_path'; import { State } from '../../../types'; diff --git a/x-pack/plugins/canvas/public/state/selectors/__tests__/workpad.js b/x-pack/plugins/canvas/public/state/selectors/workpad.test.js similarity index 80% rename from x-pack/plugins/canvas/public/state/selectors/__tests__/workpad.js rename to x-pack/plugins/canvas/public/state/selectors/workpad.test.js index 5fdc662c592cc..d5f7e003af858 100644 --- a/x-pack/plugins/canvas/public/state/selectors/__tests__/workpad.js +++ b/x-pack/plugins/canvas/public/state/selectors/workpad.test.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import * as selector from '../workpad'; +import * as selector from './workpad'; describe('workpad selectors', () => { let asts; @@ -125,42 +124,42 @@ describe('workpad selectors', () => { describe('empty state', () => { it('returns undefined', () => { - expect(selector.getSelectedPage({})).to.be(undefined); - expect(selector.getPageById({}, 'page-1')).to.be(undefined); - expect(selector.getSelectedElement({})).to.be(undefined); - expect(selector.getElementById({}, 'element-1')).to.be(undefined); - expect(selector.getResolvedArgs({}, 'element-1')).to.be(undefined); - expect(selector.getSelectedResolvedArgs({})).to.be(undefined); - expect(selector.isWriteable({})).to.be(true); + expect(selector.getSelectedPage({})).toBe(undefined); + expect(selector.getPageById({}, 'page-1')).toBe(undefined); + expect(selector.getSelectedElement({})).toBe(undefined); + expect(selector.getElementById({}, 'element-1')).toBe(undefined); + expect(selector.getResolvedArgs({}, 'element-1')).toBe(undefined); + expect(selector.getSelectedResolvedArgs({})).toBe(undefined); + expect(selector.isWriteable({})).toBe(true); }); }); describe('getSelectedPage', () => { it('returns the selected page', () => { - expect(selector.getSelectedPage(state)).to.equal('page-1'); + expect(selector.getSelectedPage(state)).toEqual('page-1'); }); }); describe('getPages', () => { it('return an empty array with no pages', () => { - expect(selector.getPages({})).to.eql([]); + expect(selector.getPages({})).toEqual([]); }); it('returns all pages in persisent state', () => { - expect(selector.getPages(state)).to.eql(state.persistent.workpad.pages); + expect(selector.getPages(state)).toEqual(state.persistent.workpad.pages); }); }); describe('getPageById', () => { it('should return matching page', () => { - expect(selector.getPageById(state, 'page-1')).to.eql(state.persistent.workpad.pages[0]); + expect(selector.getPageById(state, 'page-1')).toEqual(state.persistent.workpad.pages[0]); }); }); describe('getSelectedElement', () => { it('returns selected element', () => { const { elements } = state.persistent.workpad.pages[0]; - expect(selector.getSelectedElement(state)).to.eql({ + expect(selector.getSelectedElement(state)).toEqual({ ...elements[1], ast: asts['element-1'], }); @@ -169,7 +168,7 @@ describe('workpad selectors', () => { describe('getElements', () => { it('is an empty array with no state', () => { - expect(selector.getElements({})).to.eql([]); + expect(selector.getElements({})).toEqual([]); }); it('returns all elements on the page', () => { @@ -179,18 +178,18 @@ describe('workpad selectors', () => { ...element, ast: asts[element.id], })); - expect(selector.getElements(state)).to.eql(expected); + expect(selector.getElements(state)).toEqual(expected); }); }); describe('getElementById', () => { it('returns element matching id', () => { const { elements } = state.persistent.workpad.pages[0]; - expect(selector.getElementById(state, 'element-0')).to.eql({ + expect(selector.getElementById(state, 'element-0')).toEqual({ ...elements[0], ast: asts['element-0'], }); - expect(selector.getElementById(state, 'element-1')).to.eql({ + expect(selector.getElementById(state, 'element-1')).toEqual({ ...elements[1], ast: asts['element-1'], }); @@ -199,18 +198,18 @@ describe('workpad selectors', () => { describe('getResolvedArgs', () => { it('returns resolved args by element id', () => { - expect(selector.getResolvedArgs(state, 'element-0')).to.equal('test resolved arg, el 0'); + expect(selector.getResolvedArgs(state, 'element-0')).toEqual('test resolved arg, el 0'); }); it('returns resolved args at given path', () => { const arg = selector.getResolvedArgs(state, 'element-2', 'example1'); - expect(arg).to.equal('first thing'); + expect(arg).toEqual('first thing'); }); }); describe('getSelectedResolvedArgs', () => { it('returns resolved args for selected element', () => { - expect(selector.getSelectedResolvedArgs(state)).to.equal('test resolved arg, el 1'); + expect(selector.getSelectedResolvedArgs(state)).toEqual('test resolved arg, el 1'); }); it('returns resolved args at given path', () => { @@ -222,7 +221,7 @@ describe('workpad selectors', () => { }, }; const arg = selector.getSelectedResolvedArgs(tmpState, 'example2'); - expect(arg).to.eql(['why not', 'an array?']); + expect(arg).toEqual(['why not', 'an array?']); }); it('returns resolved args at given deep path', () => { @@ -234,14 +233,14 @@ describe('workpad selectors', () => { }, }; const arg = selector.getSelectedResolvedArgs(tmpState, ['example3', 'deeper', 'object']); - expect(arg).to.be(true); + expect(arg).toBe(true); }); }); describe('getGlobalFilters', () => { it('gets filters from all elements', () => { const filters = selector.getGlobalFilters(state); - expect(filters).to.eql([ + expect(filters).toEqual([ 'exactly value="beats" column="project"', 'timefilter filterGroup=one column=@timestamp from=now-24h to=now', ]); @@ -249,17 +248,14 @@ describe('workpad selectors', () => { it('gets returns empty array with no elements', () => { const filters = selector.getGlobalFilters({}); - expect(filters).to.be.an(Array); - expect(filters).to.have.length(0); + expect(filters).toEqual([]); }); }); describe('getGlobalFilterGroups', () => { it('gets filter group from elements', () => { const filterGroups = selector.getGlobalFilterGroups(state); - expect(filterGroups).to.be.an(Array); - expect(filterGroups).to.have.length(1); - expect(filterGroups[0]).to.equal('one'); + expect(filterGroups).toEqual(['one']); }); it('gets all unique filter groups', () => { @@ -282,7 +278,7 @@ describe('workpad selectors', () => { }); // filters are alphabetical - expect(filterGroups).to.eql(['one', 'two']); + expect(filterGroups).toEqual(['one', 'two']); }); it('gets filter groups in filter function args', () => { @@ -311,13 +307,13 @@ describe('workpad selectors', () => { // {string two} is skipped, only primitive values are extracted // filterGroup=one and {filters one} are de-duped // filters are alphabetical - expect(filterGroups).to.eql(['four', 'one', 'three']); + expect(filterGroups).toEqual(['four', 'one', 'three']); }); }); describe('isWriteable', () => { it('returns boolean for if the workpad is writeable', () => { - expect(selector.isWriteable(state)).to.equal(false); + expect(selector.isWriteable(state)).toEqual(false); }); }); }); diff --git a/x-pack/plugins/canvas/public/state/selectors/workpad.ts b/x-pack/plugins/canvas/public/state/selectors/workpad.ts index a677bcaf29e61..b05615b7930c5 100644 --- a/x-pack/plugins/canvas/public/state/selectors/workpad.ts +++ b/x-pack/plugins/canvas/public/state/selectors/workpad.ts @@ -7,7 +7,6 @@ import { get, omit } from 'lodash'; // @ts-expect-error untyped local import { safeElementFromExpression, fromExpression } from '@kbn/interpreter/common'; -// @ts-expect-error untyped local import { append } from '../../lib/modify_path'; import { getAssets } from './assets'; import { From dd4796cfdd2a7e6081a8c16d0e5fe0c8593b87f3 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 27 Jul 2020 11:07:58 -0700 Subject: [PATCH 47/96] Remove karma (#73126) Co-authored-by: spalger --- .../contributing/development-tests.asciidoc | 2 - .../development-unit-tests.asciidoc | 33 - package.json | 11 - .../lib/get_webpack_config.js | 4 - packages/kbn-plugin-generator/README.md | 4 - packages/kbn-plugin-helpers/README.md | 1 - packages/kbn-plugin-helpers/src/cli.ts | 19 - packages/kbn-plugin-helpers/src/lib/tasks.ts | 6 - .../src/tasks/test/all/README.md | 3 - .../src/tasks/test/all/index.ts | 20 - .../src/tasks/test/all/test_all_task.ts | 25 - .../src/tasks/test/karma/README.md | 60 -- .../src/tasks/test/karma/index.ts | 20 - .../src/tasks/test/karma/test_karma_task.ts | 42 -- .../__fixtures__/index.ts | 1 - .../__fixtures__/karma_report.xml | 33 - .../add_messages_to_report.test.ts | 84 +-- .../get_failures.test.ts | 27 +- .../report_metadata.test.ts | 5 +- packages/kbn-ui-shared-deps/theme.ts | 2 +- src/core/MIGRATION.md | 8 - src/core/public/legacy/legacy_service.ts | 2 +- src/core/server/http/http_config.ts | 16 +- src/dev/build/tasks/copy_source_task.ts | 1 - src/dev/jest/config.js | 1 - src/dev/precommit_hook/casing_check_config.js | 1 - .../tests_bundle/find_source_files.js | 64 -- src/legacy/core_plugins/tests_bundle/index.js | 180 ----- .../core_plugins/tests_bundle/package.json | 4 - .../tests_bundle/public/index.scss | 4 - .../tests_bundle/tests_entry_template.js | 158 ----- .../webpackShims/angular-mocks.js | 21 - src/legacy/ui/public/test_harness/.eslintrc | 2 - src/legacy/ui/public/test_harness/index.js | 20 - .../ui/public/test_harness/test_harness.css | 22 - .../ui/public/test_harness/test_harness.js | 89 --- .../test_sharding/find_test_bundle_url.js | 39 -- .../test_sharding/get_shard_num.js | 47 -- .../get_sharding_params_from_url.js | 45 -- .../test_harness/test_sharding/index.js | 20 - .../test_sharding/setup_test_sharding.js | 69 -- .../setup_top_level_describe_filter.js | 125 ---- .../ui/ui_exports/ui_export_defaults.js | 1 - tasks/config/karma.js | 207 ------ tasks/config/run.js | 72 +- tasks/jenkins.js | 1 - tasks/test.js | 27 +- test/scripts/jenkins_xpack.sh | 6 +- x-pack/README.md | 8 - x-pack/gulpfile.js | 4 - x-pack/package.json | 2 - x-pack/plugins/canvas/scripts/test_browser.js | 7 - x-pack/plugins/canvas/scripts/test_dev.js | 7 - x-pack/tasks/test.ts | 31 - yarn.lock | 629 ++---------------- 55 files changed, 76 insertions(+), 2266 deletions(-) delete mode 100644 packages/kbn-plugin-helpers/src/tasks/test/all/README.md delete mode 100644 packages/kbn-plugin-helpers/src/tasks/test/all/index.ts delete mode 100644 packages/kbn-plugin-helpers/src/tasks/test/all/test_all_task.ts delete mode 100644 packages/kbn-plugin-helpers/src/tasks/test/karma/README.md delete mode 100644 packages/kbn-plugin-helpers/src/tasks/test/karma/index.ts delete mode 100644 packages/kbn-plugin-helpers/src/tasks/test/karma/test_karma_task.ts delete mode 100644 packages/kbn-test/src/failed_tests_reporter/__fixtures__/karma_report.xml delete mode 100644 src/legacy/core_plugins/tests_bundle/find_source_files.js delete mode 100644 src/legacy/core_plugins/tests_bundle/index.js delete mode 100644 src/legacy/core_plugins/tests_bundle/package.json delete mode 100644 src/legacy/core_plugins/tests_bundle/public/index.scss delete mode 100644 src/legacy/core_plugins/tests_bundle/tests_entry_template.js delete mode 100644 src/legacy/core_plugins/tests_bundle/webpackShims/angular-mocks.js delete mode 100644 src/legacy/ui/public/test_harness/.eslintrc delete mode 100644 src/legacy/ui/public/test_harness/index.js delete mode 100644 src/legacy/ui/public/test_harness/test_harness.css delete mode 100644 src/legacy/ui/public/test_harness/test_harness.js delete mode 100644 src/legacy/ui/public/test_harness/test_sharding/find_test_bundle_url.js delete mode 100644 src/legacy/ui/public/test_harness/test_sharding/get_shard_num.js delete mode 100644 src/legacy/ui/public/test_harness/test_sharding/get_sharding_params_from_url.js delete mode 100644 src/legacy/ui/public/test_harness/test_sharding/index.js delete mode 100644 src/legacy/ui/public/test_harness/test_sharding/setup_test_sharding.js delete mode 100644 src/legacy/ui/public/test_harness/test_sharding/setup_top_level_describe_filter.js delete mode 100644 tasks/config/karma.js delete mode 100644 x-pack/plugins/canvas/scripts/test_browser.js delete mode 100644 x-pack/plugins/canvas/scripts/test_dev.js delete mode 100644 x-pack/tasks/test.ts diff --git a/docs/developer/contributing/development-tests.asciidoc b/docs/developer/contributing/development-tests.asciidoc index 78a2a90b69ce5..2e40f664faba9 100644 --- a/docs/developer/contributing/development-tests.asciidoc +++ b/docs/developer/contributing/development-tests.asciidoc @@ -26,8 +26,6 @@ root) |Functional |`test/*integration/**/config.js` `test/*functional/**/config.js` `test/accessibility/config.js` |`yarn test:ftr:server --config test/[directory]/config.js``yarn test:ftr:runner --config test/[directory]/config.js --grep=regexp` - -|Karma |`src/**/public/__tests__/*.js` |`yarn test:karma:debug` |=== For X-Pack tests located in `x-pack/` see diff --git a/docs/developer/contributing/development-unit-tests.asciidoc b/docs/developer/contributing/development-unit-tests.asciidoc index 8b4954150bb5b..5322106b17ac1 100644 --- a/docs/developer/contributing/development-unit-tests.asciidoc +++ b/docs/developer/contributing/development-unit-tests.asciidoc @@ -95,38 +95,6 @@ to proceed in this mode. node scripts/mocha --debug ---- -With `yarn test:karma`, you can run only the browser tests. Coverage -reports are available for browser tests by running -`yarn test:coverage`. You can find the results under the `coverage/` -directory that will be created upon completion. - -[source,bash] ----- -yarn test:karma ----- - -Using `yarn test:karma:debug` initializes an environment for debugging -the browser tests. Includes an dedicated instance of the {kib} server -for building the test bundle, and a karma server. When running this task -the build is optimized for the first time and then a karma-owned -instance of the browser is opened. Click the "`debug`" button to open a -new tab that executes the unit tests. - -[source,bash] ----- -yarn test:karma:debug ----- - -In the screenshot below, you’ll notice the URL is -`localhost:9876/debug.html`. You can append a `grep` query parameter -to this URL and set it to a string value which will be used to exclude -tests which don’t match. For example, if you changed the URL to -`localhost:9876/debug.html?query=my test` and then refreshed the -browser, you’d only see tests run which contain "`my test`" in the test -description. - -image:http://i.imgur.com/DwHxgfq.png[Browser test debugging] - [discrete] === Unit Testing Plugins @@ -141,5 +109,4 @@ command from your plugin: [source,bash] ---- yarn test:mocha -yarn test:karma:debug # remove the debug flag to run them once and close ---- \ No newline at end of file diff --git a/package.json b/package.json index ee91c59a8fda6..0c49ec26be194 100644 --- a/package.json +++ b/package.json @@ -41,8 +41,6 @@ "kbn": "node scripts/kbn", "es": "node scripts/es", "test": "grunt test", - "test:karma": "grunt test:karma", - "test:karma:debug": "grunt test:karmaDebug", "test:jest": "node scripts/jest", "test:jest_integration": "node scripts/jest_integration", "test:mocha": "node scripts/mocha", @@ -447,7 +445,6 @@ "grunt-available-tasks": "^0.6.3", "grunt-cli": "^1.2.0", "grunt-contrib-watch": "^1.1.0", - "grunt-karma": "^3.0.2", "grunt-peg": "^2.0.1", "grunt-run": "0.8.1", "gulp-babel": "^8.0.0", @@ -465,14 +462,6 @@ "jest-raw-loader": "^1.0.1", "jimp": "^0.9.6", "json5": "^1.0.1", - "karma": "5.0.2", - "karma-chrome-launcher": "2.2.0", - "karma-coverage": "1.1.2", - "karma-firefox-launcher": "1.1.0", - "karma-ie-launcher": "1.0.0", - "karma-junit-reporter": "1.2.0", - "karma-mocha": "2.0.0", - "karma-safari-launcher": "1.0.0", "license-checker": "^16.0.0", "listr": "^0.14.1", "load-grunt-config": "^3.0.1", diff --git a/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js b/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js index 6cb2f3d2901d3..baf5baaf916aa 100755 --- a/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js +++ b/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js @@ -28,13 +28,9 @@ exports.getWebpackConfig = function (kibanaPath, projectRoot, config) { const alias = { // Kibana defaults https://github.com/elastic/kibana/blob/6998f074542e8c7b32955db159d15661aca253d7/src/legacy/ui/ui_bundler_env.js#L30-L36 ui: fromKibana('src/legacy/ui/public'), - test_harness: fromKibana('src/test_harness/public'), // Dev defaults for test bundle https://github.com/elastic/kibana/blob/6998f074542e8c7b32955db159d15661aca253d7/src/core_plugins/tests_bundle/index.js#L73-L78 ng_mock$: fromKibana('src/test_utils/public/ng_mock'), - 'angular-mocks$': fromKibana( - 'src/legacy/core_plugins/tests_bundle/webpackShims/angular-mocks.js' - ), fixtures: fromKibana('src/fixtures'), test_utils: fromKibana('src/test_utils/public'), }; diff --git a/packages/kbn-plugin-generator/README.md b/packages/kbn-plugin-generator/README.md index 95de0e93fd075..6ad665f9b87f8 100644 --- a/packages/kbn-plugin-generator/README.md +++ b/packages/kbn-plugin-generator/README.md @@ -71,10 +71,6 @@ Generated plugins receive a handful of scripts that can be used during developme Build a distributable archive of your plugin. - - `yarn test:karma` - - Run the browser tests in a real web browser. - - `yarn test:mocha` Run the server tests using mocha. diff --git a/packages/kbn-plugin-helpers/README.md b/packages/kbn-plugin-helpers/README.md index 4c648fd9bde8c..d7ed3106c1ceb 100644 --- a/packages/kbn-plugin-helpers/README.md +++ b/packages/kbn-plugin-helpers/README.md @@ -30,7 +30,6 @@ $ plugin-helpers help start Start kibana and have it include this plugin build [options] [files...] Build a distributable archive test Run the server and browser tests - test:karma [options] Run the browser tests in a real web browser test:mocha [files...] Run the server tests using mocha Options: diff --git a/packages/kbn-plugin-helpers/src/cli.ts b/packages/kbn-plugin-helpers/src/cli.ts index b894f854a484f..18ddc62cba8a6 100644 --- a/packages/kbn-plugin-helpers/src/cli.ts +++ b/packages/kbn-plugin-helpers/src/cli.ts @@ -62,25 +62,6 @@ program })) ); -program - .command('test') - .description('Run the server and browser tests') - .on('--help', docs('test/all')) - .action(createCommanderAction('testAll')); - -program - .command('test:karma') - .description('Run the browser tests in a real web browser') - .option('--dev', 'Enable dev mode, keeps the test server running') - .option('-p, --plugins ', "Manually specify which plugins' test bundles to run") - .on('--help', docs('test/karma')) - .action( - createCommanderAction('testKarma', (command) => ({ - dev: Boolean(command.dev), - plugins: command.plugins, - })) - ); - program .command('test:mocha [files...]') .description('Run the server tests using mocha') diff --git a/packages/kbn-plugin-helpers/src/lib/tasks.ts b/packages/kbn-plugin-helpers/src/lib/tasks.ts index 7817838760a2e..bd86bb670ff39 100644 --- a/packages/kbn-plugin-helpers/src/lib/tasks.ts +++ b/packages/kbn-plugin-helpers/src/lib/tasks.ts @@ -19,23 +19,17 @@ import { buildTask } from '../tasks/build'; import { startTask } from '../tasks/start'; -import { testAllTask } from '../tasks/test/all'; -import { testKarmaTask } from '../tasks/test/karma'; import { testMochaTask } from '../tasks/test/mocha'; // define a tasks interface that we can extend in the tests export interface Tasks { build: typeof buildTask; start: typeof startTask; - testAll: typeof testAllTask; - testKarma: typeof testKarmaTask; testMocha: typeof testMochaTask; } export const tasks: Tasks = { build: buildTask, start: startTask, - testAll: testAllTask, - testKarma: testKarmaTask, testMocha: testMochaTask, }; diff --git a/packages/kbn-plugin-helpers/src/tasks/test/all/README.md b/packages/kbn-plugin-helpers/src/tasks/test/all/README.md deleted file mode 100644 index 4f5a72ac0d523..0000000000000 --- a/packages/kbn-plugin-helpers/src/tasks/test/all/README.md +++ /dev/null @@ -1,3 +0,0 @@ -Runs both the mocha and karma tests, in that order. - -This is just a simple caller to both `test/mocha` and `test/karma` \ No newline at end of file diff --git a/packages/kbn-plugin-helpers/src/tasks/test/all/index.ts b/packages/kbn-plugin-helpers/src/tasks/test/all/index.ts deleted file mode 100644 index be8db50825fc9..0000000000000 --- a/packages/kbn-plugin-helpers/src/tasks/test/all/index.ts +++ /dev/null @@ -1,20 +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. - */ - -export * from './test_all_task'; diff --git a/packages/kbn-plugin-helpers/src/tasks/test/all/test_all_task.ts b/packages/kbn-plugin-helpers/src/tasks/test/all/test_all_task.ts deleted file mode 100644 index d07c19291d2cb..0000000000000 --- a/packages/kbn-plugin-helpers/src/tasks/test/all/test_all_task.ts +++ /dev/null @@ -1,25 +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 { TaskContext } from '../../../lib'; - -export function testAllTask({ run }: TaskContext) { - run('testMocha'); - run('testKarma'); -} diff --git a/packages/kbn-plugin-helpers/src/tasks/test/karma/README.md b/packages/kbn-plugin-helpers/src/tasks/test/karma/README.md deleted file mode 100644 index 8d921e8312344..0000000000000 --- a/packages/kbn-plugin-helpers/src/tasks/test/karma/README.md +++ /dev/null @@ -1,60 +0,0 @@ -writing tests -============= - -Browser tests are written just like server tests, they are just executed differently. - - - place tests near the code they test, in `__tests__` directories throughout - the public directory - - - Use the same bdd-style `describe()` and `it()` - api to define the suites and cases of your tests. - - ```js - describe('some portion of your code', function () { - it('should do this thing', function () { - expect(true).to.be(false); - }); - }); - ``` - - -starting the test runner -======================== - -Under the covers this command uses the `test:karma` task from kibana. This will execute -your tasks once and exit when complete. - -When run with the `--dev` option, the command uses the `test:karma:debug` task from kibana. -This task sets-up a test runner that will watch your code for changes and rebuild your -tests when necessary. You access the test runner through a browser that it starts itself -(via Karma). - -If your plugin consists of a number of internal plugins, you may wish to keep the tests -isolated to a specific plugin or plugins, instead of executing all of the tests. To do this, -use `--plugins` and passing the plugins you would like to test. Multiple plugins can be -specified by separating them with commas. - - -running the tests -================= - -Once the test runner has started you a new browser window should be opened and you should -see a message saying "connected". Next to that is a "DEBUG" button. This button will open -an interactive version of your tests that you can refresh, inspects, and otherwise debug -while you write your tests. - - -focus on the task at hand -========================= - -To limit the tests that run you can either: - - 1. use the ?grep= query string to filter the test cases/suites by name - 2. Click the suite title or (play) button next to test output - 3. Add `.only` to your `describe()` or `it()` calls: - - ```js - describe.only('suite name', function () { - // ... - }); - ``` diff --git a/packages/kbn-plugin-helpers/src/tasks/test/karma/index.ts b/packages/kbn-plugin-helpers/src/tasks/test/karma/index.ts deleted file mode 100644 index 3089357b49991..0000000000000 --- a/packages/kbn-plugin-helpers/src/tasks/test/karma/index.ts +++ /dev/null @@ -1,20 +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. - */ - -export * from './test_karma_task'; diff --git a/packages/kbn-plugin-helpers/src/tasks/test/karma/test_karma_task.ts b/packages/kbn-plugin-helpers/src/tasks/test/karma/test_karma_task.ts deleted file mode 100644 index 2fe8134209894..0000000000000 --- a/packages/kbn-plugin-helpers/src/tasks/test/karma/test_karma_task.ts +++ /dev/null @@ -1,42 +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 { execFileSync } from 'child_process'; - -import { TaskContext } from '../../../lib'; -import { winCmd } from '../../../lib/win_cmd'; - -export function testKarmaTask({ plugin, options }: TaskContext) { - options = options || {}; - - const kbnServerArgs = ['--kbnServer.plugin-path=' + plugin.root]; - - if (options.plugins) { - kbnServerArgs.push('--kbnServer.tests_bundle.pluginId=' + options.plugins); - } else { - kbnServerArgs.push('--kbnServer.tests_bundle.pluginId=' + plugin.id); - } - - const task = options.dev ? 'test:karma:debug' : 'test:karma'; - const args = [task].concat(kbnServerArgs); - execFileSync(winCmd('yarn'), args, { - cwd: plugin.kibanaRoot, - stdio: ['ignore', 1, 2], - }); -} diff --git a/packages/kbn-test/src/failed_tests_reporter/__fixtures__/index.ts b/packages/kbn-test/src/failed_tests_reporter/__fixtures__/index.ts index 16ebe10ad5426..11d6cb6a2b47b 100644 --- a/packages/kbn-test/src/failed_tests_reporter/__fixtures__/index.ts +++ b/packages/kbn-test/src/failed_tests_reporter/__fixtures__/index.ts @@ -21,6 +21,5 @@ const Fs = jest.requireActual('fs'); export const FTR_REPORT = Fs.readFileSync(require.resolve('./ftr_report.xml'), 'utf8'); export const JEST_REPORT = Fs.readFileSync(require.resolve('./jest_report.xml'), 'utf8'); -export const KARMA_REPORT = Fs.readFileSync(require.resolve('./karma_report.xml'), 'utf8'); export const MOCHA_REPORT = Fs.readFileSync(require.resolve('./mocha_report.xml'), 'utf8'); export const CYPRESS_REPORT = Fs.readFileSync(require.resolve('./cypress_report.xml'), 'utf8'); diff --git a/packages/kbn-test/src/failed_tests_reporter/__fixtures__/karma_report.xml b/packages/kbn-test/src/failed_tests_reporter/__fixtures__/karma_report.xml deleted file mode 100644 index 5c4bdb9f50adf..0000000000000 --- a/packages/kbn-test/src/failed_tests_reporter/__fixtures__/karma_report.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - Error: expected 7069 to be below 64 - at Assertion.__kbnBundles__.tests../packages/kbn-expect/expect.js.Assertion.assert (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:13671:11) - at Assertion.__kbnBundles__.tests../packages/kbn-expect/expect.js.Assertion.lessThan.Assertion.below (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:13891:8) - at Function.lessThan (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:14078:15) - at _callee3$ (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158985:60) - at tryCatch (webpack://%5Bname%5D/./node_modules/regenerator-runtime/runtime.js?:62:40) - at Generator.invoke [as _invoke] (webpack://%5Bname%5D/./node_modules/regenerator-runtime/runtime.js?:288:22) - at Generator.prototype.<computed> [as next] (webpack://%5Bname%5D/./node_modules/regenerator-runtime/runtime.js?:114:21) - at asyncGeneratorStep (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158772:103) - at _next (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158774:194) - - - - - - - - - diff --git a/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.test.ts b/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.test.ts index 53a74f6cc6af2..505e898c62adf 100644 --- a/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.test.ts +++ b/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.test.ts @@ -39,13 +39,7 @@ jest.mock('fs', () => { }; }); -import { - FTR_REPORT, - JEST_REPORT, - MOCHA_REPORT, - KARMA_REPORT, - CYPRESS_REPORT, -} from './__fixtures__'; +import { FTR_REPORT, JEST_REPORT, MOCHA_REPORT, CYPRESS_REPORT } from './__fixtures__'; import { parseTestReport } from './test_report'; import { addMessagesToReport } from './add_messages_to_report'; @@ -338,79 +332,3 @@ it('rewrites cypress reports with minimal changes', async () => { `); }); - -it('rewrites karma reports with minimal changes', async () => { - const xml = await addMessagesToReport({ - report: await parseTestReport(KARMA_REPORT), - messages: [ - { - name: - 'CoordinateMapsVisualizationTest CoordinateMapsVisualization - basics should initialize OK', - classname: 'Browser Unit Tests.CoordinateMapsVisualizationTest', - message: 'foo bar', - }, - ], - log, - reportPath: Path.resolve(__dirname, './__fixtures__/karma_report.xml'), - }); - - expect(createPatch('karma.xml', KARMA_REPORT, xml, { context: 0 })).toMatchInlineSnapshot(` - Index: karma.xml - =================================================================== - --- karma.xml [object Object] - +++ karma.xml - @@ -1,5 +1,5 @@ - -‹?xml version="1.0"?› - +‹?xml version="1.0" encoding="utf-8"?› - ‹testsuite name="Chrome 75.0.3770 (Mac OS X 10.14.5)" package="" timestamp="2019-07-02T19:53:21" id="0" hostname="spalger.lan" tests="648" errors="0" failures="4" time="1.759"› - ‹properties› - ‹property name="browser.fullName" value="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36"/› - ‹/properties› - @@ -7,27 +7,31 @@ - ‹testcase name="Vis-Editor-Agg-Params plugin directive should hide custom label parameter" time="0" classname="Browser Unit Tests.Vis-Editor-Agg-Params plugin directive"› - ‹skipped/› - ‹/testcase› - ‹testcase name="CoordinateMapsVisualizationTest CoordinateMapsVisualization - basics should initialize OK" time="0.265" classname="Browser Unit Tests.CoordinateMapsVisualizationTest"› - - ‹failure type=""›Error: expected 7069 to be below 64 - - at Assertion.__kbnBundles__.tests../packages/kbn-expect/expect.js.Assertion.assert (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:13671:11) - - at Assertion.__kbnBundles__.tests../packages/kbn-expect/expect.js.Assertion.lessThan.Assertion.below (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:13891:8) - - at Function.lessThan (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:14078:15) - - at _callee3$ (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158985:60) - + ‹failure type=""›‹![CDATA[Error: expected 7069 to be below 64 - + at Assertion.__kbnBundles__.tests../packages/kbn-expect/expect.js.Assertion.assert (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:13671:11) - + at Assertion.__kbnBundles__.tests../packages/kbn-expect/expect.js.Assertion.lessThan.Assertion.below (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:13891:8) - + at Function.lessThan (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:14078:15) - + at _callee3$ (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158985:60) - at tryCatch (webpack://%5Bname%5D/./node_modules/regenerator-runtime/runtime.js?:62:40) - at Generator.invoke [as _invoke] (webpack://%5Bname%5D/./node_modules/regenerator-runtime/runtime.js?:288:22) - - at Generator.prototype.<computed> [as next] (webpack://%5Bname%5D/./node_modules/regenerator-runtime/runtime.js?:114:21) - - at asyncGeneratorStep (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158772:103) - - at _next (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158774:194) - -‹/failure› - + at Generator.prototype.‹computed› [as next] (webpack://%5Bname%5D/./node_modules/regenerator-runtime/runtime.js?:114:21) - + at asyncGeneratorStep (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158772:103) - + at _next (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158774:194) - +]]›‹/failure› - + ‹system-out›Failed Tests Reporter: - + - foo bar - + - +‹/system-out› - ‹/testcase› - ‹testcase name="CoordinateMapsVisualizationTest CoordinateMapsVisualization - basics should toggle to Heatmap OK" time="0.055" classname="Browser Unit Tests.CoordinateMapsVisualizationTest"/› - ‹testcase name="VegaParser._parseSchema should warn on vega-lite version too new to be supported" time="0.001" classname="Browser Unit Tests.VegaParser·_parseSchema"/› - ‹system-out› - - ‹![CDATA[Chrome 75.0.3770 (Mac OS X 10.14.5) LOG: 'ready to load tests for shard 1 of 4' - + Chrome 75.0.3770 (Mac OS X 10.14.5) LOG: 'ready to load tests for shard 1 of 4' - ,Chrome 75.0.3770 (Mac OS X 10.14.5) WARN: 'Unmatched GET to http://localhost:9876/api/interpreter/fns' - ... - - -]]› - + - ‹/system-out› - ‹system-err/› - -‹/testsuite› - +‹/testsuite› - \\ No newline at end of file - - `); -}); diff --git a/packages/kbn-test/src/failed_tests_reporter/get_failures.test.ts b/packages/kbn-test/src/failed_tests_reporter/get_failures.test.ts index 23d9805727f32..f570ed36111b3 100644 --- a/packages/kbn-test/src/failed_tests_reporter/get_failures.test.ts +++ b/packages/kbn-test/src/failed_tests_reporter/get_failures.test.ts @@ -19,7 +19,7 @@ import { getFailures } from './get_failures'; import { parseTestReport } from './test_report'; -import { FTR_REPORT, JEST_REPORT, KARMA_REPORT, MOCHA_REPORT } from './__fixtures__'; +import { FTR_REPORT, JEST_REPORT, MOCHA_REPORT } from './__fixtures__'; it('discovers failures in ftr report', async () => { const failures = getFailures(await parseTestReport(FTR_REPORT)); @@ -85,31 +85,6 @@ it('discovers failures in jest report', async () => { `); }); -it('discovers failures in karma report', async () => { - const failures = getFailures(await parseTestReport(KARMA_REPORT)); - expect(failures).toMatchInlineSnapshot(` - Array [ - Object { - "classname": "Browser Unit Tests.CoordinateMapsVisualizationTest", - "failure": "Error: expected 7069 to be below 64 - at Assertion.__kbnBundles__.tests../packages/kbn-expect/expect.js.Assertion.assert (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:13671:11) - at Assertion.__kbnBundles__.tests../packages/kbn-expect/expect.js.Assertion.lessThan.Assertion.below (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:13891:8) - at Function.lessThan (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:14078:15) - at _callee3$ (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158985:60) - at tryCatch (webpack://%5Bname%5D/./node_modules/regenerator-runtime/runtime.js?:62:40) - at Generator.invoke [as _invoke] (webpack://%5Bname%5D/./node_modules/regenerator-runtime/runtime.js?:288:22) - at Generator.prototype. [as next] (webpack://%5Bname%5D/./node_modules/regenerator-runtime/runtime.js?:114:21) - at asyncGeneratorStep (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158772:103) - at _next (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158774:194) - ", - "likelyIrrelevant": false, - "name": "CoordinateMapsVisualizationTest CoordinateMapsVisualization - basics should initialize OK", - "time": "0.265", - }, - ] - `); -}); - it('discovers failures in mocha report', async () => { const failures = getFailures(await parseTestReport(MOCHA_REPORT)); expect(failures).toMatchInlineSnapshot(` diff --git a/packages/kbn-test/src/failed_tests_reporter/report_metadata.test.ts b/packages/kbn-test/src/failed_tests_reporter/report_metadata.test.ts index 729d80ddfcb44..c079084965609 100644 --- a/packages/kbn-test/src/failed_tests_reporter/report_metadata.test.ts +++ b/packages/kbn-test/src/failed_tests_reporter/report_metadata.test.ts @@ -19,7 +19,7 @@ import { getReportMessageIter } from './report_metadata'; import { parseTestReport } from './test_report'; -import { FTR_REPORT, JEST_REPORT, KARMA_REPORT, MOCHA_REPORT } from './__fixtures__'; +import { FTR_REPORT, JEST_REPORT, MOCHA_REPORT } from './__fixtures__'; it('reads messages and screenshots from metadata-json properties', async () => { const ftrReport = await parseTestReport(FTR_REPORT); @@ -43,7 +43,4 @@ it('reads messages and screenshots from metadata-json properties', async () => { const mochaReport = await parseTestReport(MOCHA_REPORT); expect(Array.from(getReportMessageIter(mochaReport))).toMatchInlineSnapshot(`Array []`); - - const karmaReport = await parseTestReport(KARMA_REPORT); - expect(Array.from(getReportMessageIter(karmaReport))).toMatchInlineSnapshot(`Array []`); }); diff --git a/packages/kbn-ui-shared-deps/theme.ts b/packages/kbn-ui-shared-deps/theme.ts index 4b2758516fc26..a810e1de0a21f 100644 --- a/packages/kbn-ui-shared-deps/theme.ts +++ b/packages/kbn-ui-shared-deps/theme.ts @@ -24,7 +24,7 @@ const globals: any = typeof window === 'undefined' ? {} : window; export type Theme = typeof LightTheme; // in the Kibana app we can rely on this global being defined, but in -// some cases (like jest, or karma tests) the global is undefined +// some cases (like jest) the global is undefined export const tag: string = globals.__kbnThemeTag__ || 'v7light'; export const version = tag.startsWith('v7') ? 7 : 8; export const darkMode = tag.endsWith('dark'); diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index f7acff14915a7..72945597758e2 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1620,14 +1620,6 @@ If others are consuming your plugin's new platform contracts via the `ui/new_pla > Note: The `ui/new_platform` mock is only designed for use by old Jest tests. If you are writing new tests, you should structure your code and tests such that you don't need this mock. Instead, you should import the `core` mock directly and instantiate it. -#### What about karma tests? - -While our plan is to only provide first-class mocks for Jest tests, there are many legacy karma tests that cannot be quickly or easily converted to Jest -- particularly those which are still relying on mocking Angular services via `ngMock`. - -For these tests, we are maintaining a separate set of mocks. Files with a `.karma_mock.{js|ts|tsx}` extension will be loaded _globally_ before karma tests are run. - -It is important to note that this behavior is different from `jest.mock('ui/new_platform')`, which only mocks tests on an individual basis. If you encounter any failures in karma tests as a result of new platform migration efforts, you may need to add a `.karma_mock.js` file for the affected services, or add to the existing karma mock we are maintaining in `ui/new_platform`. - ### Provide Legacy Platform API to the New platform plugin #### On the server side diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index d77676b350f93..78a9219f3d694 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -53,7 +53,7 @@ interface BootstrapModule { * The LegacyPlatformService is responsible for initializing * the legacy platform by injecting parts of the new platform * services into the legacy platform modules, like ui/modules, - * and then bootstrapping the ui/chrome or ui/test_harness to + * and then bootstrapping the ui/chrome or ~~ui/test_harness~~ to * setup either the app or browser tests. */ export class LegacyPlatformService { diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index 83a2e712b424f..e74f6d32e92b0 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -42,21 +42,7 @@ export const config = { validate: match(validBasePathRegex, "must start with a slash, don't end with one"), }) ), - cors: schema.conditional( - schema.contextRef('dev'), - true, - schema.object( - { - origin: schema.arrayOf(schema.string()), - }, - { - defaultValue: { - origin: ['*://localhost:9876'], // karma test server - }, - } - ), - schema.boolean({ defaultValue: false }) - ), + cors: schema.boolean({ defaultValue: false }), customResponseHeaders: schema.recordOf(schema.string(), schema.any(), { defaultValue: {}, }), diff --git a/src/dev/build/tasks/copy_source_task.ts b/src/dev/build/tasks/copy_source_task.ts index 221c9162bd2a9..c8489673b83af 100644 --- a/src/dev/build/tasks/copy_source_task.ts +++ b/src/dev/build/tasks/copy_source_task.ts @@ -33,7 +33,6 @@ export const CopySource: Task = { '!src/**/{__tests__,__snapshots__,__mocks__}/**', '!src/test_utils/**', '!src/fixtures/**', - '!src/legacy/core_plugins/tests_bundle/**', '!src/legacy/core_plugins/console/public/tests/**', '!src/cli/cluster/**', '!src/cli/repl/**', diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index e11668ab57f55..5249b7d652790 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -52,7 +52,6 @@ export default { '!packages/kbn-ui-framework/src/services/**/*/index.js', 'src/legacy/core_plugins/**/*.{js,mjs,jsx,ts,tsx}', '!src/legacy/core_plugins/**/{__test__,__snapshots__}/**/*', - '!src/legacy/core_plugins/tests_bundle/**', ], moduleNameMapper: { '@elastic/eui$': '/node_modules/@elastic/eui/test-env', diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 1e4f048be8ea4..864bf7515053c 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -124,7 +124,6 @@ export const IGNORE_DIRECTORY_GLOBS = [ export const TEMPORARILY_IGNORED_PATHS = [ 'src/legacy/core_plugins/console/public/src/directives/helpExample.txt', 'src/legacy/core_plugins/console/public/src/sense_editor/theme-sense-dark.js', - 'src/legacy/core_plugins/tests_bundle/webpackShims/angular-mocks.js', 'src/legacy/core_plugins/tile_map/public/__tests__/scaledCircleMarkers.png', 'src/legacy/core_plugins/tile_map/public/__tests__/shadedCircleMarkers.png', 'src/legacy/core_plugins/tile_map/public/__tests__/shadedGeohashGrid.png', diff --git a/src/legacy/core_plugins/tests_bundle/find_source_files.js b/src/legacy/core_plugins/tests_bundle/find_source_files.js deleted file mode 100644 index eed88a5ecb8b0..0000000000000 --- a/src/legacy/core_plugins/tests_bundle/find_source_files.js +++ /dev/null @@ -1,64 +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. - */ - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { fromRoot } from '../../../core/server/utils'; -import { chain } from 'lodash'; -import { resolve } from 'path'; -import { fromNode } from 'bluebird'; -import glob from 'glob-all'; - -const findSourceFiles = async (patterns, cwd = fromRoot('.')) => { - patterns = [].concat(patterns || []); - - const matches = await fromNode((cb) => { - glob( - patterns, - { - cwd: cwd, - ignore: [ - 'node_modules/**/*', - 'bower_components/**/*', - '**/_*.js', - '**/*.test.js', - '**/*.test.mocks.js', - '**/__mocks__/**/*', - ], - symlinks: findSourceFiles.symlinks, - statCache: findSourceFiles.statCache, - realpathCache: findSourceFiles.realpathCache, - cache: findSourceFiles.cache, - }, - cb - ); - }); - - return chain(matches) - .flatten() - .uniq() - .map((match) => resolve(cwd, match)) - .value(); -}; - -findSourceFiles.symlinks = {}; -findSourceFiles.statCache = {}; -findSourceFiles.realpathCache = {}; -findSourceFiles.cache = {}; - -export default findSourceFiles; diff --git a/src/legacy/core_plugins/tests_bundle/index.js b/src/legacy/core_plugins/tests_bundle/index.js deleted file mode 100644 index da7c1c4f4527e..0000000000000 --- a/src/legacy/core_plugins/tests_bundle/index.js +++ /dev/null @@ -1,180 +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 { createReadStream } from 'fs'; -import { resolve } from 'path'; - -import globby from 'globby'; -import MultiStream from 'multistream'; -import webpackMerge from 'webpack-merge'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { fromRoot } from '../../../core/server/utils'; -import { replacePlaceholder } from '../../../optimize/public_path_placeholder'; -import findSourceFiles from './find_source_files'; -import { createTestEntryTemplate } from './tests_entry_template'; - -export default (kibana) => { - return new kibana.Plugin({ - config: (Joi) => { - return Joi.object({ - enabled: Joi.boolean().default(true), - instrument: Joi.boolean().default(false), - pluginId: Joi.string(), - }).default(); - }, - - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - async __bundleProvider__(kbnServer) { - const modules = new Set(); - - const { - config, - uiApps, - uiBundles, - plugins, - uiExports: { uiSettingDefaults = {} }, - } = kbnServer; - - const testGlobs = []; - - const testingPluginIds = config.get('tests_bundle.pluginId'); - - if (testingPluginIds) { - testingPluginIds.split(',').forEach((pluginId) => { - const plugin = plugins.find((plugin) => plugin.id === pluginId); - - if (!plugin) { - throw new Error('Invalid testingPluginId :: unknown plugin ' + pluginId); - } - - // add the modules from all of this plugins apps - for (const app of uiApps) { - if (app.getPluginId() === pluginId) { - modules.add(app.getMainModuleId()); - } - } - - testGlobs.push(`${plugin.publicDir}/**/__tests__/**/*.js`); - }); - } else { - // add all since we are not just focused on specific plugins - testGlobs.push('src/legacy/ui/public/**/*.js', '!src/legacy/ui/public/flot-charts/**/*'); - // add the modules from all of the apps - for (const app of uiApps) { - modules.add(app.getMainModuleId()); - } - - for (const plugin of plugins) { - testGlobs.push(`${plugin.publicDir}/**/__tests__/**/*.js`); - } - } - - const testFiles = await findSourceFiles(testGlobs); - for (const f of testFiles) modules.add(f); - - if (config.get('tests_bundle.instrument')) { - uiBundles.addPostLoader({ - test: /\.js$/, - exclude: /[\/\\](__tests__|node_modules|bower_components|webpackShims)[\/\\]/, - loader: 'istanbul-instrumenter-loader', - }); - } - - uiBundles.add({ - id: 'tests', - modules: [...modules], - template: createTestEntryTemplate(uiSettingDefaults), - extendConfig(webpackConfig) { - const mergedConfig = webpackMerge( - { - resolve: { - extensions: ['.karma_mock.js', '.karma_mock.tsx', '.karma_mock.ts'], - }, - node: { - fs: 'empty', - child_process: 'empty', - dns: 'empty', - net: 'empty', - tls: 'empty', - }, - }, - webpackConfig - ); - - /** - * [..] it removes the commons bundle creation from the webpack - * config when we're building the bundle for the browser tests. It - * shouldn't be created, and by default isn't, but something is - * triggering it in webpack which breaks the tests so if we just - * remove the optimization config it will never happen and the tests - * will keep working [..] - * - * TLDR: If you have any questions about this line, ask Spencer. - */ - delete mergedConfig.optimization.splitChunks.cacheGroups.commons; - - return mergedConfig; - }, - }); - - kbnServer.server.route({ - method: 'GET', - path: '/test_bundle/built_css.css', - async handler(_, h) { - const cssFiles = await globby( - testingPluginIds - ? testingPluginIds.split(',').map((id) => `built_assets/css/plugins/${id}/**/*.css`) - : `built_assets/css/**/*.css`, - { cwd: fromRoot('.'), absolute: true } - ); - - const stream = replacePlaceholder( - new MultiStream(cssFiles.map((path) => createReadStream(path))), - '/built_assets/css/' - ); - - return h.response(stream).code(200).type('text/css'); - }, - }); - - // Sets global variables normally set by the bootstrap.js script - kbnServer.server.route({ - path: '/test_bundle/karma/globals.js', - method: 'GET', - async handler(req, h) { - const basePath = config.get('server.basePath'); - - const file = `window.__kbnPublicPath__ = { 'kbn-ui-shared-deps': "${basePath}/bundles/kbn-ui-shared-deps/" };`; - - return h.response(file).header('content-type', 'application/json'); - }, - }); - }, - - __globalImportAliases__: { - ng_mock$: fromRoot('src/test_utils/public/ng_mock'), - 'angular-mocks$': require.resolve('./webpackShims/angular-mocks'), - fixtures: fromRoot('src/fixtures'), - test_utils: fromRoot('src/test_utils/public'), - }, - }, - }); -}; diff --git a/src/legacy/core_plugins/tests_bundle/package.json b/src/legacy/core_plugins/tests_bundle/package.json deleted file mode 100644 index 4d2df048d4164..0000000000000 --- a/src/legacy/core_plugins/tests_bundle/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "tests_bundle", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/tests_bundle/public/index.scss b/src/legacy/core_plugins/tests_bundle/public/index.scss deleted file mode 100644 index d8dbf8d6dc885..0000000000000 --- a/src/legacy/core_plugins/tests_bundle/public/index.scss +++ /dev/null @@ -1,4 +0,0 @@ -// This file pulls some styles of NP plugins into the legacy test stylesheet -// so they are available for karma browser tests. -@import '../../../../plugins/vis_type_vislib/public/index'; -@import '../../../../plugins/visualizations/public/index'; diff --git a/src/legacy/core_plugins/tests_bundle/tests_entry_template.js b/src/legacy/core_plugins/tests_bundle/tests_entry_template.js deleted file mode 100644 index 28c26f08621eb..0000000000000 --- a/src/legacy/core_plugins/tests_bundle/tests_entry_template.js +++ /dev/null @@ -1,158 +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 { Type } from '@kbn/config-schema'; -import pkg from '../../../../package.json'; - -export const createTestEntryTemplate = (defaultUiSettings) => (bundle) => ` -/** - * Test entry file - * - * This is programmatically created and updated, do not modify - * - * context: ${bundle.getContext()} - * - */ - -import fetchMock from 'fetch-mock/es5/client'; -import { CoreSystem } from '__kibanaCore__'; - -// Fake uiCapabilities returned to Core in browser tests -const uiCapabilities = { - navLinks: { - myLink: true, - notMyLink: true, - }, - discover: { - showWriteControls: true - }, - visualize: { - save: true - }, - dashboard: { - showWriteControls: true - }, - timelion: { - save: true - }, - management: { - kibana: { - settings: true, - index_patterns: true, - objects: true - } - } -}; - -// Mock fetch for CoreSystem calls. -fetchMock.config.fallbackToNetwork = true; -fetchMock.post(/\\/api\\/core\\/capabilities/, { - status: 200, - body: JSON.stringify(uiCapabilities), - headers: { 'Content-Type': 'application/json' }, -}); - -// render the core system in a element not attached to the document as the -// default children of the body in the browser tests are needed for mocha and -// other test components to work -const rootDomElement = document.createElement('div'); - -const coreSystem = new CoreSystem({ - injectedMetadata: { - version: '1.2.3', - buildNumber: 1234, - legacyMode: true, - legacyMetadata: { - app: { - id: 'karma', - title: 'Karma', - }, - nav: [], - version: '1.2.3', - buildNum: 1234, - devMode: true, - uiSettings: { - defaults: ${JSON.stringify( - defaultUiSettings, - (key, value) => { - if (value instanceof Type) return null; - return value; - }, - 2 - ) - .split('\n') - .join('\n ')}, - user: {} - }, - nav: [] - }, - csp: { - warnLegacyBrowsers: false, - }, - capabilities: uiCapabilities, - uiPlugins: [], - vars: { - kbnIndex: '.kibana', - esShardTimeout: 1500, - esApiVersion: ${JSON.stringify(pkg.branch)}, - esRequestTimeout: '300000', - tilemapsConfig: { - deprecated: { - isOverridden: false, - config: { - options: { - } - } - } - }, - regionmapsConfig: { - layers: [] - }, - mapConfig: { - includeElasticMapsService: true, - emsFileApiUrl: 'https://vector-staging.maps.elastic.co', - emsTileApiUrl: 'https://tiles.maps.elastic.co', - }, - vegaConfig: { - enabled: true, - enableExternalUrls: true - }, - }, - }, - rootDomElement, - requireLegacyBootstrapModule: () => { - // wrapped in NODE_ENV check so the 'ui/test_harness' module - // is not included in the distributable - if (process.env.IS_KIBANA_DISTRIBUTABLE !== 'true') { - return require('ui/test_harness'); - } - - throw new Error('tests bundle is not available in the distributable'); - }, - requireNewPlatformShimModule: () => require('ui/new_platform'), - requireLegacyFiles: () => { - ${bundle.getRequires().join('\n ')} - } -}) - -coreSystem - .setup() - .then(() => { - return coreSystem.start(); - }); -`; diff --git a/src/legacy/core_plugins/tests_bundle/webpackShims/angular-mocks.js b/src/legacy/core_plugins/tests_bundle/webpackShims/angular-mocks.js deleted file mode 100644 index 24f794cb32990..0000000000000 --- a/src/legacy/core_plugins/tests_bundle/webpackShims/angular-mocks.js +++ /dev/null @@ -1,21 +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. - */ - -require('angular'); -require('../../../../../node_modules/angular-mocks/angular-mocks.js'); diff --git a/src/legacy/ui/public/test_harness/.eslintrc b/src/legacy/ui/public/test_harness/.eslintrc deleted file mode 100644 index b1b85968796dd..0000000000000 --- a/src/legacy/ui/public/test_harness/.eslintrc +++ /dev/null @@ -1,2 +0,0 @@ -rules: - no-console: 0 diff --git a/src/legacy/ui/public/test_harness/index.js b/src/legacy/ui/public/test_harness/index.js deleted file mode 100644 index d66a4b1d67214..0000000000000 --- a/src/legacy/ui/public/test_harness/index.js +++ /dev/null @@ -1,20 +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. - */ - -export { bootstrap } from './test_harness'; diff --git a/src/legacy/ui/public/test_harness/test_harness.css b/src/legacy/ui/public/test_harness/test_harness.css deleted file mode 100644 index d0a0f50c55b9b..0000000000000 --- a/src/legacy/ui/public/test_harness/test_harness.css +++ /dev/null @@ -1,22 +0,0 @@ -/** - * This file is only for tests so it is it's own CSS - * to be imported directly by and only by this module. - */ - -body#test-harness-body { - /** - now that tests include the kibana styles, we have - to override the body { display: flex; } rule as it - prevents the visualizations from properly testing - their sizing - */ - display: block; -} - -body#test-harness-body #mocha-stats .progress { - /* bootstrap thinks it is the only one who will use ".progress" */ - height: auto; - background-color: transparent; - overflow: auto; - border-radius: 0; -} diff --git a/src/legacy/ui/public/test_harness/test_harness.js b/src/legacy/ui/public/test_harness/test_harness.js deleted file mode 100644 index 22c981fe0cf54..0000000000000 --- a/src/legacy/ui/public/test_harness/test_harness.js +++ /dev/null @@ -1,89 +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. - */ - -// chrome expects to be loaded first, let it get its way -import chrome from '../chrome'; - -import { parse as parseUrl } from 'url'; -import { Subject } from 'rxjs'; -import sinon from 'sinon'; -import { metadata } from '../metadata'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { UiSettingsClient } from '../../../../core/public/ui_settings'; - -import './test_harness.css'; -import 'ng_mock'; -import { setupTestSharding } from './test_sharding'; - -const { query } = parseUrl(window.location.href, true); -if (query && query.mocha) { - try { - window.mocha.setup(JSON.parse(query.mocha)); - } catch (error) { - throw new Error( - `'?mocha=${query.mocha}' query string param provided but it could not be parsed as json` - ); - } -} - -setupTestSharding(); - -before(() => { - // prevent accidental ajax requests - sinon.useFakeXMLHttpRequest(); -}); - -let stubUiSettings; -let done$; -function createStubUiSettings() { - if (stubUiSettings) { - done$.complete(); - } - done$ = new Subject(); - - stubUiSettings = new UiSettingsClient({ - api: { - async batchSet() { - return { settings: stubUiSettings.getAll() }; - }, - }, - onUpdateError: () => {}, - defaults: metadata.uiSettings.defaults, - initialSettings: {}, - done$, - }); -} - -createStubUiSettings(); -sinon.stub(chrome, 'getUiSettingsClient').callsFake(() => stubUiSettings); - -afterEach(function () { - createStubUiSettings(); -}); - -// Kick off mocha, called at the end of test entry files -export function bootstrap(targetDomElement) { - // allows test_harness.less to have higher priority selectors - targetDomElement.setAttribute('id', 'test-harness-body'); - - // load the hacks since we aren't actually bootstrapping the - // chrome, which is where the hacks would normally be loaded - require('uiExports/hacks'); - chrome.setupAngular(); -} diff --git a/src/legacy/ui/public/test_harness/test_sharding/find_test_bundle_url.js b/src/legacy/ui/public/test_harness/test_sharding/find_test_bundle_url.js deleted file mode 100644 index 53800d08ca05b..0000000000000 --- a/src/legacy/ui/public/test_harness/test_sharding/find_test_bundle_url.js +++ /dev/null @@ -1,39 +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. - */ - -/** - * We don't have a lot of options for passing arguments to the page that karma - * creates, so we tack some query string params onto the test bundle script url. - * - * This function finds that url by looking for a script tag that has - * the "/tests.bundle.js" segment - * - * @return {string} url - */ -export function findTestBundleUrl() { - const scriptTags = document.querySelectorAll('script[src]'); - const scriptUrls = [].map.call(scriptTags, (el) => el.getAttribute('src')); - const testBundleUrl = scriptUrls.find((url) => url.includes('/tests.bundle.js')); - - if (!testBundleUrl) { - throw new Error("test bundle url couldn't be found"); - } - - return testBundleUrl; -} diff --git a/src/legacy/ui/public/test_harness/test_sharding/get_shard_num.js b/src/legacy/ui/public/test_harness/test_sharding/get_shard_num.js deleted file mode 100644 index 55a33fc295191..0000000000000 --- a/src/legacy/ui/public/test_harness/test_sharding/get_shard_num.js +++ /dev/null @@ -1,47 +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 murmurHash3 from 'murmurhash3js'; - -// murmur hashes are 32bit unsigned integers -const MAX_HASH = Math.pow(2, 32); - -/** - * Determine the shard number for a suite by hashing - * its name and placing it based on the hash - * - * @param {number} shardTotal - the total number of shards - * @param {string} suiteName - the suite name to hash - * @return {number} shardNum - 1-based shard number - */ -export function getShardNum(shardTotal, suiteName) { - const hashIntsPerShard = MAX_HASH / shardTotal; - - const hashInt = murmurHash3.x86.hash32(suiteName); - - // murmur3 produces 32bit integers, so we devide it by the number of chunks - // to determine which chunk the suite should fall in. +1 because the current - // chunk is 1-based - const shardNum = Math.floor(hashInt / hashIntsPerShard) + 1; - - // It's not clear if hash32 can produce the MAX_HASH or not, - // but this just ensures that shard numbers don't go out of bounds - // and cause tests to be ignored unnecessarily - return Math.max(1, Math.min(shardNum, shardTotal)); -} diff --git a/src/legacy/ui/public/test_harness/test_sharding/get_sharding_params_from_url.js b/src/legacy/ui/public/test_harness/test_sharding/get_sharding_params_from_url.js deleted file mode 100644 index 65e41dbc84b63..0000000000000 --- a/src/legacy/ui/public/test_harness/test_sharding/get_sharding_params_from_url.js +++ /dev/null @@ -1,45 +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 { parse as parseUrl } from 'url'; - -/** - * This function extracts the relevant "shards" and "shard_num" - * params from the url. - * - * @param {string} testBundleUrl - * @return {object} params - * @property {number} params.shards - the total number of shards - * @property {number} params.shard_num - the current shard number, 1 based - */ -export function getShardingParamsFromUrl(url) { - const parsedUrl = parseUrl(url, true); - const parsedQuery = parsedUrl.query || {}; - - const params = {}; - if (parsedQuery.shards) { - params.shards = parseInt(parsedQuery.shards, 10); - } - - if (parsedQuery.shard_num) { - params.shard_num = parseInt(parsedQuery.shard_num, 10); - } - - return params; -} diff --git a/src/legacy/ui/public/test_harness/test_sharding/index.js b/src/legacy/ui/public/test_harness/test_sharding/index.js deleted file mode 100644 index cdca50725a058..0000000000000 --- a/src/legacy/ui/public/test_harness/test_sharding/index.js +++ /dev/null @@ -1,20 +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. - */ - -export { setupTestSharding } from './setup_test_sharding'; diff --git a/src/legacy/ui/public/test_harness/test_sharding/setup_test_sharding.js b/src/legacy/ui/public/test_harness/test_sharding/setup_test_sharding.js deleted file mode 100644 index fce1876162387..0000000000000 --- a/src/legacy/ui/public/test_harness/test_sharding/setup_test_sharding.js +++ /dev/null @@ -1,69 +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 { uniq, defaults } from 'lodash'; - -import { findTestBundleUrl } from './find_test_bundle_url'; -import { getShardingParamsFromUrl } from './get_sharding_params_from_url'; -import { setupTopLevelDescribeFilter } from './setup_top_level_describe_filter'; -import { getShardNum } from './get_shard_num'; - -const DEFAULT_PARAMS = { - shards: 1, - shard_num: 1, -}; - -export function setupTestSharding() { - const pageUrl = window.location.href; - const bundleUrl = findTestBundleUrl(); - - // supports overriding params via the debug page - // url in dev mode - const params = defaults( - {}, - getShardingParamsFromUrl(pageUrl), - getShardingParamsFromUrl(bundleUrl), - DEFAULT_PARAMS - ); - - const { shards: shardTotal, shard_num: shardNum } = params; - if (shardNum < 1 || shardNum > shardTotal) { - throw new TypeError( - `shard_num param of ${shardNum} must be greater 0 and less than the total, ${shardTotal}` - ); - } - - // track and log the number of ignored describe calls - const ignoredDescribeShards = []; - before(() => { - const ignoredCount = ignoredDescribeShards.length; - const ignoredFrom = uniq(ignoredDescribeShards).join(', '); - console.log(`Ignored ${ignoredCount} top-level suites from ${ignoredFrom}`); - }); - - // Filter top-level describe statements as they come - setupTopLevelDescribeFilter((describeName) => { - const describeShardNum = getShardNum(shardTotal, describeName); - if (describeShardNum === shardNum) return true; - // track shard numbers that we ignore - ignoredDescribeShards.push(describeShardNum); - }); - - console.log(`ready to load tests for shard ${shardNum} of ${shardTotal}`); -} diff --git a/src/legacy/ui/public/test_harness/test_sharding/setup_top_level_describe_filter.js b/src/legacy/ui/public/test_harness/test_sharding/setup_top_level_describe_filter.js deleted file mode 100644 index 726f890077b94..0000000000000 --- a/src/legacy/ui/public/test_harness/test_sharding/setup_top_level_describe_filter.js +++ /dev/null @@ -1,125 +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. - */ - -/** - * Intercept all calls to mocha.describe() and determine - * which calls make it through using a filter function. - * - * The filter function is also only called for top-level - * describe() calls; all describe calls nested within another - * are allowed based on the filter value for the parent - * describe - * - * ## example - * - * assume tests that look like this: - * - * ```js - * describe('section 1', () => { - * describe('item 1', () => { - * - * }) - * }) - * ``` - * - * If the filter function returned true for "section 1" then "item 1" - * would automatically be defined. If it returned false for "section 1" - * then "section 1" would be ignored and "item 1" would never be defined - * - * @param {function} test - a function that takes the first argument - * passed to describe, the sections name, and - * returns true if the describe call should - * be delegated to mocha, any other value causes - * the describe call to be ignored - * @return {undefined} - */ -export function setupTopLevelDescribeFilter(test) { - const originalDescribe = window.describe; - - if (!originalDescribe) { - throw new TypeError( - 'window.describe must be defined by mocha before test sharding can be setup' - ); - } - - /** - * When describe is called it is likely to make additional, nested, - * calls to describe. We track how deeply nested we are at any time - * with a depth counter, `describeCallDepth`. - * - * Before delegating a describe call to mocha we increment - * that counter, and once mocha is done we decrement it. - * - * This way, we can check if `describeCallDepth > 0` at any time - * to know if we are already within a describe call. - * - * ```js - * // +1 - * describe('section 1', () => { - * // describeCallDepth = 1 - * // +1 - * describe('item 1', () => { - * // describeCallDepth = 2 - * }) - * // -1 - * }) - * // -1 - * // describeCallDepth = 0 - * ``` - * - * @type {Number} - */ - let describeCallDepth = 0; - - const describeInterceptor = function (describeName, describeBody) { - const context = this; - - const isTopLevelCall = describeCallDepth === 0; - const shouldIgnore = isTopLevelCall && Boolean(test(describeName)) === false; - if (shouldIgnore) return; - - /** - * we wrap the delegation to mocha in a try/finally block - * to ensure that our describeCallDepth counter stays up - * to date even if the call throws an error. - * - * note that try/finally won't actually catch the error, it - * will continue to propagate up the call stack - */ - let result; - try { - describeCallDepth += 1; - result = originalDescribe.call(context, describeName, describeBody); - } finally { - describeCallDepth -= 1; - } - return result; - }; - - // to allow describe.only calls. we dont need interceptor as it will call describe internally - describeInterceptor.only = originalDescribe.only; - describeInterceptor.skip = originalDescribe.skip; - - // ensure that window.describe isn't messed with by other code - Object.defineProperty(window, 'describe', { - configurable: false, - enumerable: true, - value: describeInterceptor, - }); -} diff --git a/src/legacy/ui/ui_exports/ui_export_defaults.js b/src/legacy/ui/ui_exports/ui_export_defaults.js index f7ee9aa056762..348f4ee77fab4 100644 --- a/src/legacy/ui/ui_exports/ui_export_defaults.js +++ b/src/legacy/ui/ui_exports/ui_export_defaults.js @@ -30,7 +30,6 @@ export const UI_EXPORT_DEFAULTS = { webpackAliases: { ui: resolve(ROOT, 'src/legacy/ui/public'), __kibanaCore__$: resolve(ROOT, 'src/core/public'), - test_harness: resolve(ROOT, 'src/test_harness/public'), }, styleSheetPaths: ['light', 'dark'].map((theme) => ({ diff --git a/tasks/config/karma.js b/tasks/config/karma.js deleted file mode 100644 index 114e09876406c..0000000000000 --- a/tasks/config/karma.js +++ /dev/null @@ -1,207 +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 { dirname } from 'path'; -import { times } from 'lodash'; -import { makeJunitReportPath } from '@kbn/test'; -import * as UiSharedDeps from '@kbn/ui-shared-deps'; - -const TOTAL_CI_SHARDS = 4; -const ROOT = dirname(require.resolve('../../package.json')); -const buildHash = String(Number.MAX_SAFE_INTEGER); - -module.exports = function (grunt) { - function pickBrowser() { - if (grunt.option('browser')) { - return grunt.option('browser'); - } - if (process.env.TEST_BROWSER_HEADLESS === '1') { - return 'Chrome_Headless'; - } - return 'Chrome'; - } - - function pickReporters() { - // available reporters: https://npmjs.org/browse/keyword/karma-reporter - if (process.env.CI && process.env.DISABLE_JUNIT_REPORTER) { - return ['dots']; - } - - if (process.env.CI) { - return ['dots', 'junit']; - } - - return ['progress']; - } - - function getKarmaFiles(shardNum) { - return [ - 'http://localhost:5610/test_bundle/built_css.css', - // Sets global variables normally set by the bootstrap.js script - 'http://localhost:5610/test_bundle/karma/globals.js', - - ...UiSharedDeps.jsDepFilenames.map( - (chunkFilename) => - `http://localhost:5610/${buildHash}/bundles/kbn-ui-shared-deps/${chunkFilename}` - ), - `http://localhost:5610/${buildHash}/bundles/kbn-ui-shared-deps/${UiSharedDeps.jsFilename}`, - - shardNum === undefined - ? `http://localhost:5610/${buildHash}/bundles/tests.bundle.js` - : `http://localhost:5610/${buildHash}/bundles/tests.bundle.js?shards=${TOTAL_CI_SHARDS}&shard_num=${shardNum}`, - - `http://localhost:5610/${buildHash}/bundles/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`, - // this causes tilemap tests to fail, probably because the eui styles haven't been - // included in the karma harness a long some time, if ever - // `http://localhost:5610/bundles/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}`, - `http://localhost:5610/${buildHash}/bundles/tests.style.css`, - ]; - } - - const config = { - options: { - // base path that will be used to resolve all patterns (eg. files, exclude) - basePath: '', - - captureTimeout: 30000, - browserNoActivityTimeout: 120000, - frameworks: ['mocha'], - plugins: [ - 'karma-chrome-launcher', - 'karma-coverage', - 'karma-firefox-launcher', - 'karma-ie-launcher', - 'karma-junit-reporter', - 'karma-mocha', - 'karma-safari-launcher', - ], - port: 9876, - colors: true, - logLevel: grunt.option('debug') || grunt.option('verbose') ? 'DEBUG' : 'INFO', - autoWatch: false, - browsers: [pickBrowser()], - customLaunchers: { - Chrome_Headless: { - base: 'Chrome', - flags: ['--headless', '--disable-gpu', '--remote-debugging-port=9222'], - }, - }, - - reporters: pickReporters(), - - junitReporter: { - outputFile: makeJunitReportPath(ROOT, 'karma'), - useBrowserName: false, - nameFormatter: (_, result) => [...result.suite, result.description].join(' '), - classNameFormatter: (_, result) => { - const rootSuite = result.suite[0] || result.description; - return `Browser Unit Tests.${rootSuite.replace(/\./g, '·')}`; - }, - }, - - // list of files / patterns to load in the browser - files: getKarmaFiles(), - - proxies: { - '/tests/': 'http://localhost:5610/tests/', - '/test_bundle/': 'http://localhost:5610/test_bundle/', - [`/${buildHash}/bundles/`]: `http://localhost:5610/${buildHash}/bundles/`, - }, - - client: { - mocha: { - reporter: 'html', // change Karma's debug.html to the mocha web reporter - timeout: 10000, - slow: 5000, - }, - }, - }, - - dev: { singleRun: false }, - unit: { singleRun: true }, - coverage: { - singleRun: true, - reporters: ['coverage'], - coverageReporter: { - reporters: [{ type: 'html', dir: 'coverage' }, { type: 'text-summary' }], - }, - }, - }; - - /** - * ------------------------------------------------------------ - * CI sharding - * ------------------------------------------------------------ - * - * Every test retains nearly all of the memory it causes to be allocated, - * which has started to kill the test browser as the size of the test suite - * increases. This is a deep-rooted problem that will take some serious - * work to fix. - * - * CI sharding is a short-term solution that splits the top-level describe - * calls into different "shards" and instructs karma to only run one shard - * at a time, reloading the browser in between each shard and forcing the - * memory from the previous shard to be released. - * - * ## how - * - * Rather than modify the bundling process to produce multiple testing - * bundles, top-level describe calls are sharded by their first argument, - * the suite name. - * - * The number of shards to create is controlled with the TOTAL_CI_SHARDS - * constant defined at the top of this file. - * - * ## controlling sharding - * - * To control sharding in a specific karma configuration, the total number - * of shards to create (?shards=X), and the current shard number - * (&shard_num=Y), are added to the testing bundle url and read by the - * test_harness/setup_test_sharding[1] module. This allows us to use a - * different number of shards in different scenarios (ie. running - * `yarn test:karma` runs the tests in a single shard, effectively - * disabling sharding) - * - * These same parameters can also be defined in the URL/query string of the - * karma debug page (started when you run `yarn test:karma:debug`). - * - * ## debugging - * - * It is *possible* that some tests will only pass if run after/with certain - * other suites. To debug this, make sure that your tests pass in isolation - * (by clicking the suite name on the karma debug page) and that it runs - * correctly in it's given shard (using the `?shards=X&shard_num=Y` query - * string params on the karma debug page). You can spot the shard number - * a test is running in by searching for the "ready to load tests for shard X" - * log message. - * - * [1]: src/legacy/ui/public/test_harness/test_sharding/setup_test_sharding.js - */ - times(TOTAL_CI_SHARDS, (i) => { - const n = i + 1; - config[`ciShard-${n}`] = { - singleRun: true, - options: { - files: getKarmaFiles(n), - }, - }; - }); - - return config; -}; diff --git a/tasks/config/run.js b/tasks/config/run.js index 98a1226834bc6..9ac8f72d56d4a 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -17,7 +17,6 @@ * under the License. */ -import { resolve } from 'path'; import { getFunctionalTestGroupRunConfigs } from '../function_test_groups'; const { version } = require('../../package.json'); @@ -25,44 +24,7 @@ const KIBANA_INSTALL_DIR = process.env.KIBANA_INSTALL_DIR || `./build/oss/kibana-${version}-SNAPSHOT-${process.platform}-x86_64`; -module.exports = function (grunt) { - function createKbnServerTask({ runBuild, flags = [] }) { - return { - options: { - wait: false, - ready: /http server running/, - quiet: false, - failOnError: false, - }, - cmd: runBuild ? `./build/${runBuild}/bin/kibana` : process.execPath, - args: [ - ...(runBuild ? [] : [require.resolve('../../scripts/kibana'), '--oss']), - - '--logging.json=false', - - ...flags, - - // allow the user to override/inject flags by defining cli args starting with `--kbnServer.` - ...grunt.option.flags().reduce(function (flags, flag) { - if (flag.startsWith('--kbnServer.')) { - flags.push(`--${flag.slice(12)}`); - } - - return flags; - }, []), - ], - }; - } - - const karmaTestServerFlags = [ - '--env.name=development', - '--plugins.initialize=false', - '--optimize.bundleFilter=tests', - '--optimize.validateSyntaxOfNodeModules=false', - '--server.port=5610', - '--migrations.skip=true', - ]; - +module.exports = function () { const NODE = 'node'; const YARN = 'yarn'; const scriptWithGithubChecks = ({ title, options, cmd, args }) => @@ -177,37 +139,6 @@ module.exports = function (grunt) { ], }), - // used by the test:karma task - // runs the kibana server to serve the browser test bundle - karmaTestServer: createKbnServerTask({ - flags: [...karmaTestServerFlags], - }), - browserSCSS: createKbnServerTask({ - flags: [...karmaTestServerFlags, '--optimize', '--optimize.enabled=false'], - }), - - // used by the test:coverage task - // runs the kibana server to serve the instrumented version of the browser test bundle - karmaTestCoverageServer: createKbnServerTask({ - flags: [...karmaTestServerFlags, '--tests_bundle.instrument=true'], - }), - - // used by the test:karma:debug task - // runs the kibana server to serve the browser test bundle, but listens for changes - // to the public/browser code and rebuilds the test bundle on changes - karmaTestDebugServer: createKbnServerTask({ - flags: [ - ...karmaTestServerFlags, - '--dev', - '--no-dev-config', - '--no-watch', - '--no-base-path', - '--optimize.watchPort=5611', - '--optimize.watchPrebuild=true', - '--optimize.bundleDir=' + resolve(__dirname, '../../data/optimize/testdev'), - ], - }), - verifyNotice: scriptWithGithubChecks({ title: 'Verify NOTICE.txt', options: { @@ -325,7 +256,6 @@ module.exports = function (grunt) { 'test:jest_integration' ), test_projects: gruntTaskWithGithubChecks('Project tests', 'test:projects'), - test_karma_ci: gruntTaskWithGithubChecks('Browser tests', 'test:karma-ci'), ...getFunctionalTestGroupRunConfigs({ kibanaInstallDir: KIBANA_INSTALL_DIR, diff --git a/tasks/jenkins.js b/tasks/jenkins.js index eece5df61a7d1..adfb6f0f46868 100644 --- a/tasks/jenkins.js +++ b/tasks/jenkins.js @@ -37,7 +37,6 @@ module.exports = function (grunt) { 'run:test_jest', 'run:test_jest_integration', 'run:test_projects', - 'run:test_karma_ci', 'run:test_hardening', 'run:test_package_safer_lodash_set', 'run:apiIntegrationTests', diff --git a/tasks/test.js b/tasks/test.js index 09821b97fe2e8..f370ea0b948c6 100644 --- a/tasks/test.js +++ b/tasks/test.js @@ -17,8 +17,6 @@ * under the License. */ -import _, { keys } from 'lodash'; - import { run } from '../utilities/visual_regression'; module.exports = function (grunt) { @@ -31,25 +29,6 @@ module.exports = function (grunt) { } ); - grunt.registerTask('test:karma', [ - 'checkPlugins', - 'run:browserSCSS', - 'run:karmaTestServer', - 'karma:unit', - ]); - - grunt.registerTask('test:karma-ci', () => { - const ciShardTasks = keys(grunt.config.get('karma')) - .filter((key) => key.startsWith('ciShard-')) - .map((key) => `karma:${key}`); - - grunt.log.ok(`Running UI tests in ${ciShardTasks.length} shards`); - grunt.task.run(['run:browserSCSS']); - grunt.task.run(['run:karmaTestServer', ...ciShardTasks]); - }); - - grunt.registerTask('test:coverage', ['run:karmaTestCoverageServer', 'karma:coverage']); - grunt.registerTask('test:quick', [ 'checkPlugins', 'run:mocha', @@ -57,18 +36,16 @@ module.exports = function (grunt) { 'test:jest', 'test:jest_integration', 'test:projects', - 'test:karma', 'run:apiIntegrationTests', ]); - grunt.registerTask('test:karmaDebug', ['checkPlugins', 'run:karmaTestDebugServer', 'karma:dev']); grunt.registerTask('test:mochaCoverage', ['run:mochaCoverage']); grunt.registerTask('test', (subTask) => { if (subTask) grunt.fail.fatal(`invalid task "test:${subTask}"`); grunt.task.run( - _.compact([ + [ !grunt.option('quick') && 'run:eslint', !grunt.option('quick') && 'run:sasslint', !grunt.option('quick') && 'run:checkTsProjects', @@ -78,7 +55,7 @@ module.exports = function (grunt) { 'run:checkFileCasing', 'run:licenses', 'test:quick', - ]) + ].filter(Boolean) ); }); diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh index bc927b1ed7b4d..77480554f738c 100755 --- a/test/scripts/jenkins_xpack.sh +++ b/test/scripts/jenkins_xpack.sh @@ -3,9 +3,9 @@ source test/scripts/jenkins_test_setup.sh if [[ -z "$CODE_COVERAGE" ]] ; then - echo " -> Running mocha tests" - cd "$XPACK_DIR" - checks-reporter-with-killswitch "X-Pack Karma Tests" yarn test:karma + echo " -> Building legacy styles for x-pack canvas storyshot tests" + cd "$KIBANA_DIR" + node scripts/build_sass echo "" echo "" diff --git a/x-pack/README.md b/x-pack/README.md index 03d2e3287c0f0..0449f1fc1bdab 100644 --- a/x-pack/README.md +++ b/x-pack/README.md @@ -44,14 +44,6 @@ If you want to run tests only for a specific plugin (to save some time), you can yarn test --plugins [,]* # where is "reporting", etc. ``` -#### Debugging browser tests -``` -yarn test:karma:debug -``` -Initializes an environment for debugging the browser tests. Includes an dedicated instance of the kibana server for building the test bundle, and a karma server. When running this task the build is optimized for the first time and then a karma-owned instance of the browser is opened. Click the "debug" button to open a new tab that executes the unit tests. - -Run single tests by appending `grep` parameter to the end of the URL. For example `http://localhost:9876/debug.html?grep=ML%20-%20Explorer%20Controller` will only run tests with 'ML - Explorer Controller' in the describe block. - #### Running server unit tests You can run mocha unit tests by running: diff --git a/x-pack/gulpfile.js b/x-pack/gulpfile.js index 7e5ab9b18f019..78ed2bff8cb01 100644 --- a/x-pack/gulpfile.js +++ b/x-pack/gulpfile.js @@ -8,7 +8,6 @@ require('../src/setup_node_env'); const { buildTask } = require('./tasks/build'); const { devTask } = require('./tasks/dev'); -const { testTask, testKarmaTask, testKarmaDebugTask } = require('./tasks/test'); const { downloadChromium } = require('./tasks/download_chromium'); // export the tasks that are runnable from the CLI @@ -16,7 +15,4 @@ module.exports = { build: buildTask, dev: devTask, downloadChromium, - test: testTask, - 'test:karma': testKarmaTask, - 'test:karma:debug': testKarmaDebugTask, }; diff --git a/x-pack/package.json b/x-pack/package.json index 39bdb76ac7a73..d1f638ccad8d0 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -11,8 +11,6 @@ "build": "gulp build", "testonly": "echo 'Deprecated, use `yarn test`' && gulp test", "test": "gulp test", - "test:karma:debug": "gulp test:karma:debug", - "test:karma": "gulp test:karma", "test:jest": "node scripts/jest", "test:mocha": "node scripts/mocha" }, diff --git a/x-pack/plugins/canvas/scripts/test_browser.js b/x-pack/plugins/canvas/scripts/test_browser.js deleted file mode 100644 index e04fac0615284..0000000000000 --- a/x-pack/plugins/canvas/scripts/test_browser.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -require('./_helpers').runGulpTask('canvas:test:karma'); diff --git a/x-pack/plugins/canvas/scripts/test_dev.js b/x-pack/plugins/canvas/scripts/test_dev.js deleted file mode 100644 index 8b03d7930d473..0000000000000 --- a/x-pack/plugins/canvas/scripts/test_dev.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -require('./_helpers').runGulpTask('canvas:karma:debug'); diff --git a/x-pack/tasks/test.ts b/x-pack/tasks/test.ts deleted file mode 100644 index 0d990bff9f44e..0000000000000 --- a/x-pack/tasks/test.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as pluginHelpers from '@kbn/plugin-helpers'; -import gulp from 'gulp'; - -import { getEnabledPlugins } from './helpers/flags'; - -export const testServerTask = async () => { - throw new Error('server mocha tests are now included in the `node scripts/mocha` script'); -}; - -export const testKarmaTask = async () => { - const plugins = await getEnabledPlugins(); - await pluginHelpers.run('testKarma', { - plugins: plugins.join(','), - }); -}; - -export const testKarmaDebugTask = async () => { - const plugins = await getEnabledPlugins(); - await pluginHelpers.run('testKarma', { - dev: true, - plugins: plugins.join(','), - }); -}; - -export const testTask = gulp.series(testKarmaTask, testServerTask); diff --git a/yarn.lock b/yarn.lock index fd6019750dda5..0638a019a9402 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6544,11 +6544,6 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -abbrev@1.0.x: - version "1.0.9" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" - integrity sha1-kbR5JYinc4wl813W9jdSovh3YTU= - abort-controller@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-2.0.3.tgz#b174827a732efadff81227ed4b8d1cc569baf20a" @@ -6706,11 +6701,6 @@ after-all-results@^2.0.0: resolved "https://registry.yarnpkg.com/after-all-results/-/after-all-results-2.0.0.tgz#6ac2fc202b500f88da8f4f5530cfa100f4c6a2d0" integrity sha1-asL8ICtQD4jaj09VMM+hAPTGotA= -after@0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" - integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8= - agent-base@4: version "4.2.0" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.0.tgz#9838b5c3392b962bad031e6a4c5e1024abec45ce" @@ -7661,11 +7651,6 @@ array.prototype.flatmap@^1.2.3: es-abstract "^1.17.0-next.1" function-bind "^1.1.1" -arraybuffer.slice@~0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675" - integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog== - arrify@^1.0.0, arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -7831,11 +7816,6 @@ async-value@^1.2.2: resolved "https://registry.yarnpkg.com/async-value/-/async-value-1.2.2.tgz#84517a1e7cb6b1a5b5e181fa31be10437b7fb125" integrity sha1-hFF6Hny2saW14YH6Mb4QQ3t/sSU= -async@1.x, async@^1.4.2, async@^1.5.2, async@~1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" - integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= - async@2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/async/-/async-2.4.0.tgz#4990200f18ea5b837c2cc4f8c031a6985c385611" @@ -7843,6 +7823,11 @@ async@2.4.0: dependencies: lodash "^4.14.0" +async@^1.4.2, async@^1.5.2, async@~1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= + async@^2.0.0, async@^2.1.4: version "2.6.0" resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" @@ -7857,7 +7842,7 @@ async@^2.6.0, async@^2.6.1: dependencies: lodash "^4.17.10" -async@^2.6.2, async@^2.6.3: +async@^2.6.3: version "2.6.3" resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== @@ -8546,11 +8531,6 @@ bach@^1.0.0: async-settle "^1.0.0" now-and-later "^2.0.0" -backo2@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" - integrity sha1-MasayLEpNjRj41s+u2n038+6eUc= - backport@5.5.1: version "5.5.1" resolved "https://registry.yarnpkg.com/backport/-/backport-5.5.1.tgz#2eeddbdc4cfc0530119bdb2b0c3c30bc7ef574dd" @@ -8582,11 +8562,6 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= -base64-arraybuffer@0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" - integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg= - base64-js@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978" @@ -8602,11 +8577,6 @@ base64-js@^1.1.2, base64-js@^1.2.1, base64-js@^1.3.0, base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== -base64id@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6" - integrity sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY= - base64url@^3.0.0, base64url@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" @@ -8654,13 +8624,6 @@ before-after-hook@^1.4.0: resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-1.4.0.tgz#2b6bf23dca4f32e628fd2747c10a37c74a4b484d" integrity sha512-l5r9ir56nda3qu14nAXIlyq1MmUSs0meCIaFAh8HwkFwP1F8eToOuS3ah2VAHHcY04jaYD7FpJC5JTXHYRbkzg== -better-assert@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522" - integrity sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI= - dependencies: - callsite "1.0.0" - big-integer@^1.6.16: version "1.6.48" resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e" @@ -8739,11 +8702,6 @@ bl@^3.0.0: dependencies: readable-stream "^3.0.1" -blob@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" - integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig== - block-stream@*: version "0.0.9" resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" @@ -8828,22 +8786,6 @@ body-parser@1.19.0, body-parser@^1.18.1, body-parser@^1.18.3: raw-body "2.4.0" type-is "~1.6.17" -body-parser@^1.16.1: - version "1.18.2" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454" - integrity sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ= - dependencies: - bytes "3.0.0" - content-type "~1.0.4" - debug "2.6.9" - depd "~1.1.1" - http-errors "~1.6.2" - iconv-lite "0.4.19" - on-finished "~2.3.0" - qs "6.5.1" - raw-body "2.3.2" - type-is "~1.6.15" - body@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/body/-/body-5.1.0.tgz#e4ba0ce410a46936323367609ecb4e6553125069" @@ -8987,7 +8929,7 @@ braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" -braces@^3.0.1, braces@^3.0.2, braces@~3.0.2: +braces@^3.0.1, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -9491,11 +9433,6 @@ caller-path@^2.0.0: dependencies: caller-callsite "^2.0.0" -callsite@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" - integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA= - callsites@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" @@ -9962,7 +9899,7 @@ chokidar@^2.0.0, chokidar@^2.1.2, chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" -chokidar@^3.0.0, chokidar@^3.2.2: +chokidar@^3.2.2: version "3.3.1" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450" integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg== @@ -10493,16 +10430,16 @@ colors@1.0.3: resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= -colors@^1.1.0, colors@^1.2.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.2.tgz#2df8ff573dfbf255af562f8ce7181d6b971a359b" - integrity sha512-rhP0JSBGYvpcNQj4s5AdShMeE5ahMop96cTeDl/v9qQQm2fYClE2QXZRi8wLzc+GmXSxdIqqbOIAhyObEXDbfQ== - colors@^1.1.2, colors@^1.3.2: version "1.3.3" resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.3.tgz#39e005d546afe01e01f9c4ca8fa50f686a01205d" integrity sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg== +colors@^1.2.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.2.tgz#2df8ff573dfbf255af562f8ce7181d6b971a359b" + integrity sha512-rhP0JSBGYvpcNQj4s5AdShMeE5ahMop96cTeDl/v9qQQm2fYClE2QXZRi8wLzc+GmXSxdIqqbOIAhyObEXDbfQ== + colors@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" @@ -10619,21 +10556,11 @@ compare-versions@3.5.1: resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.5.1.tgz#26e1f5cf0d48a77eced5046b9f67b6b61075a393" integrity sha512-9fGPIB7C6AyM18CJJBHt5EnCZDG3oiTJYy0NjfIAGjKpzv0tkxWko7TNQHF5ymqm7IH03tqmeuBxtvD+Izh6mg== -component-bind@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" - integrity sha1-AMYIq33Nk4l8AAllGx06jh5zu9E= - -component-emitter@1.2.1, component-emitter@^1.2.0, component-emitter@^1.2.1: +component-emitter@^1.2.0, component-emitter@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= -component-inherit@0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" - integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM= - compose-function@3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/compose-function/-/compose-function-3.0.3.tgz#9ed675f13cc54501d30950a486ff6a7ba3ab185f" @@ -10817,16 +10744,6 @@ connect@^3.4.0: parseurl "~1.3.3" utils-merge "1.0.1" -connect@^3.6.0: - version "3.6.6" - resolved "https://registry.yarnpkg.com/connect/-/connect-3.6.6.tgz#09eff6c55af7236e137135a72574858b6786f524" - integrity sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ= - dependencies: - debug "2.6.9" - finalhandler "1.1.0" - parseurl "~1.3.2" - utils-merge "1.0.1" - console-browserify@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" @@ -11559,11 +11476,6 @@ custom-event-polyfill@^0.3.0: resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-0.3.0.tgz#99807839be62edb446b645832e0d80ead6fa1888" integrity sha1-mYB4Ob5i7bRGtkWDLg2A6tb6GIg= -custom-event@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" - integrity sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU= - cyclist@~0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" @@ -11882,24 +11794,11 @@ date-fns@^1.27.2: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6" integrity sha512-lbTXWZ6M20cWH8N9S6afb0SBm6tMk+uUg6z3MqHPKE9atmsY3kJkTm8vKe93izJ2B2+q5MV990sM2CHgtAZaOw== -date-format@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/date-format/-/date-format-2.1.0.tgz#31d5b5ea211cf5fd764cd38baf9d033df7e125cf" - integrity sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA== - date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs= -dateformat@^1.0.6, dateformat@~1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9" - integrity sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk= - dependencies: - get-stdin "^4.0.1" - meow "^3.3.0" - dateformat@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-2.2.0.tgz#4065e2013cf9fb916ddfd82efb506ad4c6769062" @@ -11910,6 +11809,14 @@ dateformat@^3.0.2: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== +dateformat@~1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9" + integrity sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk= + dependencies: + get-stdin "^4.0.1" + meow "^3.3.0" + debug-fabulous@1.X: version "1.1.0" resolved "https://registry.yarnpkg.com/debug-fabulous/-/debug-fabulous-1.1.0.tgz#af8a08632465224ef4174a9f06308c3c2a1ebc8e" @@ -11926,7 +11833,7 @@ debug@2.6.9, debug@^2.0.0, debug@^2.1.0, debug@^2.1.1, debug@^2.2.0, debug@^2.3. dependencies: ms "2.0.0" -debug@3.1.0, debug@=3.1.0, debug@~3.1.0: +debug@3.1.0, debug@=3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== @@ -12300,7 +12207,7 @@ depd@1.1.1: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" integrity sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k= -depd@~1.1.1, depd@~1.1.2: +depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= @@ -12509,11 +12416,6 @@ dfa@^1.2.0: resolved "https://registry.yarnpkg.com/dfa/-/dfa-1.2.0.tgz#96ac3204e2d29c49ea5b57af8d92c2ae12790657" integrity sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q== -di@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" - integrity sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw= - diacritics@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/diacritics/-/diacritics-1.3.0.tgz#3efa87323ebb863e6696cebb0082d48ff3d6f7a1" @@ -12668,16 +12570,6 @@ dom-helpers@^5.0.0: "@babel/runtime" "^7.6.3" csstype "^2.6.7" -dom-serialize@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" - integrity sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs= - dependencies: - custom-event "~1.0.0" - ent "~2.2.0" - extend "^3.0.0" - void-elements "^2.0.0" - dom-serializer@0, dom-serializer@~0.1.0, dom-serializer@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" @@ -13130,7 +13022,7 @@ enabled@2.0.x: resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== -encodeurl@^1.0.2, encodeurl@~1.0.1, encodeurl@~1.0.2: +encodeurl@^1.0.2, encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= @@ -13156,46 +13048,6 @@ end-of-stream@^1.4.1, end-of-stream@^1.4.4: dependencies: once "^1.4.0" -engine.io-client@~3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.2.1.tgz#6f54c0475de487158a1a7c77d10178708b6add36" - integrity sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw== - dependencies: - component-emitter "1.2.1" - component-inherit "0.0.3" - debug "~3.1.0" - engine.io-parser "~2.1.1" - has-cors "1.1.0" - indexof "0.0.1" - parseqs "0.0.5" - parseuri "0.0.5" - ws "~3.3.1" - xmlhttprequest-ssl "~1.5.4" - yeast "0.1.2" - -engine.io-parser@~2.1.0, engine.io-parser@~2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.1.3.tgz#757ab970fbf2dfb32c7b74b033216d5739ef79a6" - integrity sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA== - dependencies: - after "0.8.2" - arraybuffer.slice "~0.0.7" - base64-arraybuffer "0.1.5" - blob "0.0.5" - has-binary2 "~1.0.2" - -engine.io@~3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.2.1.tgz#b60281c35484a70ee0351ea0ebff83ec8c9522a2" - integrity sha512-+VlKzHzMhaU+GsCIg4AoXF1UdDFjHHwMmMKqMJNDNLlUlejz58FCy4LBqB2YVJskHGYl06BatYWKP2TVdVXE5w== - dependencies: - accepts "~1.3.4" - base64id "1.0.0" - cookie "0.3.1" - debug "~3.1.0" - engine.io-parser "~2.1.0" - ws "~3.3.1" - enhanced-resolve@4.1.0, enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" @@ -13214,11 +13066,6 @@ enhanced-resolve@~0.9.0: memory-fs "^0.2.0" tapable "^0.1.8" -ent@~2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" - integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0= - entities@^1.1.1, entities@^1.1.2, entities@~1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" @@ -13570,18 +13417,6 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.0, escape-string-regexp@^1 resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= -escodegen@1.8.x: - version "1.8.1" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018" - integrity sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg= - dependencies: - esprima "^2.7.1" - estraverse "^1.9.1" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.2.0" - escodegen@^1.11.0: version "1.14.1" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.1.tgz#ba01d0c8278b5e95a9a45350142026659027a457" @@ -13983,11 +13818,6 @@ espree@^6.1.2: acorn-jsx "^5.1.0" eslint-visitor-keys "^1.1.0" -esprima@2.7.x, esprima@^2.7.1: - version "2.7.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" - integrity sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE= - esprima@^3.1.3, esprima@~3.1.0: version "3.1.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" @@ -14017,11 +13847,6 @@ esrecurse@^4.1.0: dependencies: estraverse "^4.1.0" -estraverse@^1.9.1: - version "1.9.3" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44" - integrity sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q= - estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" @@ -14075,11 +13900,6 @@ eventemitter2@~0.4.13: resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-0.4.14.tgz#8f61b75cde012b2e9eb284d4545583b5643b61ab" integrity sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas= -eventemitter3@1.x.x: - version "1.2.0" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" - integrity sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg= - eventemitter3@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" @@ -14876,19 +14696,6 @@ filter-obj@^1.1.0: resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" integrity sha1-mzERErxsYSehbgFsbF1/GeCAXFs= -finalhandler@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5" - integrity sha1-zgtoVbRYU+eRsvzGgARtiCU91/U= - dependencies: - debug "2.6.9" - encodeurl "~1.0.1" - escape-html "~1.0.3" - on-finished "~2.3.0" - parseurl "~1.3.2" - statuses "~1.3.1" - unpipe "~1.0.0" - finalhandler@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" @@ -15340,13 +15147,6 @@ front-matter@2.1.2: dependencies: js-yaml "^3.4.6" -fs-access@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/fs-access/-/fs-access-1.0.1.tgz#d6a87f262271cefebec30c553407fb995da8777a" - integrity sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o= - dependencies: - null-check "^1.0.0" - fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" @@ -15875,17 +15675,6 @@ glob@7.1.4, glob@~7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^5.0.15, glob@~5.0.0: - version "5.0.15" - resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" - integrity sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E= - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "2 || 3" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@^6.0.1, glob@^6.0.4: version "6.0.4" resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" @@ -15909,6 +15698,17 @@ glob@^7.0.5, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@~7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" +glob@~5.0.0: + version "5.0.15" + resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" + integrity sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E= + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@~7.0.0: version "7.0.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.6.tgz#211bafaf49e525b8cd93260d14ab136152b3f57a" @@ -16620,13 +16420,6 @@ grunt-contrib-watch@^1.1.0: lodash "^4.17.10" tiny-lr "^1.1.1" -grunt-karma@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/grunt-karma/-/grunt-karma-3.0.2.tgz#4f14386d43ee45f8f6b98081862e4910f5056764" - integrity sha512-imNhQO1bR1O7X6/3F5vO0o7mKy4xdkpSd40QVfxGO70cBAFcOqjv2Zu5QzsfEsSrppuu3N0vIQPbfBRjeGdpWg== - dependencies: - lodash "^4.17.10" - grunt-known-options@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/grunt-known-options/-/grunt-known-options-1.1.0.tgz#a4274eeb32fa765da5a7a3b1712617ce3b144149" @@ -16917,23 +16710,11 @@ has-ansi@^3.0.0: dependencies: ansi-regex "^3.0.0" -has-binary2@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d" - integrity sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw== - dependencies: - isarray "2.0.1" - has-color@~0.1.0: version "0.1.7" resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f" integrity sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8= -has-cors@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" - integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk= - has-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" @@ -17348,16 +17129,6 @@ http-deceiver@^1.2.7: resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= -http-errors@1.6.2, http-errors@~1.6.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" - integrity sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY= - dependencies: - depd "1.1.1" - inherits "2.0.3" - setprototypeof "1.0.3" - statuses ">= 1.3.1 < 2" - http-errors@1.6.3, http-errors@~1.6.3: version "1.6.3" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" @@ -17379,6 +17150,16 @@ http-errors@1.7.2, http-errors@~1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" +http-errors@~1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" + integrity sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY= + dependencies: + depd "1.1.1" + inherits "2.0.3" + setprototypeof "1.0.3" + statuses ">= 1.3.1 < 2" + http-errors@~1.7.0: version "1.7.3" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" @@ -17425,14 +17206,6 @@ http-proxy-middleware@0.19.1: lodash "^4.17.11" micromatch "^3.1.10" -http-proxy@^1.13.0: - version "1.16.2" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.16.2.tgz#06dff292952bf64dbe8471fa9df73066d4f37742" - integrity sha1-Bt/ykpUr9k2+hHH6nfcwZtTzd0I= - dependencies: - eventemitter3 "1.x.x" - requires-port "1.x.x" - http-proxy@^1.17.0: version "1.17.0" resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a" @@ -18929,11 +18702,6 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= -isarray@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e" - integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4= - isarray@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" @@ -18944,11 +18712,6 @@ isbinaryfile@4.0.2: resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.2.tgz#bfc45642da645681c610cca831022e30af426488" integrity sha512-C3FSxJdNrEr2F4z6uFtNzECDM5hXk+46fxaa+cwBe5/XrWSmzdG8DDgyjfX6/NRdBB21q2JXuRAzPCUs+fclnQ== -isbinaryfile@^4.0.2: - version "4.0.6" - resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.6.tgz#edcb62b224e2b4710830b67498c8e4e5a4d2610b" - integrity sha512-ORrEy+SNVqUhrCaal4hA4fBzhggQQ+BaLntyPOdoEiwlKZW9BZiJXjg3RMiruE4tPEI3pyVPpySHQF/dKWperg== - isemail@3.x.x: version "3.1.4" resolved "https://registry.yarnpkg.com/isemail/-/isemail-3.1.4.tgz#76e2187ff7bee59d57522c6fd1c3f09a331933cf" @@ -19106,26 +18869,6 @@ istanbul-reports@^3.0.2: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -istanbul@^0.4.0: - version "0.4.5" - resolved "https://registry.yarnpkg.com/istanbul/-/istanbul-0.4.5.tgz#65c7d73d4c4da84d4f3ac310b918fb0b8033733b" - integrity sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs= - dependencies: - abbrev "1.0.x" - async "1.x" - escodegen "1.8.x" - esprima "2.7.x" - glob "^5.0.15" - handlebars "^4.0.1" - js-yaml "3.x" - mkdirp "0.5.x" - nopt "3.x" - once "1.x" - resolve "1.1.x" - supports-color "^3.1.0" - which "^1.1.1" - wordwrap "^1.0.0" - istextorbinary@^2.1.0: version "2.2.1" resolved "https://registry.yarnpkg.com/istextorbinary/-/istextorbinary-2.2.1.tgz#a5231a08ef6dd22b268d0895084cf8d58b5bec53" @@ -19798,7 +19541,7 @@ js-tokens@^3.0.0, js-tokens@^3.0.2: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@3.13.1, js-yaml@3.x, js-yaml@^3.10.0, js-yaml@^3.13.1, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.9.0, js-yaml@~3.13.0, js-yaml@~3.13.1: +js-yaml@3.13.1, js-yaml@^3.10.0, js-yaml@^3.13.1, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.9.0, js-yaml@~3.13.0, js-yaml@~3.13.1: version "3.13.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== @@ -20209,87 +19952,6 @@ jws@^3.2.2: jwa "^1.4.1" safe-buffer "^5.0.1" -karma-chrome-launcher@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz#cf1b9d07136cc18fe239327d24654c3dbc368acf" - integrity sha512-uf/ZVpAabDBPvdPdveyk1EPgbnloPvFFGgmRhYLTDH7gEB4nZdSBk8yTU47w1g/drLSx5uMOkjKk7IWKfWg/+w== - dependencies: - fs-access "^1.0.0" - which "^1.2.1" - -karma-coverage@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/karma-coverage/-/karma-coverage-1.1.2.tgz#cc09dceb589a83101aca5fe70c287645ef387689" - integrity sha512-eQawj4Cl3z/CjxslYy9ariU4uDh7cCNFZHNWXWRpl0pNeblY/4wHR7M7boTYXWrn9bY0z2pZmr11eKje/S/hIw== - dependencies: - dateformat "^1.0.6" - istanbul "^0.4.0" - lodash "^4.17.0" - minimatch "^3.0.0" - source-map "^0.5.1" - -karma-firefox-launcher@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/karma-firefox-launcher/-/karma-firefox-launcher-1.1.0.tgz#2c47030452f04531eb7d13d4fc7669630bb93339" - integrity sha512-LbZ5/XlIXLeQ3cqnCbYLn+rOVhuMIK9aZwlP6eOLGzWdo1UVp7t6CN3DP4SafiRLjexKwHeKHDm0c38Mtd3VxA== - -karma-ie-launcher@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/karma-ie-launcher/-/karma-ie-launcher-1.0.0.tgz#497986842c490190346cd89f5494ca9830c6d59c" - integrity sha1-SXmGhCxJAZA0bNifVJTKmDDG1Zw= - dependencies: - lodash "^4.6.1" - -karma-junit-reporter@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/karma-junit-reporter/-/karma-junit-reporter-1.2.0.tgz#4f9c40cedfb1a395f8aef876abf96189917c6396" - integrity sha1-T5xAzt+xo5X4rvh2q/lhiZF8Y5Y= - dependencies: - path-is-absolute "^1.0.0" - xmlbuilder "8.2.2" - -karma-mocha@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-2.0.0.tgz#ad6b56b6a72e9b191e4c432dd30f4a44fc2435bc" - integrity sha512-qiZkZDJnn2kb9t2m4LoM4cYJHJVPoxvAYYe0B+go5s+A/3vc/3psUT05zW4yFz4vT0xHf+XzTTery8zdr8GWgA== - dependencies: - minimist "^1.2.3" - -karma-safari-launcher@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/karma-safari-launcher/-/karma-safari-launcher-1.0.0.tgz#96982a2cc47d066aae71c553babb28319115a2ce" - integrity sha1-lpgqLMR9BmquccVTursoMZEVos4= - -karma@5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/karma/-/karma-5.0.2.tgz#e404373dac6e3fa08409ae4d9eda7d83adb43ee5" - integrity sha512-RpUuCuGJfN3WnjYPGIH+VBF8023Lfm3TQH6D1kcNL+FxtEPc2UUz/nVjbVAGXH4Pm+Q7FVOAQjdAeFUpXpQ3IA== - dependencies: - body-parser "^1.16.1" - braces "^3.0.2" - chokidar "^3.0.0" - colors "^1.1.0" - connect "^3.6.0" - di "^0.0.1" - dom-serialize "^2.2.0" - flatted "^2.0.0" - glob "^7.1.1" - graceful-fs "^4.1.2" - http-proxy "^1.13.0" - isbinaryfile "^4.0.2" - lodash "^4.17.14" - log4js "^4.0.0" - mime "^2.3.1" - minimatch "^3.0.2" - qjobs "^1.1.4" - range-parser "^1.2.0" - rimraf "^2.6.0" - socket.io "2.1.1" - source-map "^0.6.1" - tmp "0.0.33" - ua-parser-js "0.7.21" - yargs "^15.3.1" - kdbush@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/kdbush/-/kdbush-3.0.0.tgz#f8484794d47004cc2d85ed3a79353dbe0abc2bf0" @@ -21127,7 +20789,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.11.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.6.1, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: +lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.11.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -21200,17 +20862,6 @@ log-update@^1.0.2: ansi-escapes "^1.0.0" cli-cursor "^1.0.2" -log4js@^4.0.0: - version "4.5.1" - resolved "https://registry.yarnpkg.com/log4js/-/log4js-4.5.1.tgz#e543625e97d9e6f3e6e7c9fc196dd6ab2cae30b5" - integrity sha512-EEEgFcE9bLgaYUKuozyFfytQM2wDHtXn4tAN41pkaxpNjAykv11GVdeI4tHtmPWW4Xrgh9R/2d7XYghDVjbKKw== - dependencies: - date-format "^2.0.0" - debug "^4.1.1" - flatted "^2.0.0" - rfdc "^1.1.4" - streamroller "^1.0.6" - logform@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/logform/-/logform-2.1.2.tgz#957155ebeb67a13164069825ce67ddb5bb2dd360" @@ -22048,7 +21699,7 @@ minimist@1.2.0, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2 resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= -minimist@1.2.5, minimist@^1.2.3, minimist@^1.2.5: +minimist@1.2.5, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -22970,7 +22621,7 @@ nodemon@^2.0.2: chalk "~0.4.0" underscore "~1.6.0" -"nopt@2 || 3", nopt@3.x, nopt@~3.0.1, nopt@~3.0.6: +"nopt@2 || 3", nopt@~3.0.1, nopt@~3.0.6: version "3.0.6" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k= @@ -23172,11 +22823,6 @@ nth-check@^1.0.2, nth-check@~1.0.1: dependencies: boolbase "~1.0.0" -null-check@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/null-check/-/null-check-1.0.0.tgz#977dffd7176012b9ec30d2a39db5cf72a0439edd" - integrity sha1-l33/1xdgErnsMNKjnbXPcqBDnt0= - null-loader@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/null-loader/-/null-loader-3.0.0.tgz#3e2b6c663c5bda8c73a54357d8fa0708dc61b245" @@ -23262,11 +22908,6 @@ object-assign@^3.0.0: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" integrity sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I= -object-component@0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291" - integrity sha1-8MaapQ78lbhmwYb0AKM3acsvEpE= - object-copy@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" @@ -23484,7 +23125,7 @@ on-headers@~1.0.2: resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== -once@1.x, once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0: +once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= @@ -24177,20 +23818,6 @@ parse5@^3.0.1: dependencies: "@types/node" "*" -parseqs@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d" - integrity sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0= - dependencies: - better-assert "~1.0.0" - -parseuri@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a" - integrity sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo= - dependencies: - better-assert "~1.0.0" - parseurl@~1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" @@ -25334,16 +24961,6 @@ q@^1.1.2: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= -qjobs@^1.1.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" - integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg== - -qs@6.5.1: - version "6.5.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" - integrity sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A== - qs@6.5.2, qs@^6.4.0, qs@^6.5.1, qs@~6.5.1, qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" @@ -25492,25 +25109,15 @@ randomfill@^1.0.3: randombytes "^2.0.5" safe-buffer "^5.1.0" -range-parser@^1.2.0, range-parser@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" - integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4= - range-parser@^1.2.1, range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" - integrity sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k= - dependencies: - bytes "3.0.0" - http-errors "1.6.2" - iconv-lite "0.4.19" - unpipe "1.0.0" +range-parser@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4= raw-body@2.3.3: version "2.3.3" @@ -27396,7 +27003,7 @@ requirejs@^2.3.5: resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.6.tgz#e5093d9601c2829251258c0b9445d4d19fa9e7c9" integrity sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg== -requires-port@1.x.x, requires-port@^1.0.0: +requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= @@ -27515,7 +27122,7 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -resolve@1.1.7, resolve@1.1.x, resolve@~1.1.0, resolve@~1.1.7: +resolve@1.1.7, resolve@~1.1.0, resolve@~1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= @@ -27633,11 +27240,6 @@ rework@1.0.1: convert-source-map "^0.3.3" css "^2.0.0" -rfdc@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2" - integrity sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug== - right-align@^0.1.1: version "0.1.3" resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" @@ -27645,7 +27247,7 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@2, rimraf@^2.2.0, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2: +rimraf@2, rimraf@^2.2.0, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.6.1, rimraf@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" integrity sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w== @@ -28716,52 +28318,6 @@ socket-location@^1.0.0: dependencies: await-event "^2.1.0" -socket.io-adapter@~1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz#2a805e8a14d6372124dd9159ad4502f8cb07f06b" - integrity sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs= - -socket.io-client@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.1.1.tgz#dcb38103436ab4578ddb026638ae2f21b623671f" - integrity sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ== - dependencies: - backo2 "1.0.2" - base64-arraybuffer "0.1.5" - component-bind "1.0.0" - component-emitter "1.2.1" - debug "~3.1.0" - engine.io-client "~3.2.0" - has-binary2 "~1.0.2" - has-cors "1.1.0" - indexof "0.0.1" - object-component "0.0.3" - parseqs "0.0.5" - parseuri "0.0.5" - socket.io-parser "~3.2.0" - to-array "0.1.4" - -socket.io-parser@~3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.2.0.tgz#e7c6228b6aa1f814e6148aea325b51aa9499e077" - integrity sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA== - dependencies: - component-emitter "1.2.1" - debug "~3.1.0" - isarray "2.0.1" - -socket.io@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.1.1.tgz#a069c5feabee3e6b214a75b40ce0652e1cfb9980" - integrity sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA== - dependencies: - debug "~3.1.0" - engine.io "~3.2.0" - has-binary2 "~1.0.2" - socket.io-adapter "~1.1.0" - socket.io-client "2.1.1" - socket.io-parser "~3.2.0" - sockjs-client@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.3.0.tgz#12fc9d6cb663da5739d3dc5fb6e8687da95cb177" @@ -28893,13 +28449,6 @@ source-map@~0.1.30: dependencies: amdefine ">=0.0.4" -source-map@~0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d" - integrity sha1-2rc/vPwrqBm03gO9b26qSBZLP50= - dependencies: - amdefine ">=0.0.4" - sourcemap-codec@^1.4.1: version "1.4.6" resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz#e30a74f0402bad09807640d39e971090a08ce1e9" @@ -29242,11 +28791,6 @@ stats-lite@^2.2.0: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= -statuses@~1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" - integrity sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4= - stdout-stream@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.0.tgz#a2c7c8587e54d9427ea9edb3ac3f2cd522df378b" @@ -29320,17 +28864,6 @@ stream-spigot@~2.1.2: dependencies: readable-stream "~1.1.0" -streamroller@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-1.0.6.tgz#8167d8496ed9f19f05ee4b158d9611321b8cacd9" - integrity sha512-3QC47Mhv3/aZNFpDDVO44qQb9gwB9QggMEE0sQmkTAwBVYdBRWISdsywlkfm5II1Q5y/pmrHflti/IgmIzdDBg== - dependencies: - async "^2.6.2" - date-format "^2.0.0" - debug "^3.2.6" - fs-extra "^7.0.1" - lodash "^4.17.14" - strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" @@ -29834,7 +29367,7 @@ supports-color@^2.0.0: resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= -supports-color@^3.1.0, supports-color@^3.1.2: +supports-color@^3.1.2: version "3.2.3" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY= @@ -30531,7 +30064,7 @@ tmp@0.0.30: dependencies: os-tmpdir "~1.0.1" -tmp@0.0.33, tmp@0.0.x, tmp@^0.0.33: +tmp@0.0.x, tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== @@ -30565,11 +30098,6 @@ to-absolute-glob@^2.0.0: is-absolute "^1.0.0" is-negated-glob "^1.0.0" -to-array@0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" - integrity sha1-F+bBH3PdTz10zaek/zI46a2b+JA= - to-arraybuffer@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" @@ -31463,7 +30991,7 @@ type-fest@^0.8.0, type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-is@~1.6.15, type-is@~1.6.16: +type-is@~1.6.16: version "1.6.16" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" integrity sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q== @@ -31544,7 +31072,7 @@ typings-tester@^0.3.2: dependencies: commander "^2.12.2" -ua-parser-js@0.7.21, ua-parser-js@^0.7.18, ua-parser-js@^0.7.9: +ua-parser-js@^0.7.18, ua-parser-js@^0.7.9: version "0.7.21" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777" integrity sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ== @@ -31594,11 +31122,6 @@ uid-safe@2.1.5: dependencies: random-bytes "~1.0.0" -ultron@~1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" - integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og== - unbzip2-stream@^1.0.9: version "1.2.5" resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.2.5.tgz#73a033a567bbbde59654b193c44d48a7e4f43c47" @@ -32825,7 +32348,7 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.0.tgz#bd76d6a23323e2ca8ffa12028dc04559c75f9019" integrity sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw== -void-elements@^2.0.0, void-elements@^2.0.1: +void-elements@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w= @@ -33250,7 +32773,7 @@ which@1, which@1.3.1, which@^1.2.9, which@^1.3.1, which@~1.3.0: dependencies: isexe "^2.0.0" -which@^1.1.1, which@^1.2.1, which@^1.2.14, which@^1.2.8: +which@^1.2.14, which@^1.2.8: version "1.3.0" resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" integrity sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg== @@ -33590,15 +33113,6 @@ ws@^7.0.0: dependencies: async-limiter "^1.0.0" -ws@~3.3.1: - version "3.3.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2" - integrity sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA== - dependencies: - async-limiter "~1.0.0" - safe-buffer "~5.1.0" - ultron "~1.1.0" - x-is-function@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/x-is-function/-/x-is-function-1.0.4.tgz#5d294dc3d268cbdd062580e0c5df77a391d1fa1e" @@ -33681,11 +33195,6 @@ xmlbuilder@13.0.2: resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-13.0.2.tgz#02ae33614b6a047d1c32b5389c1fdacb2bce47a7" integrity sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ== -xmlbuilder@8.2.2: - version "8.2.2" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-8.2.2.tgz#69248673410b4ba42e1a6136551d2922335aa773" - integrity sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M= - xmlbuilder@~11.0.0: version "11.0.1" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" @@ -33706,11 +33215,6 @@ xmldom@0.1.27: resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" integrity sha1-1QH5ezvbQDr4757MIFcxh6rawOk= -xmlhttprequest-ssl@~1.5.4: - version "1.5.5" - resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e" - integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4= - xpath@0.0.27: version "0.0.27" resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.27.tgz#dd3421fbdcc5646ac32c48531b4d7e9d0c2cfa92" @@ -34010,11 +33514,6 @@ yazl@^2.5.1: dependencies: buffer-crc32 "~0.2.3" -yeast@0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" - integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk= - yeoman-character@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/yeoman-character/-/yeoman-character-1.1.0.tgz#90d4b5beaf92759086177015b2fdfa2e0684d7c7" From 867a672c7a0be1b1c447c9ceaf84ec4883061b10 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Mon, 27 Jul 2020 14:13:50 -0400 Subject: [PATCH 48/96] [Security Solution] Use docker for endpoint tests (#73092) * Copying api integration tests into their own directory * Removing api integration tests and using ingest docker image * Fixing typo * Fixing type errors and empty string and reenabling tests * Rebuilding docs * Renaming url override variable Co-authored-by: Elastic Machine --- .../architecture/code-exploration.asciidoc | 4 +- x-pack/plugins/security_solution/README.md | 130 ++++++++++++++++++ x-pack/scripts/functional_tests.js | 1 + .../api_integration/apis/endpoint/index.ts | 22 --- x-pack/test/api_integration/apis/index.js | 1 - x-pack/test/api_integration/services/index.ts | 2 - .../ingest_manager_api_integration/config.ts | 9 +- .../apps/endpoint/endpoint_list.ts | 2 +- .../apps/endpoint/index.ts | 18 ++- .../test/security_solution_endpoint/config.ts | 7 + .../apis}/artifacts/index.ts | 8 +- .../apis}/data_stream_helper.ts | 2 +- .../apis/fixtures/package_registry_config.yml | 2 + .../apis/index.ts | 34 +++++ .../apis}/metadata.ts | 2 +- .../apis}/policy.ts | 2 +- .../apis}/resolver.ts | 10 +- .../config.ts | 31 +++++ .../ftr_provider_context.d.ts | 11 ++ .../registry.ts | 77 +++++++++++ .../services/index.ts | 13 ++ .../services/resolver.ts | 0 22 files changed, 341 insertions(+), 47 deletions(-) create mode 100644 x-pack/plugins/security_solution/README.md delete mode 100644 x-pack/test/api_integration/apis/endpoint/index.ts rename x-pack/test/{api_integration/apis/endpoint => security_solution_endpoint_api_int/apis}/artifacts/index.ts (98%) rename x-pack/test/{api_integration/apis/endpoint => security_solution_endpoint_api_int/apis}/data_stream_helper.ts (94%) create mode 100644 x-pack/test/security_solution_endpoint_api_int/apis/fixtures/package_registry_config.yml create mode 100644 x-pack/test/security_solution_endpoint_api_int/apis/index.ts rename x-pack/test/{api_integration/apis/endpoint => security_solution_endpoint_api_int/apis}/metadata.ts (99%) rename x-pack/test/{api_integration/apis/endpoint => security_solution_endpoint_api_int/apis}/policy.ts (96%) rename x-pack/test/{api_integration/apis/endpoint => security_solution_endpoint_api_int/apis}/resolver.ts (98%) create mode 100644 x-pack/test/security_solution_endpoint_api_int/config.ts create mode 100644 x-pack/test/security_solution_endpoint_api_int/ftr_provider_context.d.ts create mode 100644 x-pack/test/security_solution_endpoint_api_int/registry.ts create mode 100644 x-pack/test/security_solution_endpoint_api_int/services/index.ts rename x-pack/test/{api_integration => security_solution_endpoint_api_int}/services/resolver.ts (100%) diff --git a/docs/developer/architecture/code-exploration.asciidoc b/docs/developer/architecture/code-exploration.asciidoc index f18a6c2f14926..4481dea44795c 100644 --- a/docs/developer/architecture/code-exploration.asciidoc +++ b/docs/developer/architecture/code-exploration.asciidoc @@ -524,9 +524,9 @@ WARNING: Missing README. See Configuring security in Kibana. -- {kib-repo}blob/{branch}/x-pack/plugins/security_solution[securitySolution] +- {kib-repo}blob/{branch}/x-pack/plugins/security_solution/README.md[securitySolution] -WARNING: Missing README. +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] diff --git a/x-pack/plugins/security_solution/README.md b/x-pack/plugins/security_solution/README.md new file mode 100644 index 0000000000000..6680dbf1a149b --- /dev/null +++ b/x-pack/plugins/security_solution/README.md @@ -0,0 +1,130 @@ +# Security Solution + +Welcome to the Kibana Security Solution plugin! This README will go over getting started with development and testing. + +## Development + +## Tests + +The endpoint specific tests leverage the ingest manager to install the endpoint package. Before the api integration +and functional tests are run the ingest manager is initialized. This initialization process includes reaching out to +a package registry service to install the endpoint package. The endpoint tests support three different ways to run +the tests given the constraint on an available package registry. + +1. Using Docker +2. Running your own local package registry +3. Using the default external package registry + +These scenarios will be outlined the sections below. + +### Endpoint API Integration Tests Location + +The endpoint api integration tests are located [here](../../test/security_solution_endpoint_api_int) + +### Endpoint Functional Tests Location + +The endpoint functional tests are located [here](../../test/security_solution_endpoint) + +### Using Docker + +To run the tests using the recommended docker image version you must have `docker` installed. The testing infrastructure +will stand up a docker container using the image defined [here](../../test/ingest_manager_api_integration/config.ts#L15) + +Make sure you're in the Kibana root directory. + +#### Endpoint API Integration Tests + +In one terminal, run: + +```bash +INGEST_MANAGEMENT_PACKAGE_REGISTRY_PORT=12345 yarn test:ftr:server --config x-pack/test/security_solution_endpoint_api_int/config.ts +``` + +In another terminal, run: + +```bash +INGEST_MANAGEMENT_PACKAGE_REGISTRY_PORT=12345 yarn test:ftr:runner --config x-pack/test/security_solution_endpoint_api_int/config.ts +``` + +#### Endpoint Functional Tests + +In one terminal, run: + +```bash +INGEST_MANAGEMENT_PACKAGE_REGISTRY_PORT=12345 yarn test:ftr:server --config x-pack/test/security_solution_endpoint/config.ts +``` + +In another terminal, run: + +```bash +INGEST_MANAGEMENT_PACKAGE_REGISTRY_PORT=12345 yarn test:ftr:runner --config x-pack/test/security_solution_endpoint/config.ts +``` + +### Running your own package registry + +If you are doing endpoint package development it will be useful to run your own package registry to serve the latest package you're building. +To do this use the following commands: + +Make sure you're in the Kibana root directory. + +#### Endpoint API Integration Tests + +In one terminal, run: + +```bash +PACKAGE_REGISTRY_URL_OVERRIDE= yarn test:ftr:server --config x-pack/test/security_solution_endpoint_api_int/config.ts +``` + +In another terminal, run: + +```bash +PACKAGE_REGISTRY_URL_OVERRIDE= yarn test:ftr:runner --config x-pack/test/security_solution_endpoint_api_int/config.ts +``` + +#### Endpoint Functional Tests + +In one terminal, run: + +```bash +PACKAGE_REGISTRY_URL_OVERRIDE= yarn test:ftr:server --config x-pack/test/security_solution_endpoint/config.ts +``` + +In another terminal, run: + +```bash +PACKAGE_REGISTRY_URL_OVERRIDE= yarn test:ftr:runner --config x-pack/test/security_solution_endpoint/config.ts +``` + +### Using the default public registry + +If you don't have docker installed and don't want to run your own registry, you can run the tests using the ingest manager's default public package registry. The actual package registry used is [here](../../plugins/ingest_manager/common/constants/epm.ts#L9) + +Make sure you're in the Kibana root directory. + +#### Endpoint API Integration Tests + +In one terminal, run: + +```bash +yarn test:ftr:server --config x-pack/test/security_solution_endpoint_api_int/config.ts +``` + +In another terminal, run: + +```bash +yarn test:ftr:runner --config x-pack/test/security_solution_endpoint_api_int/config.ts +``` + +#### Endpoint Functional Tests + +In one terminal, run: + +```bash +yarn test:ftr:server --config x-pack/test/security_solution_endpoint/config.ts +``` + +In another terminal, run: + +```bash +yarn test:ftr:runner --config x-pack/test/security_solution_endpoint/config.ts +``` diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index c568b92e85515..eeff81d492d26 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -53,6 +53,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/licensing_plugin/config.legacy.ts'), require.resolve('../test/endpoint_api_integration_no_ingest/config.ts'), require.resolve('../test/reporting_api_integration/config.js'), + require.resolve('../test/security_solution_endpoint_api_int/config.ts'), require.resolve('../test/ingest_manager_api_integration/config.ts'), ]; diff --git a/x-pack/test/api_integration/apis/endpoint/index.ts b/x-pack/test/api_integration/apis/endpoint/index.ts deleted file mode 100644 index 5ada2bd094d4b..0000000000000 --- a/x-pack/test/api_integration/apis/endpoint/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function endpointAPIIntegrationTests({ - loadTestFile, - getService, -}: FtrProviderContext) { - describe('Endpoint plugin', function () { - const ingestManager = getService('ingestManager'); - this.tags(['endpoint']); - before(async () => { - await ingestManager.setup(); - }); - loadTestFile(require.resolve('./resolver')); - loadTestFile(require.resolve('./metadata')); - loadTestFile(require.resolve('./policy')); - }); -} diff --git a/x-pack/test/api_integration/apis/index.js b/x-pack/test/api_integration/apis/index.js index 05b305ccd833f..23532d1311754 100644 --- a/x-pack/test/api_integration/apis/index.js +++ b/x-pack/test/api_integration/apis/index.js @@ -28,7 +28,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./lens')); loadTestFile(require.resolve('./ml')); loadTestFile(require.resolve('./transform')); - loadTestFile(require.resolve('./endpoint')); loadTestFile(require.resolve('./lists')); loadTestFile(require.resolve('./upgrade_assistant')); }); diff --git a/x-pack/test/api_integration/services/index.ts b/x-pack/test/api_integration/services/index.ts index 75cc2b451ea2e..7113e117582dd 100644 --- a/x-pack/test/api_integration/services/index.ts +++ b/x-pack/test/api_integration/services/index.ts @@ -27,7 +27,6 @@ import { InfraOpsSourceConfigurationProvider } from './infraops_source_configura import { InfraLogSourceConfigurationProvider } from './infra_log_source_configuration'; import { MachineLearningProvider } from './ml'; import { IngestManagerProvider } from '../../common/services/ingest_manager'; -import { ResolverGeneratorProvider } from './resolver'; import { TransformProvider } from './transform'; export const services = { @@ -48,6 +47,5 @@ export const services = { usageAPI: UsageAPIProvider, ml: MachineLearningProvider, ingestManager: IngestManagerProvider, - resolverGenerator: ResolverGeneratorProvider, transform: TransformProvider, }; diff --git a/x-pack/test/ingest_manager_api_integration/config.ts b/x-pack/test/ingest_manager_api_integration/config.ts index 2aa2e62a4b9e1..ddb49a09a7afa 100644 --- a/x-pack/test/ingest_manager_api_integration/config.ts +++ b/x-pack/test/ingest_manager_api_integration/config.ts @@ -9,6 +9,11 @@ import path from 'path'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { defineDockerServersConfig } from '@kbn/test'; +// Docker image to use for Ingest Manager API integration tests. +// This hash comes from the commit hash here: https://github.com/elastic/package-storage/commit/48f3935a72b0c5aacc6fec8ef36d559b089a238b +export const dockerImage = + 'docker.elastic.co/package-registry/distribution:48f3935a72b0c5aacc6fec8ef36d559b089a238b'; + export default async function ({ readConfigFile }: FtrConfigProviderContext) { const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); @@ -29,10 +34,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { )}:/packages/test-packages`, ]; - // Docker image to use for Ingest Manager API integration tests. - const dockerImage = - 'docker.elastic.co/package-registry/distribution:184b85f19e8fd14363e36150173d338ff9659f01'; - return { testFiles: [require.resolve('./apis')], servers: xPackAPITestsConfig.get('servers'), 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 6971d9f523e7e..07667a140d090 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 @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { deleteMetadataStream } from '../../../api_integration/apis/endpoint/data_stream_helper'; +import { deleteMetadataStream } from '../../../security_solution_endpoint_api_int/apis/data_stream_helper'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'endpoint', 'header', 'endpointPageUtils']); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts index bd933c9a136f2..eec3da4ce1c5e 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts @@ -3,12 +3,28 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { DEFAULT_REGISTRY_URL } from '../../../../plugins/ingest_manager/common'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { + isRegistryEnabled, + getRegistryUrl, +} from '../../../security_solution_endpoint_api_int/registry'; + +export default function (providerContext: FtrProviderContext) { + const { loadTestFile, getService } = providerContext; -export default function ({ loadTestFile, getService }: FtrProviderContext) { describe('endpoint', function () { this.tags('ciGroup7'); const ingestManager = getService('ingestManager'); + const log = getService('log'); + + if (!isRegistryEnabled()) { + log.warning('These tests are being run with an external package registry'); + } + + const registryUrl = getRegistryUrl() ?? DEFAULT_REGISTRY_URL; + log.info(`Package registry URL for tests: ${registryUrl}`); + before(async () => { await ingestManager.setup(); }); diff --git a/x-pack/test/security_solution_endpoint/config.ts b/x-pack/test/security_solution_endpoint/config.ts index 2d94163fa1018..5aa5e42ffd4ee 100644 --- a/x-pack/test/security_solution_endpoint/config.ts +++ b/x-pack/test/security_solution_endpoint/config.ts @@ -8,6 +8,10 @@ import { resolve } from 'path'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { pageObjects } from './page_objects'; import { services } from './services'; +import { + getRegistryUrlAsArray, + createEndpointDockerConfig, +} from '../security_solution_endpoint_api_int/registry'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); @@ -16,6 +20,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...xpackFunctionalConfig.getAll(), pageObjects, testFiles: [resolve(__dirname, './apps/endpoint')], + dockerServers: createEndpointDockerConfig(), junit: { reportName: 'X-Pack Endpoint Functional Tests', }, @@ -31,6 +36,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { serverArgs: [ ...xpackFunctionalConfig.get('kbnTestServer.serverArgs'), '--xpack.ingestManager.enabled=true', + // if you return an empty string here the kibana server will not start properly but an empty array works + ...getRegistryUrlAsArray(), ], }, }; diff --git a/x-pack/test/api_integration/apis/endpoint/artifacts/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts similarity index 98% rename from x-pack/test/api_integration/apis/endpoint/artifacts/index.ts rename to x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts index b37522ed52b5c..a4a8de418157f 100644 --- a/x-pack/test/api_integration/apis/endpoint/artifacts/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts @@ -8,11 +8,8 @@ import expect from '@kbn/expect'; import { createHash } from 'crypto'; import { inflateSync } from 'zlib'; -import { FtrProviderContext } from '../../../ftr_provider_context'; -import { - getSupertestWithoutAuth, - setupIngest, -} from '../../../../ingest_manager_api_integration/apis/fleet/agents/services'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { getSupertestWithoutAuth } from '../../../ingest_manager_api_integration/apis/fleet/agents/services'; export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; @@ -22,7 +19,6 @@ export default function (providerContext: FtrProviderContext) { let agentAccessAPIKey: string; describe('artifact download', () => { - setupIngest(providerContext); before(async () => { await esArchiver.load('endpoint/artifacts/api_feature', { useCreate: true }); diff --git a/x-pack/test/api_integration/apis/endpoint/data_stream_helper.ts b/x-pack/test/security_solution_endpoint_api_int/apis/data_stream_helper.ts similarity index 94% rename from x-pack/test/api_integration/apis/endpoint/data_stream_helper.ts rename to x-pack/test/security_solution_endpoint_api_int/apis/data_stream_helper.ts index b239ab41e41f1..b16da16b3137f 100644 --- a/x-pack/test/api_integration/apis/endpoint/data_stream_helper.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/data_stream_helper.ts @@ -10,7 +10,7 @@ import { eventsIndexPattern, alertsIndexPattern, policyIndexPattern, -} from '../../../../plugins/security_solution/common/endpoint/constants'; +} from '../../../plugins/security_solution/common/endpoint/constants'; export async function deleteDataStream(getService: (serviceName: 'es') => Client, index: string) { const client = getService('es'); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/fixtures/package_registry_config.yml b/x-pack/test/security_solution_endpoint_api_int/apis/fixtures/package_registry_config.yml new file mode 100644 index 0000000000000..4d93386b4d4e1 --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/apis/fixtures/package_registry_config.yml @@ -0,0 +1,2 @@ +package_paths: + - /packages/production diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts new file mode 100644 index 0000000000000..fb11a7c52fd35 --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts @@ -0,0 +1,34 @@ +/* + * 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 { FtrProviderContext } from '../ftr_provider_context'; +import { isRegistryEnabled, getRegistryUrl } from '../registry'; +import { DEFAULT_REGISTRY_URL } from '../../../plugins/ingest_manager/common'; + +export default function endpointAPIIntegrationTests(providerContext: FtrProviderContext) { + const { loadTestFile, getService } = providerContext; + + describe('Endpoint plugin', function () { + const ingestManager = getService('ingestManager'); + + this.tags('ciGroup7'); + const log = getService('log'); + + if (!isRegistryEnabled()) { + log.warning('These tests are being run with an external package registry'); + } + + const registryUrl = getRegistryUrl() ?? DEFAULT_REGISTRY_URL; + log.info(`Package registry URL for tests: ${registryUrl}`); + + before(async () => { + await ingestManager.setup(); + }); + loadTestFile(require.resolve('./resolver')); + loadTestFile(require.resolve('./metadata')); + loadTestFile(require.resolve('./policy')); + loadTestFile(require.resolve('./artifacts')); + }); +} diff --git a/x-pack/test/api_integration/apis/endpoint/metadata.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts similarity index 99% rename from x-pack/test/api_integration/apis/endpoint/metadata.ts rename to x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts index 41531269ddeb9..719327e5f9b79 100644 --- a/x-pack/test/api_integration/apis/endpoint/metadata.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect/expect.js'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../ftr_provider_context'; import { deleteMetadataStream } from './data_stream_helper'; /** diff --git a/x-pack/test/api_integration/apis/endpoint/policy.ts b/x-pack/test/security_solution_endpoint_api_int/apis/policy.ts similarity index 96% rename from x-pack/test/api_integration/apis/endpoint/policy.ts rename to x-pack/test/security_solution_endpoint_api_int/apis/policy.ts index e33423d172567..66bcc0e759916 100644 --- a/x-pack/test/api_integration/apis/endpoint/policy.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/policy.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect/expect.js'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../ftr_provider_context'; import { deletePolicyStream } from './data_stream_helper'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/endpoint/resolver.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver.ts similarity index 98% rename from x-pack/test/api_integration/apis/endpoint/resolver.ts rename to x-pack/test/security_solution_endpoint_api_int/apis/resolver.ts index fa980aed30502..3b515f86c6761 100644 --- a/x-pack/test/api_integration/apis/endpoint/resolver.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver.ts @@ -16,12 +16,12 @@ import { LegacyEndpointEvent, ResolverNodeStats, ResolverRelatedAlerts, -} from '../../../../plugins/security_solution/common/endpoint/types'; +} from '../../../plugins/security_solution/common/endpoint/types'; import { parentEntityId, eventId, -} from '../../../../plugins/security_solution/common/endpoint/models/event'; -import { FtrProviderContext } from '../../ftr_provider_context'; +} from '../../../plugins/security_solution/common/endpoint/models/event'; +import { FtrProviderContext } from '../ftr_provider_context'; import { Event, Tree, @@ -29,8 +29,8 @@ import { RelatedEventCategory, RelatedEventInfo, categoryMapping, -} from '../../../../plugins/security_solution/common/endpoint/generate_data'; -import { Options, GeneratedTrees } from '../../services/resolver'; +} from '../../../plugins/security_solution/common/endpoint/generate_data'; +import { Options, GeneratedTrees } from '../services/resolver'; /** * Check that the given lifecycle is in the resolver tree's corresponding map diff --git a/x-pack/test/security_solution_endpoint_api_int/config.ts b/x-pack/test/security_solution_endpoint_api_int/config.ts new file mode 100644 index 0000000000000..726059a8d73fe --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/config.ts @@ -0,0 +1,31 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { createEndpointDockerConfig, getRegistryUrlAsArray } from './registry'; +import { services } from './services'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); + + return { + ...xPackAPITestsConfig.getAll(), + testFiles: [require.resolve('./apis')], + dockerServers: createEndpointDockerConfig(), + services, + junit: { + reportName: 'X-Pack Endpoint API Integration Tests', + }, + kbnTestServer: { + ...xPackAPITestsConfig.get('kbnTestServer'), + serverArgs: [ + ...xPackAPITestsConfig.get('kbnTestServer.serverArgs'), + // if you return an empty string here the kibana server will not start properly but an empty array works + ...getRegistryUrlAsArray(), + ], + }, + }; +} diff --git a/x-pack/test/security_solution_endpoint_api_int/ftr_provider_context.d.ts b/x-pack/test/security_solution_endpoint_api_int/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..e3add3748f56d --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/ftr_provider_context.d.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 { GenericFtrProviderContext } from '@kbn/test/types/ftr'; + +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/security_solution_endpoint_api_int/registry.ts b/x-pack/test/security_solution_endpoint_api_int/registry.ts new file mode 100644 index 0000000000000..cc474cbf29aaf --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/registry.ts @@ -0,0 +1,77 @@ +/* + * 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 path from 'path'; + +import { defineDockerServersConfig } from '@kbn/test'; +import { dockerImage as ingestDockerImage } from '../ingest_manager_api_integration/config'; + +/** + * This is used by CI to set the docker registry port + * you can also define this environment variable locally when running tests which + * will spin up a local docker package registry locally for you + * if this is defined it takes precedence over the `packageRegistryOverride` variable + */ +const dockerRegistryPort: string | undefined = process.env.INGEST_MANAGEMENT_PACKAGE_REGISTRY_PORT; + +/** + * If you don't want to use the docker image version pinned below and instead want to run your own + * registry or use an external registry you can define this environment variable when running + * the tests to use that registry url instead. + * + * This is particularly useful when a developer needs to test a new package against the kibana + * integration or functional tests. Instead of having to publish a whole new docker image we + * can set this environment variable which will point to the location of where your package registry + * is serving the updated package. + * + * This variable will not and should not be used by CI. CI should always use the pinned docker image below. + */ +const packageRegistryOverride: string | undefined = process.env.PACKAGE_REGISTRY_URL_OVERRIDE; + +const defaultRegistryConfigPath = path.join( + __dirname, + './apis/fixtures/package_registry_config.yml' +); + +export function createEndpointDockerConfig( + packageRegistryConfig: string = defaultRegistryConfigPath, + dockerImage: string = ingestDockerImage, + dockerArgs: string[] = [] +) { + const args: string[] = [ + '-v', + `${packageRegistryConfig}:/package-registry/config.yml`, + ...dockerArgs, + ]; + return defineDockerServersConfig({ + registry: { + enabled: !!dockerRegistryPort, + image: dockerImage, + portInContainer: 8080, + port: dockerRegistryPort, + args, + waitForLogLine: 'package manifests loaded', + }, + }); +} + +export function getRegistryUrl(): string | undefined { + let registryUrl: string | undefined; + if (dockerRegistryPort !== undefined) { + registryUrl = `--xpack.ingestManager.registryUrl=http://localhost:${dockerRegistryPort}`; + } else if (packageRegistryOverride !== undefined) { + registryUrl = `--xpack.ingestManager.registryUrl=${packageRegistryOverride}`; + } + return registryUrl; +} + +export function getRegistryUrlAsArray(): string[] { + const registryUrl: string | undefined = getRegistryUrl(); + return registryUrl !== undefined ? [registryUrl] : []; +} + +export function isRegistryEnabled() { + return getRegistryUrl() !== undefined; +} diff --git a/x-pack/test/security_solution_endpoint_api_int/services/index.ts b/x-pack/test/security_solution_endpoint_api_int/services/index.ts new file mode 100644 index 0000000000000..47d45557022d3 --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/services/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { services as xPackAPIServices } from '../../api_integration/services'; +import { ResolverGeneratorProvider } from './resolver'; + +export const services = { + ...xPackAPIServices, + resolverGenerator: ResolverGeneratorProvider, +}; diff --git a/x-pack/test/api_integration/services/resolver.ts b/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts similarity index 100% rename from x-pack/test/api_integration/services/resolver.ts rename to x-pack/test/security_solution_endpoint_api_int/services/resolver.ts From 34fd7b301d3e1b3eec0eac6b2ce33d02e93166b9 Mon Sep 17 00:00:00 2001 From: Marshall Main <55718608+marshallmain@users.noreply.github.com> Date: Mon, 27 Jul 2020 14:17:19 -0400 Subject: [PATCH 49/96] Increase limit on exception items to 10k (#73117) Co-authored-by: Elastic Machine --- .../server/lib/detection_engine/signals/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 1c59a4b7ea5d0..90373ee676121 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -183,7 +183,7 @@ export const getExceptions = async ({ listId: foundList.list_id, namespaceType, page: 1, - perPage: 5000, + perPage: 10000, filter: undefined, sortOrder: undefined, sortField: undefined, From 7c24a61c435099acfe751b0379a8092df4d5b5e3 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 27 Jul 2020 15:19:42 -0400 Subject: [PATCH 50/96] Make ingest node pipelines api tests more robust (#73289) --- .../ingest_pipelines/ingest_pipelines.ts | 90 +++++++++++++------ .../ingest_pipelines/lib/elasticsearch.ts | 21 ++++- 2 files changed, 82 insertions(+), 29 deletions(-) diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts index 6a827298521dd..b3fab42a46114 100644 --- a/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts @@ -14,16 +14,26 @@ const API_BASE_PATH = '/api/ingest_pipelines'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const { createPipeline, deletePipeline } = registerEsHelpers(getService); + const { createPipeline, deletePipeline, cleanupPipelines } = registerEsHelpers(getService); + + describe('Pipelines', function () { + after(async () => { + await cleanupPipelines(); + }); - describe.skip('Pipelines', function () { describe('Create', () => { const PIPELINE_ID = 'test_create_pipeline'; const REQUIRED_FIELDS_PIPELINE_ID = 'test_create_required_fields_pipeline'; - after(() => { - deletePipeline(PIPELINE_ID); - deletePipeline(REQUIRED_FIELDS_PIPELINE_ID); + after(async () => { + // Clean up any pipelines created in test cases + await Promise.all([PIPELINE_ID, REQUIRED_FIELDS_PIPELINE_ID].map(deletePipeline)).catch( + (err) => { + // eslint-disable-next-line no-console + console.log(`[Cleanup error] Error deleting pipelines: ${err.message}`); + throw err; + } + ); }); it('should create a pipeline', async () => { @@ -127,8 +137,16 @@ export default function ({ getService }: FtrProviderContext) { ], }; - before(() => createPipeline({ body: PIPELINE, id: PIPELINE_ID })); - after(() => deletePipeline(PIPELINE_ID)); + before(async () => { + // Create pipeline that can be used to test PUT request + try { + await createPipeline({ body: PIPELINE, id: PIPELINE_ID }, true); + } catch (err) { + // eslint-disable-next-line no-console + console.log('[Setup error] Error creating ingest node pipeline'); + throw err; + } + }); it('should allow an existing pipeline to be updated', async () => { const uri = `${API_BASE_PATH}/${PIPELINE_ID}`; @@ -185,7 +203,7 @@ export default function ({ getService }: FtrProviderContext) { }); describe('Get', () => { - const PIPELINE_ID = 'test_pipeline'; + const PIPELINE_ID = 'test_get_pipeline'; const PIPELINE = { description: 'test pipeline description', processors: [ @@ -198,8 +216,16 @@ export default function ({ getService }: FtrProviderContext) { version: 1, }; - before(() => createPipeline({ body: PIPELINE, id: PIPELINE_ID })); - after(() => deletePipeline(PIPELINE_ID)); + before(async () => { + // Create pipeline that can be used to test GET request + try { + await createPipeline({ body: PIPELINE, id: PIPELINE_ID }, true); + } catch (err) { + // eslint-disable-next-line no-console + console.log('[Setup error] Error creating ingest node pipeline'); + throw err; + } + }); describe('all pipelines', () => { it('should return an array of pipelines', async () => { @@ -245,29 +271,40 @@ export default function ({ getService }: FtrProviderContext) { version: 1, }; + const pipelineA = { body: PIPELINE, id: 'test_delete_pipeline_a' }; + const pipelineB = { body: PIPELINE, id: 'test_delete_pipeline_b' }; + const pipelineC = { body: PIPELINE, id: 'test_delete_pipeline_c' }; + const pipelineD = { body: PIPELINE, id: 'test_delete_pipeline_d' }; + + before(async () => { + // Create several pipelines that can be used to test deletion + await Promise.all( + [pipelineA, pipelineB, pipelineC, pipelineD].map((pipeline) => createPipeline(pipeline)) + ).catch((err) => { + // eslint-disable-next-line no-console + console.log(`[Setup error] Error creating pipelines: ${err.message}`); + throw err; + }); + }); + it('should delete a pipeline', async () => { - // Create pipeline to be deleted - const PIPELINE_ID = 'test_delete_pipeline'; - createPipeline({ body: PIPELINE, id: PIPELINE_ID }); + const { id } = pipelineA; - const uri = `${API_BASE_PATH}/${PIPELINE_ID}`; + const uri = `${API_BASE_PATH}/${id}`; const { body } = await supertest.delete(uri).set('kbn-xsrf', 'xxx').expect(200); expect(body).to.eql({ - itemsDeleted: [PIPELINE_ID], + itemsDeleted: [id], errors: [], }); }); it('should delete multiple pipelines', async () => { - // Create pipelines to be deleted - const PIPELINE_ONE_ID = 'test_delete_pipeline_1'; - const PIPELINE_TWO_ID = 'test_delete_pipeline_2'; - createPipeline({ body: PIPELINE, id: PIPELINE_ONE_ID }); - createPipeline({ body: PIPELINE, id: PIPELINE_TWO_ID }); + const { id: pipelineBId } = pipelineB; + const { id: pipelineCId } = pipelineC; - const uri = `${API_BASE_PATH}/${PIPELINE_ONE_ID},${PIPELINE_TWO_ID}`; + const uri = `${API_BASE_PATH}/${pipelineBId},${pipelineCId}`; const { body: { itemsDeleted, errors }, @@ -276,24 +313,21 @@ export default function ({ getService }: FtrProviderContext) { expect(errors).to.eql([]); // The itemsDeleted array order isn't guaranteed, so we assert against each pipeline name instead - [PIPELINE_ONE_ID, PIPELINE_TWO_ID].forEach((pipelineName) => { + [pipelineBId, pipelineCId].forEach((pipelineName) => { expect(itemsDeleted.includes(pipelineName)).to.be(true); }); }); it('should return an error for any pipelines not sucessfully deleted', async () => { const PIPELINE_DOES_NOT_EXIST = 'pipeline_does_not_exist'; + const { id: existingPipelineId } = pipelineD; - // Create pipeline to be deleted - const PIPELINE_ONE_ID = 'test_delete_pipeline_1'; - createPipeline({ body: PIPELINE, id: PIPELINE_ONE_ID }); - - const uri = `${API_BASE_PATH}/${PIPELINE_ONE_ID},${PIPELINE_DOES_NOT_EXIST}`; + const uri = `${API_BASE_PATH}/${existingPipelineId},${PIPELINE_DOES_NOT_EXIST}`; const { body } = await supertest.delete(uri).set('kbn-xsrf', 'xxx').expect(200); expect(body).to.eql({ - itemsDeleted: [PIPELINE_ONE_ID], + itemsDeleted: [existingPipelineId], errors: [ { name: PIPELINE_DOES_NOT_EXIST, diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/elasticsearch.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/elasticsearch.ts index 2f42596a66b54..6de91e1154a85 100644 --- a/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/elasticsearch.ts +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/elasticsearch.ts @@ -26,14 +26,33 @@ interface Pipeline { * @param {ElasticsearchClient} es The Elasticsearch client instance */ export const registerEsHelpers = (getService: FtrProviderContext['getService']) => { + let pipelinesCreated: string[] = []; + const es = getService('legacyEs'); - const createPipeline = (pipeline: Pipeline) => es.ingest.putPipeline(pipeline); + const createPipeline = (pipeline: Pipeline, cachePipeline?: boolean) => { + if (cachePipeline) { + pipelinesCreated.push(pipeline.id); + } + + return es.ingest.putPipeline(pipeline); + }; const deletePipeline = (pipelineId: string) => es.ingest.deletePipeline({ id: pipelineId }); + const cleanupPipelines = () => + Promise.all(pipelinesCreated.map(deletePipeline)) + .then(() => { + pipelinesCreated = []; + }) + .catch((err) => { + // eslint-disable-next-line no-console + console.log(`[Cleanup error] Error deleting ES resources: ${err.message}`); + }); + return { createPipeline, deletePipeline, + cleanupPipelines, }; }; From f23359c099ece3807f68b9b8ab24a72ff005d911 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Mon, 27 Jul 2020 14:20:26 -0500 Subject: [PATCH 51/96] [APM] Fix license management URL (#73294) The URL to license management has changed, so update our links, both in the prompt on the Service Map, and in the app-wide expired license message. Use `useKibanaUrl` in both places and update that hook to make the `hash` argument optional, since many apps don't use a hash now. I looked for a more reliable way to get the URL for that app but couldn't come up with anything. I'd appreciate any suggestions if there's a better method. --- .../apm/public/components/shared/LicensePrompt/index.tsx | 3 +-- .../context/LicenseContext/InvalidLicenseNotification.tsx | 5 ++--- x-pack/plugins/apm/public/hooks/useKibanaUrl.ts | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx b/x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx index 50be268d9ccd0..8409326243614 100644 --- a/x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx @@ -16,8 +16,7 @@ interface Props { export function LicensePrompt({ text, showBetaBadge = false }: Props) { const licensePageUrl = useKibanaUrl( - '/app/kibana', - '/management/stack/license_management/home' + '/app/management/stack/license_management' ); const renderLicenseBody = ( diff --git a/x-pack/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx b/x-pack/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx index 481e89e09685e..1195038a6b753 100644 --- a/x-pack/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx +++ b/x-pack/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx @@ -6,11 +6,10 @@ import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { useApmPluginContext } from '../../hooks/useApmPluginContext'; +import { useKibanaUrl } from '../../hooks/useKibanaUrl'; export function InvalidLicenseNotification() { - const { core } = useApmPluginContext(); - const manageLicenseURL = core.http.basePath.prepend( + const manageLicenseURL = useKibanaUrl( '/app/management/stack/license_management' ); diff --git a/x-pack/plugins/apm/public/hooks/useKibanaUrl.ts b/x-pack/plugins/apm/public/hooks/useKibanaUrl.ts index 186a752f52487..b4a354c231633 100644 --- a/x-pack/plugins/apm/public/hooks/useKibanaUrl.ts +++ b/x-pack/plugins/apm/public/hooks/useKibanaUrl.ts @@ -9,7 +9,7 @@ import { useApmPluginContext } from './useApmPluginContext'; export function useKibanaUrl( /** The path to the plugin */ path: string, - /** The hash path */ hash: string + /** The hash path */ hash?: string ) { const { core } = useApmPluginContext(); return url.format({ From 1c690c68af6ea05248ca04b259fb1f01970a044c Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Mon, 27 Jul 2020 15:39:52 -0400 Subject: [PATCH 52/96] [Uptime] Increase timeout in attempt to fix skipped a11y test (#73078) * Increase timeout in attempt to fix skipped a11y test. * Temporarily only run uptime tests for faster flaky testing. * Uncomment other test suites. * Unskip test and delete comment. Co-authored-by: Elastic Machine --- x-pack/test/accessibility/apps/uptime.ts | 3 +-- x-pack/test/functional/services/uptime/navigation.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/test/accessibility/apps/uptime.ts b/x-pack/test/accessibility/apps/uptime.ts index e6ef1cfe8cfe2..ebd120fa0feea 100644 --- a/x-pack/test/accessibility/apps/uptime.ts +++ b/x-pack/test/accessibility/apps/uptime.ts @@ -17,8 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const es = getService('es'); - // FLAKY: https://github.com/elastic/kibana/issues/72994 - describe.skip('uptime', () => { + describe('uptime', () => { before(async () => { await esArchiver.load('uptime/blank'); await makeChecks(es, A11Y_TEST_MONITOR_ID, 150, 1, 1000, { diff --git a/x-pack/test/functional/services/uptime/navigation.ts b/x-pack/test/functional/services/uptime/navigation.ts index f8e0c0cff41f4..ab511abf130a5 100644 --- a/x-pack/test/functional/services/uptime/navigation.ts +++ b/x-pack/test/functional/services/uptime/navigation.ts @@ -41,7 +41,7 @@ export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProv goToSettings: async () => { await goToUptimeRoot(); await testSubjects.click('settings-page-link', 5000); - await testSubjects.existOrFail('uptimeSettingsPage', { timeout: 2000 }); + await testSubjects.existOrFail('uptimeSettingsPage', { timeout: 10000 }); }, checkIfOnMonitorPage: async (monitorId: string) => { From 2ae470e897976abb939c31708bff41fd0d0dcd07 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Mon, 27 Jul 2020 16:04:10 -0500 Subject: [PATCH 53/96] Add Kea.js support to Enterprise Search plugin (#72160) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Kea packages - kea and kea-waitfor * Add Kea declarations and types Hopefully TypeScript support coming soon from author * Add Kea to entry point * Add logic for overview * Update components to use Kea * Fix a couple of tests that weren’t getting complete coverage * Remove kea-waitfor Turns out we don’t need it * Remove unused declaration * Update x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.ts Co-authored-by: Constance * Update x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.ts Co-authored-by: Constance * Update x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/overview_logic.mock.ts Co-authored-by: Constance * Update x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.test.ts Co-authored-by: Constance * [Opinionated] Remove extra actions defs - they're already being defined in IOverviewActions, so no need to repeat them * DRY out a new reusable/generics IKeaLogic/Listeners interface - Multiple logic files can now do IKeaListeners and not have to declare their own IListenerParams! + bonus IKeaSelectors just for consistency * DRY out Kea reducers definitions to generics interface * [Refactor] Improve KeaReducers generic to actually type-check/check key names - Typescript will now throw an error if you put in a key name that isn't declared in your actions/values interface - default & new states now will be type checked!! :tada: * [Refactor] Update selectors() and listeners() to also check types and keys * [Refactor] Move param defs to bottom of file instead of inline - so that inline definitions mostly focus on type checks, and more boilerplate defs are DRYed out - I played around with 2.1 obj definitions and got terrible results here :( * Update tests and remove selectors per code review * Remove last statsColumns instance * Remove last instance of hideOnboarding Co-authored-by: Constance Co-authored-by: Constance Chen Co-authored-by: Elastic Machine --- x-pack/package.json | 3 +- .../public/applications/kea.d.ts | 13 ++ .../public/applications/shared/types.ts | 56 ++++++ .../components/overview/__mocks__/index.ts | 7 + .../overview/__mocks__/overview_logic.mock.ts | 47 +++++ .../overview/onboarding_steps.test.tsx | 77 ++++---- .../components/overview/onboarding_steps.tsx | 27 +-- .../overview/organization_stats.test.tsx | 8 +- .../overview/organization_stats.tsx | 115 ++++++------ .../components/overview/overview.test.tsx | 45 +++-- .../components/overview/overview.tsx | 97 ++-------- .../overview/overview_logic.test.ts | 141 +++++++++++++++ .../components/overview/overview_logic.ts | 168 ++++++++++++++++++ .../overview/recent_activity.test.tsx | 21 ++- .../components/overview/recent_activity.tsx | 13 +- .../content_section/content_section.test.tsx | 3 +- .../view_content_header.test.tsx | 3 +- .../applications/workplace_search/index.tsx | 11 +- .../applications/workplace_search/types.ts | 11 ++ yarn.lock | 5 + 20 files changed, 634 insertions(+), 237 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/kea.d.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/overview_logic.mock.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.ts diff --git a/x-pack/package.json b/x-pack/package.json index d1f638ccad8d0..dee99d6f0ddac 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -282,6 +282,7 @@ "json-stable-stringify": "^1.0.1", "jsonwebtoken": "^8.5.1", "jsts": "^1.6.2", + "kea": "^2.0.1", "lodash": "^4.17.15", "lz-string": "^1.4.4", "mapbox-gl": "^1.10.0", @@ -384,4 +385,4 @@ "cypress-multi-reporters" ] } -} \ No newline at end of file +} diff --git a/x-pack/plugins/enterprise_search/public/applications/kea.d.ts b/x-pack/plugins/enterprise_search/public/applications/kea.d.ts new file mode 100644 index 0000000000000..961d93ccc12e6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/kea.d.ts @@ -0,0 +1,13 @@ +/* + * 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. + */ + +declare module 'kea' { + export function useValues(logic?: object): object; + export function useActions(logic?: object): object; + export function getContext(): { store: object }; + export function resetContext(context: object): object; + export function kea(logic: object): object; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts index 3f28710d92295..74bb53ef3a954 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -12,3 +12,59 @@ export interface IFlashMessagesProps { isWrapped?: boolean; children?: React.ReactNode; } + +export interface IKeaLogic { + mount(): void; + values: IKeaValues; + actions: IKeaActions; +} + +/** + * This reusable interface mostly saves us a few characters / allows us to skip + * defining params inline. Unfortunately, the return values *do not work* as + * expected (hence the voids). While I can tell selectors to use TKeaSelectors, + * the return value is *not* properly type checked if it's not declared inline. :/ + * + * Also note that if you switch to Kea 2.1's plain object notation - + * `selectors: {}` vs. `selectors: () => ({})` + * - type checking also stops working and type errors become significantly less + * helpful - showing less specific error messages and highlighting. 👎 + */ +export interface IKeaParams { + selectors?(params: { selectors: IKeaValues }): void; + listeners?(params: { actions: IKeaActions; values: IKeaValues }): void; +} + +/** + * This reducers() type checks that: + * + * 1. The value object keys are defined within IKeaValues + * 2. The default state (array[0]) matches the type definition within IKeaValues + * 3. The action object keys (array[1]) are defined within IKeaActions + * 3. The new state returned by the action matches the type definition within IKeaValues + */ +export type TKeaReducers = { + [Value in keyof IKeaValues]?: [ + IKeaValues[Value], + { + [Action in keyof IKeaActions]?: (state: IKeaValues, payload: IKeaValues) => IKeaValues[Value]; + } + ]; +}; + +/** + * This selectors() type checks that: + * + * 1. The object keys are defined within IKeaValues + * 2. The selected values are defined within IKeaValues + * 3. The returned value match the type definition within IKeaValues + * + * The unknown[] and any[] are unfortunately because I have no idea how to + * assert for arbitrary type/values as an array + */ +export type TKeaSelectors = { + [Value in keyof IKeaValues]?: [ + (selectors: IKeaValues) => unknown[], + (...args: any[]) => IKeaValues[Value] // eslint-disable-line @typescript-eslint/no-explicit-any + ]; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/index.ts new file mode 100644 index 0000000000000..e5169a51ce522 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { setMockValues, mockLogicValues, mockLogicActions } from './overview_logic.mock'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/overview_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/overview_logic.mock.ts new file mode 100644 index 0000000000000..43cff5de6668d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/overview_logic.mock.ts @@ -0,0 +1,47 @@ +/* + * 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 { IOverviewValues } from '../overview_logic'; +import { IAccount, IOrganization, IUser } from '../../../types'; + +export const mockLogicValues = { + accountsCount: 0, + activityFeed: [], + canCreateContentSources: false, + canCreateInvitations: false, + currentUser: {} as IUser, + fpAccount: {} as IAccount, + hasOrgSources: false, + hasUsers: false, + isFederatedAuth: true, + isOldAccount: false, + organization: {} as IOrganization, + pendingInvitationsCount: 0, + personalSourcesCount: 0, + sourcesCount: 0, + dataLoading: true, + hasErrorConnecting: false, + flashMessages: {}, +} as IOverviewValues; + +export const mockLogicActions = { + initializeOverview: jest.fn(() => ({})), +}; + +jest.mock('kea', () => ({ + ...(jest.requireActual('kea') as object), + useActions: jest.fn(() => ({ ...mockLogicActions })), + useValues: jest.fn(() => ({ ...mockLogicValues })), +})); + +import { useValues } from 'kea'; + +export const setMockValues = (values: object) => { + (useValues as jest.Mock).mockImplementationOnce(() => ({ + ...mockLogicValues, + ...values, + })); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.test.tsx index 6174dc1c795eb..3cf88cf120cc4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.test.tsx @@ -5,6 +5,8 @@ */ import '../../../__mocks__/shallow_usecontext.mock'; +import './__mocks__/overview_logic.mock'; +import { setMockValues } from './__mocks__'; import React from 'react'; import { shallow } from 'enzyme'; @@ -16,7 +18,6 @@ import { sendTelemetry } from '../../../shared/telemetry'; import { OnboardingSteps, OrgNameOnboarding } from './onboarding_steps'; import { OnboardingCard } from './onboarding_card'; -import { defaultServerData } from './overview'; const account = { id: '1', @@ -30,7 +31,8 @@ const account = { describe('OnboardingSteps', () => { describe('Shared Sources', () => { it('renders 0 sources state', () => { - const wrapper = shallow(); + setMockValues({ canCreateContentSources: true }); + const wrapper = shallow(); expect(wrapper.find(OnboardingCard)).toHaveLength(1); expect(wrapper.find(OnboardingCard).prop('actionPath')).toBe(ORG_SOURCES_PATH); @@ -40,9 +42,8 @@ describe('OnboardingSteps', () => { }); it('renders completed sources state', () => { - const wrapper = shallow( - - ); + setMockValues({ sourcesCount: 2, hasOrgSources: true }); + const wrapper = shallow(); expect(wrapper.find(OnboardingCard).prop('description')).toEqual( 'You have added 2 shared sources. Happy searching.' @@ -50,9 +51,8 @@ describe('OnboardingSteps', () => { }); it('disables link when the user cannot create sources', () => { - const wrapper = shallow( - - ); + setMockValues({ canCreateContentSources: false }); + const wrapper = shallow(); expect(wrapper.find(OnboardingCard).prop('actionPath')).toBe(undefined); }); @@ -60,15 +60,14 @@ describe('OnboardingSteps', () => { describe('Users & Invitations', () => { it('renders 0 users when not on federated auth', () => { - const wrapper = shallow( - - ); + setMockValues({ + canCreateInvitations: true, + isFederatedAuth: false, + fpAccount: account, + accountsCount: 0, + hasUsers: false, + }); + const wrapper = shallow(); expect(wrapper.find(OnboardingCard)).toHaveLength(2); expect(wrapper.find(OnboardingCard).last().prop('actionPath')).toBe(USERS_PATH); @@ -78,15 +77,13 @@ describe('OnboardingSteps', () => { }); it('renders completed users state', () => { - const wrapper = shallow( - - ); + setMockValues({ + isFederatedAuth: false, + fpAccount: account, + accountsCount: 1, + hasUsers: true, + }); + const wrapper = shallow(); expect(wrapper.find(OnboardingCard).last().prop('description')).toEqual( 'Nice, you’ve invited colleagues to search with you.' @@ -94,21 +91,15 @@ describe('OnboardingSteps', () => { }); it('disables link when the user cannot create invitations', () => { - const wrapper = shallow( - - ); - + setMockValues({ isFederatedAuth: false, canCreateInvitations: false }); + const wrapper = shallow(); expect(wrapper.find(OnboardingCard).last().prop('actionPath')).toBe(undefined); }); }); describe('Org Name', () => { it('renders button to change name', () => { - const wrapper = shallow(); + const wrapper = shallow(); const button = wrapper .find(OrgNameOnboarding) @@ -120,15 +111,13 @@ describe('OnboardingSteps', () => { }); it('hides card when name has been changed', () => { - const wrapper = shallow( - - ); + setMockValues({ + organization: { + name: 'foo', + defaultOrgName: 'bar', + }, + }); + const wrapper = shallow(); expect(wrapper.find(OrgNameOnboarding)).toHaveLength(0); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.tsx index 1b00347437338..7fe1eae502329 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.tsx @@ -7,6 +7,7 @@ import React, { useContext } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { useValues } from 'kea'; import { EuiSpacer, @@ -28,7 +29,7 @@ import { ORG_SOURCES_PATH, USERS_PATH, ORG_SETTINGS_PATH } from '../../routes'; import { ContentSection } from '../shared/content_section'; -import { IAppServerData } from './overview'; +import { OverviewLogic, IOverviewValues } from './overview_logic'; import { OnboardingCard } from './onboarding_card'; @@ -57,17 +58,19 @@ const ONBOARDING_USERS_CARD_DESCRIPTION = i18n.translate( { defaultMessage: 'Invite your colleagues into this organization to search with you.' } ); -export const OnboardingSteps: React.FC = ({ - hasUsers, - hasOrgSources, - canCreateContentSources, - canCreateInvitations, - accountsCount, - sourcesCount, - fpAccount: { isCurated }, - organization: { name, defaultOrgName }, - isFederatedAuth, -}) => { +export const OnboardingSteps: React.FC = () => { + const { + hasUsers, + hasOrgSources, + canCreateContentSources, + canCreateInvitations, + accountsCount, + sourcesCount, + fpAccount: { isCurated }, + organization: { name, defaultOrgName }, + isFederatedAuth, + } = useValues(OverviewLogic) as IOverviewValues; + const accountsPath = !isFederatedAuth && (canCreateInvitations || isCurated) ? USERS_PATH : undefined; const sourcesPath = canCreateContentSources || isCurated ? ORG_SOURCES_PATH : undefined; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/organization_stats.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/organization_stats.test.tsx index 112e9a910667a..d9b05c5da777d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/organization_stats.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/organization_stats.test.tsx @@ -5,6 +5,8 @@ */ import '../../../__mocks__/shallow_usecontext.mock'; +import './__mocks__/overview_logic.mock'; +import { setMockValues } from './__mocks__'; import React from 'react'; import { shallow } from 'enzyme'; @@ -12,18 +14,18 @@ import { EuiFlexGrid } from '@elastic/eui'; import { OrganizationStats } from './organization_stats'; import { StatisticCard } from './statistic_card'; -import { defaultServerData } from './overview'; describe('OrganizationStats', () => { it('renders', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(StatisticCard)).toHaveLength(2); expect(wrapper.find(EuiFlexGrid).prop('columns')).toEqual(2); }); it('renders additional cards for federated auth', () => { - const wrapper = shallow(); + setMockValues({ isFederatedAuth: false }); + const wrapper = shallow(); expect(wrapper.find(StatisticCard)).toHaveLength(4); expect(wrapper.find(EuiFlexGrid).prop('columns')).toEqual(4); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/organization_stats.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/organization_stats.tsx index aa9be81f32bae..4c5efce9baf12 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/organization_stats.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/organization_stats.tsx @@ -6,6 +6,7 @@ import React from 'react'; import { EuiFlexGrid } from '@elastic/eui'; +import { useValues } from 'kea'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -13,62 +14,66 @@ import { i18n } from '@kbn/i18n'; import { ContentSection } from '../shared/content_section'; import { ORG_SOURCES_PATH, USERS_PATH } from '../../routes'; -import { IAppServerData } from './overview'; +import { OverviewLogic, IOverviewValues } from './overview_logic'; import { StatisticCard } from './statistic_card'; -export const OrganizationStats: React.FC = ({ - sourcesCount, - pendingInvitationsCount, - accountsCount, - personalSourcesCount, - isFederatedAuth, -}) => ( - - } - headerSpacer="m" - > - - - {!isFederatedAuth && ( - <> - - - - )} - { + const { + sourcesCount, + pendingInvitationsCount, + accountsCount, + personalSourcesCount, + isFederatedAuth, + } = useValues(OverviewLogic) as IOverviewValues; + + return ( + + } + headerSpacer="m" + > + + + {!isFederatedAuth && ( + <> + + + )} - count={personalSourcesCount} - /> - - -); + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.test.tsx index e5e5235c52368..744fd8aeb1951 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.test.tsx @@ -5,11 +5,11 @@ */ import '../../../__mocks__/react_router_history.mock'; +import './__mocks__/overview_logic.mock'; +import { mockLogicActions, setMockValues } from './__mocks__'; import React from 'react'; -import { shallow } from 'enzyme'; - -import { mountWithAsyncContext, mockKibanaContext } from '../../../__mocks__'; +import { shallow, mount } from 'enzyme'; import { ErrorState } from '../error_state'; import { Loading } from '../shared/loading'; @@ -18,11 +18,9 @@ import { ViewContentHeader } from '../shared/view_content_header'; import { OnboardingSteps } from './onboarding_steps'; import { OrganizationStats } from './organization_stats'; import { RecentActivity } from './recent_activity'; -import { Overview, defaultServerData } from './overview'; +import { Overview } from './overview'; describe('Overview', () => { - const mockHttp = mockKibanaContext.http; - describe('non-happy-path states', () => { it('isLoading', () => { const wrapper = shallow(); @@ -30,24 +28,24 @@ describe('Overview', () => { expect(wrapper.find(Loading)).toHaveLength(1); }); - it('hasErrorConnecting', async () => { - const wrapper = await mountWithAsyncContext(, { - http: { - ...mockHttp, - get: () => Promise.reject({ invalidPayload: true }), - }, - }); + it('hasErrorConnecting', () => { + setMockValues({ hasErrorConnecting: true }); + const wrapper = shallow(); expect(wrapper.find(ErrorState)).toHaveLength(1); }); }); describe('happy-path states', () => { - it('renders onboarding state', async () => { - const mockApi = jest.fn(() => defaultServerData); - const wrapper = await mountWithAsyncContext(, { - http: { ...mockHttp, get: mockApi }, - }); + it('calls initialize function', async () => { + mount(); + + expect(mockLogicActions.initializeOverview).toHaveBeenCalled(); + }); + + it('renders onboarding state', () => { + setMockValues({ dataLoading: false }); + const wrapper = shallow(); expect(wrapper.find(ViewContentHeader)).toHaveLength(1); expect(wrapper.find(OnboardingSteps)).toHaveLength(1); @@ -55,9 +53,9 @@ describe('Overview', () => { expect(wrapper.find(RecentActivity)).toHaveLength(1); }); - it('renders when onboarding complete', async () => { - const obCompleteData = { - ...defaultServerData, + it('renders when onboarding complete', () => { + setMockValues({ + dataLoading: false, hasUsers: true, hasOrgSources: true, isOldAccount: true, @@ -65,11 +63,8 @@ describe('Overview', () => { name: 'foo', defaultOrgName: 'bar', }, - }; - const mockApi = jest.fn(() => obCompleteData); - const wrapper = await mountWithAsyncContext(, { - http: { ...mockHttp, get: mockApi }, }); + const wrapper = shallow(); expect(wrapper.find(OnboardingSteps)).toHaveLength(0); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.tsx index bacd65a2be75f..b75a2841dad9b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.tsx @@ -4,15 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useContext, useEffect } from 'react'; import { EuiPage, EuiPageBody, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useActions, useValues } from 'kea'; import { SetWorkplaceSearchBreadcrumbs as SetBreadcrumbs } from '../../../shared/kibana_breadcrumbs'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { KibanaContext, IKibanaContext } from '../../../index'; -import { IAccount } from '../../types'; +import { OverviewLogic, IOverviewActions, IOverviewValues } from './overview_logic'; import { ErrorState } from '../error_state'; @@ -22,57 +23,7 @@ import { ViewContentHeader } from '../shared/view_content_header'; import { OnboardingSteps } from './onboarding_steps'; import { OrganizationStats } from './organization_stats'; -import { RecentActivity, IFeedActivity } from './recent_activity'; - -export interface IAppServerData { - hasUsers: boolean; - hasOrgSources: boolean; - canCreateContentSources: boolean; - canCreateInvitations: boolean; - isOldAccount: boolean; - sourcesCount: number; - pendingInvitationsCount: number; - accountsCount: number; - personalSourcesCount: number; - activityFeed: IFeedActivity[]; - organization: { - name: string; - defaultOrgName: string; - }; - isFederatedAuth: boolean; - currentUser: { - firstName: string; - email: string; - name: string; - color: string; - }; - fpAccount: IAccount; -} - -export const defaultServerData = { - accountsCount: 1, - activityFeed: [], - canCreateContentSources: true, - canCreateInvitations: true, - currentUser: { - firstName: '', - email: '', - name: '', - color: '', - }, - fpAccount: {} as IAccount, - hasOrgSources: false, - hasUsers: false, - isFederatedAuth: true, - isOldAccount: false, - organization: { - name: '', - defaultOrgName: '', - }, - pendingInvitationsCount: 0, - personalSourcesCount: 0, - sourcesCount: 0, -} as IAppServerData; +import { RecentActivity } from './recent_activity'; const ONBOARDING_HEADER_TITLE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.overviewOnboardingHeader.title', @@ -96,34 +47,24 @@ const HEADER_DESCRIPTION = i18n.translate( export const Overview: React.FC = () => { const { http } = useContext(KibanaContext) as IKibanaContext; - const [isLoading, setIsLoading] = useState(true); - const [hasErrorConnecting, setHasErrorConnecting] = useState(false); - const [appData, setAppData] = useState(defaultServerData); - - const getAppData = async () => { - try { - const response = await http.get('/api/workplace_search/overview'); - setAppData(response); - } catch (error) { - setHasErrorConnecting(true); - } finally { - setIsLoading(false); - } - }; - - useEffect(() => { - getAppData(); - }, []); - - if (hasErrorConnecting) return ; - if (isLoading) return ; + const { initializeOverview } = useActions(OverviewLogic) as IOverviewActions; const { + dataLoading, + hasErrorConnecting, hasUsers, hasOrgSources, isOldAccount, organization: { name: orgName, defaultOrgName }, - } = appData as IAppServerData; + } = useValues(OverviewLogic) as IOverviewValues; + + useEffect(() => { + initializeOverview({ http }); + }, [initializeOverview]); + + if (hasErrorConnecting) return ; + if (dataLoading) return ; + const hideOnboarding = hasUsers && hasOrgSources && isOldAccount && orgName !== defaultOrgName; const headerTitle = hideOnboarding ? HEADER_TITLE : ONBOARDING_HEADER_TITLE; @@ -140,11 +81,11 @@ export const Overview: React.FC = () => { description={headerDescription} action={} /> - {!hideOnboarding && } + {!hideOnboarding && } - + - + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.test.ts new file mode 100644 index 0000000000000..285ec9b973378 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.test.ts @@ -0,0 +1,141 @@ +/* + * 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 { resetContext } from 'kea'; +import { act } from 'react-dom/test-utils'; + +import { mockKibanaContext } from '../../../__mocks__'; + +import { mockLogicValues } from './__mocks__'; +import { OverviewLogic } from './overview_logic'; + +describe('OverviewLogic', () => { + let unmount: any; + + beforeEach(() => { + resetContext({}); + unmount = OverviewLogic.mount() as any; + jest.clearAllMocks(); + }); + + afterEach(() => { + unmount(); + }); + + it('has expected default values', () => { + expect(OverviewLogic.values).toEqual(mockLogicValues); + }); + + describe('setServerData', () => { + const feed = [{ foo: 'bar' }] as any; + const user = { firstName: 'Joe', email: 'e@e.e', name: 'Joe Jo', color: 'pearl' }; + const account = { + name: 'Jane doe', + id: '1243', + isAdmin: true, + canCreatePersonalSources: true, + groups: [], + supportEligible: true, + }; + const org = { name: 'ACME', defaultOrgName: 'Org' }; + + const data = { + accountsCount: 1, + activityFeed: feed, + canCreateContentSources: true, + canCreateInvitations: true, + currentUser: user, + fpAccount: account, + hasOrgSources: true, + hasUsers: true, + isFederatedAuth: false, + isOldAccount: true, + organization: org, + pendingInvitationsCount: 1, + personalSourcesCount: 1, + sourcesCount: 1, + }; + + beforeEach(() => { + OverviewLogic.actions.setServerData(data); + }); + + it('will set `dataLoading` to false', () => { + expect(OverviewLogic.values.dataLoading).toEqual(false); + }); + + it('will set server values', () => { + expect(OverviewLogic.values.organization).toEqual(org); + expect(OverviewLogic.values.isFederatedAuth).toEqual(false); + expect(OverviewLogic.values.currentUser).toEqual(user); + expect(OverviewLogic.values.fpAccount).toEqual(account); + expect(OverviewLogic.values.canCreateInvitations).toEqual(true); + expect(OverviewLogic.values.hasUsers).toEqual(true); + expect(OverviewLogic.values.hasOrgSources).toEqual(true); + expect(OverviewLogic.values.canCreateContentSources).toEqual(true); + expect(OverviewLogic.values.isOldAccount).toEqual(true); + expect(OverviewLogic.values.sourcesCount).toEqual(1); + expect(OverviewLogic.values.pendingInvitationsCount).toEqual(1); + expect(OverviewLogic.values.accountsCount).toEqual(1); + expect(OverviewLogic.values.personalSourcesCount).toEqual(1); + expect(OverviewLogic.values.activityFeed).toEqual(feed); + }); + }); + + describe('setFlashMessages', () => { + it('will set `flashMessages`', () => { + const flashMessages = { error: ['error'] }; + OverviewLogic.actions.setFlashMessages(flashMessages); + + expect(OverviewLogic.values.flashMessages).toEqual(flashMessages); + }); + }); + + describe('setHasErrorConnecting', () => { + it('will set `hasErrorConnecting`', () => { + OverviewLogic.actions.setHasErrorConnecting(true); + + expect(OverviewLogic.values.hasErrorConnecting).toEqual(true); + expect(OverviewLogic.values.dataLoading).toEqual(false); + }); + }); + + describe('initializeOverview', () => { + it('calls API and sets values', async () => { + const mockHttp = mockKibanaContext.http; + const mockApi = jest.fn(() => mockLogicValues as any); + const setServerDataSpy = jest.spyOn(OverviewLogic.actions, 'setServerData'); + + await act(async () => + OverviewLogic.actions.initializeOverview({ + http: { + ...mockHttp, + get: mockApi, + }, + }) + ); + + expect(mockApi).toHaveBeenCalledWith('/api/workplace_search/overview'); + expect(setServerDataSpy).toHaveBeenCalled(); + }); + + it('handles error state', async () => { + const mockHttp = mockKibanaContext.http; + const setHasErrorConnectingSpy = jest.spyOn(OverviewLogic.actions, 'setHasErrorConnecting'); + + await act(async () => + OverviewLogic.actions.initializeOverview({ + http: { + ...mockHttp, + get: () => Promise.reject(), + }, + }) + ); + + expect(setHasErrorConnectingSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.ts new file mode 100644 index 0000000000000..f1b4f447f7445 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.ts @@ -0,0 +1,168 @@ +/* + * 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 { HttpSetup } from 'src/core/public'; + +import { kea } from 'kea'; + +import { IAccount, IOrganization, IUser } from '../../types'; +import { IFlashMessagesProps, IKeaLogic, TKeaReducers, IKeaParams } from '../../../shared/types'; + +import { IFeedActivity } from './recent_activity'; + +export interface IOverviewServerData { + hasUsers: boolean; + hasOrgSources: boolean; + canCreateContentSources: boolean; + canCreateInvitations: boolean; + isOldAccount: boolean; + sourcesCount: number; + pendingInvitationsCount: number; + accountsCount: number; + personalSourcesCount: number; + activityFeed: IFeedActivity[]; + organization: IOrganization; + isFederatedAuth: boolean; + currentUser: IUser; + fpAccount: IAccount; +} + +export interface IOverviewActions { + setServerData(serverData: IOverviewServerData): void; + setFlashMessages(flashMessages: IFlashMessagesProps): void; + setHasErrorConnecting(hasErrorConnecting: boolean): void; + initializeOverview({ http }: { http: HttpSetup }): void; +} + +export interface IOverviewValues extends IOverviewServerData { + dataLoading: boolean; + hasErrorConnecting: boolean; + flashMessages: IFlashMessagesProps; +} + +export const OverviewLogic = kea({ + actions: (): IOverviewActions => ({ + setServerData: (serverData) => serverData, + setFlashMessages: (flashMessages) => ({ flashMessages }), + setHasErrorConnecting: (hasErrorConnecting) => ({ hasErrorConnecting }), + initializeOverview: ({ http }) => ({ http }), + }), + reducers: (): TKeaReducers => ({ + organization: [ + {} as IOrganization, + { + setServerData: (_, { organization }) => organization, + }, + ], + isFederatedAuth: [ + true, + { + setServerData: (_, { isFederatedAuth }) => isFederatedAuth, + }, + ], + currentUser: [ + {} as IUser, + { + setServerData: (_, { currentUser }) => currentUser, + }, + ], + fpAccount: [ + {} as IAccount, + { + setServerData: (_, { fpAccount }) => fpAccount, + }, + ], + canCreateInvitations: [ + false, + { + setServerData: (_, { canCreateInvitations }) => canCreateInvitations, + }, + ], + flashMessages: [ + {}, + { + setFlashMessages: (_, { flashMessages }) => flashMessages, + }, + ], + hasUsers: [ + false, + { + setServerData: (_, { hasUsers }) => hasUsers, + }, + ], + hasOrgSources: [ + false, + { + setServerData: (_, { hasOrgSources }) => hasOrgSources, + }, + ], + canCreateContentSources: [ + false, + { + setServerData: (_, { canCreateContentSources }) => canCreateContentSources, + }, + ], + isOldAccount: [ + false, + { + setServerData: (_, { isOldAccount }) => isOldAccount, + }, + ], + sourcesCount: [ + 0, + { + setServerData: (_, { sourcesCount }) => sourcesCount, + }, + ], + pendingInvitationsCount: [ + 0, + { + setServerData: (_, { pendingInvitationsCount }) => pendingInvitationsCount, + }, + ], + accountsCount: [ + 0, + { + setServerData: (_, { accountsCount }) => accountsCount, + }, + ], + personalSourcesCount: [ + 0, + { + setServerData: (_, { personalSourcesCount }) => personalSourcesCount, + }, + ], + activityFeed: [ + [], + { + setServerData: (_, { activityFeed }) => activityFeed, + }, + ], + dataLoading: [ + true, + { + setServerData: () => false, + setHasErrorConnecting: () => false, + }, + ], + hasErrorConnecting: [ + false, + { + setHasErrorConnecting: (_, { hasErrorConnecting }) => hasErrorConnecting, + }, + ], + }), + listeners: ({ actions }): Partial => ({ + initializeOverview: async ({ http }: { http: HttpSetup }) => { + try { + const response = await http.get('/api/workplace_search/overview'); + actions.setServerData(response); + } catch (error) { + actions.setHasErrorConnecting(true); + } + }, + }), +} as IKeaParams) as IKeaLogic; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.test.tsx index e9bdedb199dad..22a82af18527d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.test.tsx @@ -5,6 +5,8 @@ */ import '../../../__mocks__/shallow_usecontext.mock'; +import './__mocks__/overview_logic.mock'; +import { setMockValues } from './__mocks__'; import React from 'react'; import { shallow } from 'enzyme'; @@ -12,14 +14,13 @@ import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiLink } from '@elastic/eui'; import { RecentActivity, RecentActivityItem } from './recent_activity'; -import { defaultServerData } from './overview'; jest.mock('../../../shared/telemetry', () => ({ sendTelemetry: jest.fn() })); import { sendTelemetry } from '../../../shared/telemetry'; -const org = { name: 'foo', defaultOrgName: 'bar' }; +const organization = { name: 'foo', defaultOrgName: 'bar' }; -const feed = [ +const activityFeed = [ { id: 'demo', sourceId: 'd2d2d23d', @@ -30,17 +31,19 @@ const feed = [ ]; describe('RecentActivity', () => { - it('renders with no feed data', () => { - const wrapper = shallow(); + it('renders with no activityFeed data', () => { + const wrapper = shallow(); expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); // Branch coverage - renders without error for custom org name - shallow(); + setMockValues({ organization }); + shallow(); }); - it('renders an activity feed with links', () => { - const wrapper = shallow(); + it('renders an activityFeed with links', () => { + setMockValues({ activityFeed }); + const wrapper = shallow(); const activity = wrapper.find(RecentActivityItem).dive(); expect(activity).toHaveLength(1); @@ -51,7 +54,7 @@ describe('RecentActivity', () => { }); it('renders activity item error state', () => { - const props = { ...feed[0], status: 'error' }; + const props = { ...activityFeed[0], status: 'error' }; const wrapper = shallow(); expect(wrapper.find('.activity--error')).toHaveLength(1); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.tsx index 8d69582c93684..2c0fbe1275cbf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.tsx @@ -7,6 +7,7 @@ import React, { useContext } from 'react'; import moment from 'moment'; +import { useValues } from 'kea'; import { EuiEmptyPrompt, EuiLink, EuiPanel, EuiSpacer, EuiLinkProps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -17,7 +18,7 @@ import { sendTelemetry } from '../../../shared/telemetry'; import { KibanaContext, IKibanaContext } from '../../../index'; import { getSourcePath } from '../../routes'; -import { IAppServerData } from './overview'; +import { OverviewLogic, IOverviewValues } from './overview_logic'; import './recent_activity.scss'; @@ -29,10 +30,12 @@ export interface IFeedActivity { sourceId: string; } -export const RecentActivity: React.FC = ({ - organization: { name, defaultOrgName }, - activityFeed, -}) => { +export const RecentActivity: React.FC = () => { + const { + organization: { name, defaultOrgName }, + activityFeed, + } = useValues(OverviewLogic) as IOverviewValues; + return ( , testSubj: 'contentSection', - className: 'test', }; describe('ContentSection', () => { it('renders', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.prop('data-test-subj')).toEqual('contentSection'); expect(wrapper.prop('className')).toEqual('test'); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.test.tsx index 4680f15771caa..b0b07c46b4ea8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.test.tsx @@ -26,9 +26,10 @@ describe('ViewContentHeader', () => { }); it('shows description, when present', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find('p').text()).toEqual('Hello World'); + expect(wrapper.find(EuiFlexGroup).prop('alignItems')).toEqual('center'); }); it('shows action, when present', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index 36b1a56ecba26..cfa70ea29eca8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -6,6 +6,13 @@ import React, { useContext } from 'react'; import { Route, Redirect } from 'react-router-dom'; +import { Provider } from 'react-redux'; +import { Store } from 'redux'; +import { getContext, resetContext } from 'kea'; + +resetContext({ createStore: true }); + +const store = getContext().store as Store; import { KibanaContext, IKibanaContext } from '../index'; @@ -17,13 +24,13 @@ import { Overview } from './components/overview'; export const WorkplaceSearch: React.FC = () => { const { enterpriseSearchUrl } = useContext(KibanaContext) as IKibanaContext; return ( - <> + {!enterpriseSearchUrl ? : } - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index b448c59c52f3e..77c35adef3300 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -13,4 +13,15 @@ export interface IAccount { supportEligible: boolean; } +export interface IOrganization { + name: string; + defaultOrgName: string; +} +export interface IUser { + firstName: string; + email: string; + name: string; + color: string; +} + export type TSpacerSize = 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl'; diff --git a/yarn.lock b/yarn.lock index 0638a019a9402..899bc45fbe3fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19957,6 +19957,11 @@ kdbush@^3.0.0: resolved "https://registry.yarnpkg.com/kdbush/-/kdbush-3.0.0.tgz#f8484794d47004cc2d85ed3a79353dbe0abc2bf0" integrity sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew== +kea@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/kea/-/kea-2.1.1.tgz#6e3c65c4873b67d270a2ec7bf73b0d178937234c" + integrity sha512-W9o4lHLOcEDIu3ASHPrWJJJzL1bMkGyxaHn9kuaDgI96ztBShVrf52R0QPGlQ2k9ca3XnkB/dnVHio1UB8kGWA== + kew@~0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/kew/-/kew-0.1.7.tgz#0a32a817ff1a9b3b12b8c9bacf4bc4d679af8e72" From 88aebc9fe17fa0583b7c5b9af17520511c9b18ad Mon Sep 17 00:00:00 2001 From: liza-mae Date: Mon, 27 Jul 2020 15:10:33 -0600 Subject: [PATCH 54/96] Remove ca cert path for cloud testing (#73317) --- test/common/services/elasticsearch.ts | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/test/common/services/elasticsearch.ts b/test/common/services/elasticsearch.ts index 0436dc901292d..a01f9ff3c8da5 100644 --- a/test/common/services/elasticsearch.ts +++ b/test/common/services/elasticsearch.ts @@ -27,11 +27,18 @@ import { FtrProviderContext } from '../ftr_provider_context'; export function ElasticsearchProvider({ getService }: FtrProviderContext) { const config = getService('config'); - return new Client({ - ssl: { - ca: fs.readFileSync(CA_CERT_PATH, 'utf-8'), - }, - nodes: [formatUrl(config.get('servers.elasticsearch'))], - requestTimeout: config.get('timeouts.esRequestTimeout'), - }); + if (process.env.TEST_CLOUD) { + return new Client({ + nodes: [formatUrl(config.get('servers.elasticsearch'))], + requestTimeout: config.get('timeouts.esRequestTimeout'), + }); + } else { + return new Client({ + ssl: { + ca: fs.readFileSync(CA_CERT_PATH, 'utf-8'), + }, + nodes: [formatUrl(config.get('servers.elasticsearch'))], + requestTimeout: config.get('timeouts.esRequestTimeout'), + }); + } } From 5a472189715931012096b99b95651ffd5791179c Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Mon, 27 Jul 2020 16:24:45 -0500 Subject: [PATCH 55/96] [APM] Fix focus map link on service map (#73338) The link was including `serviceName` from the `urlParams` so it was linking to the wrong service. Overwrite the service name so the link is correct. Fixes #72911. --- .../app/ServiceMap/Popover/Buttons.test.tsx | 32 +++++++++++++++++++ .../app/ServiceMap/Popover/Buttons.tsx | 7 +++- 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.test.tsx diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.test.tsx new file mode 100644 index 0000000000000..4146266b17916 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.test.tsx @@ -0,0 +1,32 @@ +/* + * 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 { Buttons } from './Buttons'; +import { render } from '@testing-library/react'; + +describe('Popover Buttons', () => { + it('renders', () => { + expect(() => + render() + ).not.toThrowError(); + }); + + it('handles focus click', async () => { + const onFocusClick = jest.fn(); + const result = render( + + ); + const focusButton = await result.findByText('Focus map'); + + focusButton.click(); + + expect(onFocusClick).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx index d67447e04ef81..cb33fb32f3b0d 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx @@ -22,7 +22,12 @@ export function Buttons({ onFocusClick = () => {}, selectedNodeServiceName, }: ButtonsProps) { - const urlParams = useUrlParams().urlParams as APMQueryParams; + // The params may contain the service name. We want to use the selected node's + // service name in the button URLs, so make a copy and set the + // `serviceName` property. + const urlParams = { ...useUrlParams().urlParams } as APMQueryParams; + urlParams.serviceName = selectedNodeServiceName; + const detailsUrl = getAPMHref( `/services/${selectedNodeServiceName}/transactions`, '', From 157fb097a9aeed8a9e167efa91347617a258ca5b Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 27 Jul 2020 14:31:02 -0700 Subject: [PATCH 56/96] [dev/build/docker_generator] convert to typescript (#73339) Co-authored-by: spalger --- ...e_dockerfiles.js => bundle_dockerfiles.ts} | 28 ++++++++-------- .../os_packages/docker_generator/index.ts | 1 - .../docker_generator/{run.js => run.ts} | 19 ++++++++--- .../docker_generator/template_context.ts | 33 +++++++++++++++++++ ...emplate.js => build_docker_sh.template.ts} | 4 ++- ...ile.template.js => dockerfile.template.ts} | 4 ++- .../templates/{index.js => index.ts} | 0 ...yml.template.js => kibana_yml.template.ts} | 4 ++- 8 files changed, 71 insertions(+), 22 deletions(-) rename src/dev/build/tasks/os_packages/docker_generator/{bundle_dockerfiles.js => bundle_dockerfiles.ts} (80%) rename src/dev/build/tasks/os_packages/docker_generator/{run.js => run.ts} (90%) create mode 100644 src/dev/build/tasks/os_packages/docker_generator/template_context.ts rename src/dev/build/tasks/os_packages/docker_generator/templates/{build_docker_sh.template.js => build_docker_sh.template.ts} (94%) rename src/dev/build/tasks/os_packages/docker_generator/templates/{dockerfile.template.js => dockerfile.template.ts} (98%) rename src/dev/build/tasks/os_packages/docker_generator/templates/{index.js => index.ts} (100%) rename src/dev/build/tasks/os_packages/docker_generator/templates/{kibana_yml.template.js => kibana_yml.template.ts} (91%) diff --git a/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.js b/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.ts similarity index 80% rename from src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.js rename to src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.ts index 3f34a84057668..7a8f7316913be 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.js +++ b/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.ts @@ -18,10 +18,14 @@ */ import { resolve } from 'path'; -import { compressTar, copyAll, mkdirp, write } from '../../../lib'; + +import { ToolingLog } from '@kbn/dev-utils'; + +import { compressTar, copyAll, mkdirp, write, Config } from '../../../lib'; import { dockerfileTemplate } from './templates'; +import { TemplateContext } from './template_context'; -export async function bundleDockerFiles(config, log, build, scope) { +export async function bundleDockerFiles(config: Config, log: ToolingLog, scope: TemplateContext) { log.info( `Generating kibana${scope.imageFlavor}${scope.ubiImageFlavor} docker build context bundle` ); @@ -50,17 +54,15 @@ export async function bundleDockerFiles(config, log, build, scope) { // Compress dockerfiles dir created inside // docker build dir as output it as a target // on targets folder - await compressTar( - { - archiverOptions: { - gzip: true, - gzipOptions: { - level: 9, - }, + await compressTar({ + source: dockerFilesBuildDir, + destination: dockerFilesOutputDir, + archiverOptions: { + gzip: true, + gzipOptions: { + level: 9, }, - createRootDirectory: false, }, - dockerFilesBuildDir, - dockerFilesOutputDir - ); + createRootDirectory: false, + }); } diff --git a/src/dev/build/tasks/os_packages/docker_generator/index.ts b/src/dev/build/tasks/os_packages/docker_generator/index.ts index 78d2b197dc7b2..dff56585fc704 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/index.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/index.ts @@ -17,5 +17,4 @@ * under the License. */ -// @ts-expect-error not ts yet export { runDockerGenerator, runDockerGeneratorForUBI } from './run'; diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.js b/src/dev/build/tasks/os_packages/docker_generator/run.ts similarity index 90% rename from src/dev/build/tasks/os_packages/docker_generator/run.js rename to src/dev/build/tasks/os_packages/docker_generator/run.ts index b6dab43887f14..0a26729f3502d 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.js +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -20,8 +20,12 @@ import { access, link, unlink, chmod } from 'fs'; import { resolve } from 'path'; import { promisify } from 'util'; -import { write, copyAll, mkdirp, exec } from '../../../lib'; + +import { ToolingLog } from '@kbn/dev-utils'; + +import { write, copyAll, mkdirp, exec, Config, Build } from '../../../lib'; import * as dockerTemplates from './templates'; +import { TemplateContext } from './template_context'; import { bundleDockerFiles } from './bundle_dockerfiles'; const accessAsync = promisify(access); @@ -29,7 +33,12 @@ const linkAsync = promisify(link); const unlinkAsync = promisify(unlink); const chmodAsync = promisify(chmod); -export async function runDockerGenerator(config, log, build, ubi = false) { +export async function runDockerGenerator( + config: Config, + log: ToolingLog, + build: Build, + ubi: boolean = false +) { // UBI var config const baseOSImage = ubi ? 'registry.access.redhat.com/ubi7/ubi-minimal:7.7' : 'centos:7'; const ubiVersionTag = 'ubi7'; @@ -52,7 +61,7 @@ export async function runDockerGenerator(config, log, build, ubi = false) { const dockerOutputDir = config.resolveFromTarget( `kibana${imageFlavor}${ubiImageFlavor}-${versionTag}-docker.tar.gz` ); - const scope = { + const scope: TemplateContext = { artifactTarball, imageFlavor, versionTag, @@ -112,10 +121,10 @@ export async function runDockerGenerator(config, log, build, ubi = false) { }); // Pack Dockerfiles and create a target for them - await bundleDockerFiles(config, log, build, scope); + await bundleDockerFiles(config, log, scope); } -export async function runDockerGeneratorForUBI(config, log, build) { +export async function runDockerGeneratorForUBI(config: Config, log: ToolingLog, build: Build) { // Only run ubi docker image build for default distribution if (build.isOss()) { return; diff --git a/src/dev/build/tasks/os_packages/docker_generator/template_context.ts b/src/dev/build/tasks/os_packages/docker_generator/template_context.ts new file mode 100644 index 0000000000000..115d4c6927c30 --- /dev/null +++ b/src/dev/build/tasks/os_packages/docker_generator/template_context.ts @@ -0,0 +1,33 @@ +/* + * 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. + */ + +export interface TemplateContext { + artifactTarball: string; + imageFlavor: string; + versionTag: string; + license: string; + artifactsDir: string; + imageTag: string; + dockerBuildDir: string; + dockerOutputDir: string; + baseOSImage: string; + ubiImageFlavor: string; + dockerBuildDate: string; + usePublicArtifact?: boolean; +} diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.js b/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts similarity index 94% rename from src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.js rename to src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts index 4e8dfe188b867..ff6fcf7548d9d 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.js +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts @@ -19,6 +19,8 @@ import dedent from 'dedent'; +import { TemplateContext } from '../template_context'; + function generator({ imageTag, imageFlavor, @@ -26,7 +28,7 @@ function generator({ dockerOutputDir, baseOSImage, ubiImageFlavor, -}) { +}: TemplateContext) { return dedent(` #!/usr/bin/env bash # diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.js b/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts similarity index 98% rename from src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.js rename to src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts index 5832d00162b20..ea2f881768c8f 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.js +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts @@ -19,6 +19,8 @@ import dedent from 'dedent'; +import { TemplateContext } from '../template_context'; + function generator({ artifactTarball, versionTag, @@ -27,7 +29,7 @@ function generator({ baseOSImage, ubiImageFlavor, dockerBuildDate, -}) { +}: TemplateContext) { const copyArtifactTarballInsideDockerOptFolder = () => { if (usePublicArtifact) { return `RUN cd /opt && curl --retry 8 -s -L -O https://artifacts.elastic.co/downloads/kibana/${artifactTarball} && cd -`; diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/index.js b/src/dev/build/tasks/os_packages/docker_generator/templates/index.ts similarity index 100% rename from src/dev/build/tasks/os_packages/docker_generator/templates/index.js rename to src/dev/build/tasks/os_packages/docker_generator/templates/index.ts diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.js b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts similarity index 91% rename from src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.js rename to src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts index c80f9334cfaeb..240ec6f4e9326 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.js +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts @@ -19,7 +19,9 @@ import dedent from 'dedent'; -function generator({ imageFlavor }) { +import { TemplateContext } from '../template_context'; + +function generator({ imageFlavor }: TemplateContext) { return dedent(` # # ** THIS IS AN AUTO-GENERATED FILE ** From 57997beed8f7eaf7f67cd17d397eb4abcd6abf36 Mon Sep 17 00:00:00 2001 From: Constance Date: Mon, 27 Jul 2020 15:06:42 -0700 Subject: [PATCH 57/96] [Enterprise Search] Error state UI tweaks to account for current Cloud SSO behavior (#73324) * Do not disable the Launch App Search button on the error page - so users always have access to App Search * Add troubleshooting steps that mention user authentication - more info can be found in setup guide * Tweak styling/spacing on troubleshooting steps * Copyedits (thanks Chris!) --- .../components/empty_states/error_state.tsx | 2 +- .../engine_overview_header.test.tsx | 8 ------- .../engine_overview_header.tsx | 23 +++++------------- .../error_state/error_state_prompt.scss | 12 ++++++++++ .../shared/error_state/error_state_prompt.tsx | 24 ++++++++++++++++++- 5 files changed, 42 insertions(+), 27 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.scss diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx index 7ac02082ee75c..346e70d32f7b1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx @@ -21,7 +21,7 @@ export const ErrorState: React.FC = () => { - + diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.test.tsx index 2e49540270ef0..7d2106f2a56f7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.test.tsx @@ -30,12 +30,4 @@ describe('EngineOverviewHeader', () => { button.simulate('click'); expect(sendTelemetry).toHaveBeenCalled(); }); - - it('renders a disabled button when isButtonDisabled is true', () => { - const wrapper = shallow(); - const button = wrapper.find('[data-test-subj="launchButton"]'); - - expect(button.prop('isDisabled')).toBe(true); - expect(button.prop('href')).toBeUndefined(); - }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.tsx index 9aafa8ec0380c..cc480d241ad50 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.tsx @@ -18,34 +18,23 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { sendTelemetry } from '../../../shared/telemetry'; import { KibanaContext, IKibanaContext } from '../../../index'; -interface IEngineOverviewHeaderProps { - isButtonDisabled?: boolean; -} - -export const EngineOverviewHeader: React.FC = ({ - isButtonDisabled, -}) => { +export const EngineOverviewHeader: React.FC = () => { const { enterpriseSearchUrl, http } = useContext(KibanaContext) as IKibanaContext; const buttonProps = { fill: true, iconType: 'popout', 'data-test-subj': 'launchButton', - } as EuiButtonProps & EuiLinkProps; - - if (isButtonDisabled) { - buttonProps.isDisabled = true; - } else { - buttonProps.href = `${enterpriseSearchUrl}/as`; - buttonProps.target = '_blank'; - buttonProps.onClick = () => + href: `${enterpriseSearchUrl}/as`, + target: '_blank', + onClick: () => sendTelemetry({ http, product: 'app_search', action: 'clicked', metric: 'header_launch_button', - }); - } + }), + } as EuiButtonProps & EuiLinkProps; return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.scss b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.scss new file mode 100644 index 0000000000000..0d9926ab147bf --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.scss @@ -0,0 +1,12 @@ +.troubleshootingSteps { + text-align: left; + + li { + margin: $euiSizeS auto; + } + + ul, + ol { + margin-bottom: 0; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx index 81455cea0b497..ccd5beff66e70 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx @@ -11,6 +11,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton } from '../react_router_helpers'; import { KibanaContext, IKibanaContext } from '../../index'; +import './error_state_prompt.scss'; + export const ErrorStatePrompt: React.FC = () => { const { enterpriseSearchUrl } = useContext(KibanaContext) as IKibanaContext; @@ -38,7 +40,7 @@ export const ErrorStatePrompt: React.FC = () => { }} />

-
    +
    1. { defaultMessage="Confirm that the Enterprise Search server is responsive." />
    2. +
    3. + +
        +
      • + +
      • +
      • + +
      • +
      +
    4. Date: Mon, 27 Jul 2020 18:19:16 -0400 Subject: [PATCH 58/96] [Security Solution][Exceptions] - Update exception item comments to include id (#73129) ## Summary This PR is somewhat of an intermediary step. Comments on exception list items are denormalized. We initially decided that we would not add `uuid` to comments, but found that it is in fact necessary. This is intermediary in the sense that what we ideally want to have is a dedicated `comments` CRUD route. Also just note that I added a callout for when a version conflict occurs (ie: exception item was updated by someone else while a user is editing the same item). With this PR users are able to: - Create comments when creating exception list items - Add new comments on exception item update Users will currently be blocked from: - Deleting comments - Updating comments - Updating exception item if version conflict is found --- x-pack/plugins/lists/common/constants.mock.ts | 1 + .../create_endpoint_list_item_schema.test.ts | 36 +- .../create_exception_list_item_schema.test.ts | 36 +- ...ate_exception_list_item_validation.test.ts | 43 ++ .../update_exception_list_item_validation.ts | 40 ++ .../{comments.mock.ts => comment.mock.ts} | 7 +- .../{comments.test.ts => comment.test.ts} | 109 +++-- .../schemas/types/{comments.ts => comment.ts} | 23 +- ...omments.mock.ts => create_comment.mock.ts} | 4 +- ...omments.test.ts => create_comment.test.ts} | 50 +-- .../{create_comments.ts => create_comment.ts} | 11 +- .../types/default_comments_array.test.ts | 21 +- .../schemas/types/default_comments_array.ts | 6 +- .../default_create_comments_array.test.ts | 30 +- .../types/default_create_comments_array.ts | 6 +- .../default_update_comments_array.test.ts | 23 +- .../types/default_update_comments_array.ts | 2 +- .../lists/common/schemas/types/index.ts | 6 +- ...omments.mock.ts => update_comment.mock.ts} | 15 +- .../schemas/types/update_comment.test.ts | 150 +++++++ .../{update_comments.ts => update_comment.ts} | 20 +- .../schemas/types/update_comments.test.ts | 108 ----- x-pack/plugins/lists/common/shared_exports.ts | 5 +- .../update_exception_list_item_route.ts | 6 + .../server/saved_objects/exception_list.ts | 3 + .../updates/simple_update_item.json | 25 +- .../create_exception_list_item.ts | 5 +- .../services/exception_lists/utils.test.ts | 390 ++---------------- .../server/services/exception_lists/utils.ts | 115 +----- .../common/shared_imports.ts | 5 +- .../exceptions/add_exception_comments.tsx | 4 +- .../exceptions/add_exception_modal/index.tsx | 4 +- .../components/exceptions/builder/index.tsx | 2 +- .../exceptions/edit_exception_modal/index.tsx | 40 +- .../edit_exception_modal/translations.ts | 15 + .../components/exceptions/helpers.test.tsx | 55 ++- .../common/components/exceptions/helpers.tsx | 55 ++- .../exception_item/exception_details.test.tsx | 2 +- .../viewer/exception_item/index.stories.tsx | 2 +- .../viewer/exception_item/index.test.tsx | 2 +- .../components/exceptions/viewer/index.tsx | 3 +- 41 files changed, 702 insertions(+), 783 deletions(-) create mode 100644 x-pack/plugins/lists/common/schemas/request/update_exception_list_item_validation.test.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/update_exception_list_item_validation.ts rename x-pack/plugins/lists/common/schemas/types/{comments.mock.ts => comment.mock.ts} (71%) rename x-pack/plugins/lists/common/schemas/types/{comments.test.ts => comment.test.ts} (56%) rename x-pack/plugins/lists/common/schemas/types/{comments.ts => comment.ts} (56%) rename x-pack/plugins/lists/common/schemas/types/{create_comments.mock.ts => create_comment.mock.ts} (73%) rename x-pack/plugins/lists/common/schemas/types/{create_comments.test.ts => create_comment.test.ts} (72%) rename x-pack/plugins/lists/common/schemas/types/{create_comments.ts => create_comment.ts} (64%) rename x-pack/plugins/lists/common/schemas/types/{update_comments.mock.ts => update_comment.mock.ts} (54%) create mode 100644 x-pack/plugins/lists/common/schemas/types/update_comment.test.ts rename x-pack/plugins/lists/common/schemas/types/{update_comments.ts => update_comment.ts} (58%) delete mode 100644 x-pack/plugins/lists/common/schemas/types/update_comments.test.ts diff --git a/x-pack/plugins/lists/common/constants.mock.ts b/x-pack/plugins/lists/common/constants.mock.ts index 30f219c3ec101..22706890e2020 100644 --- a/x-pack/plugins/lists/common/constants.mock.ts +++ b/x-pack/plugins/lists/common/constants.mock.ts @@ -6,6 +6,7 @@ import { EntriesArray } from './schemas/types'; export const DATE_NOW = '2020-04-20T15:25:31.830Z'; +export const OLD_DATE_RELATIVE_TO_DATE_NOW = '2020-04-19T15:25:31.830Z'; export const USER = 'some user'; export const LIST_INDEX = '.lists'; export const LIST_ITEM_INDEX = '.items'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts index 5de9fbb0d5b50..75e0410be610a 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts @@ -8,8 +8,8 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; -import { getCreateCommentsArrayMock } from '../types/create_comments.mock'; -import { getCommentsMock } from '../types/comments.mock'; +import { getCreateCommentsArrayMock } from '../types/create_comment.mock'; +import { getCommentsMock } from '../types/comment.mock'; import { CommentsArray } from '../types'; import { @@ -19,7 +19,7 @@ import { import { getCreateEndpointListItemSchemaMock } from './create_endpoint_list_item_schema.mock'; describe('create_endpoint_list_item_schema', () => { - test('it should validate a typical list item request not counting the auto generated uuid', () => { + test('it should pass validation when supplied a typical list item request not counting the auto generated uuid', () => { const payload = getCreateEndpointListItemSchemaMock(); const decoded = createEndpointListItemSchema.decode(payload); const checked = exactCheck(payload, decoded); @@ -29,7 +29,7 @@ describe('create_endpoint_list_item_schema', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate an undefined for "description"', () => { + test('it should fail validation when supplied an undefined for "description"', () => { const payload = getCreateEndpointListItemSchemaMock(); delete payload.description; const decoded = createEndpointListItemSchema.decode(payload); @@ -41,7 +41,7 @@ describe('create_endpoint_list_item_schema', () => { expect(message.schema).toEqual({}); }); - test('it should not validate an undefined for "name"', () => { + test('it should fail validation when supplied an undefined for "name"', () => { const payload = getCreateEndpointListItemSchemaMock(); delete payload.name; const decoded = createEndpointListItemSchema.decode(payload); @@ -53,7 +53,7 @@ describe('create_endpoint_list_item_schema', () => { expect(message.schema).toEqual({}); }); - test('it should not validate an undefined for "type"', () => { + test('it should fail validation when supplied an undefined for "type"', () => { const payload = getCreateEndpointListItemSchemaMock(); delete payload.type; const decoded = createEndpointListItemSchema.decode(payload); @@ -65,7 +65,7 @@ describe('create_endpoint_list_item_schema', () => { expect(message.schema).toEqual({}); }); - test('it should not validate a "list_id" since it does not required one', () => { + test('it should fail validation when supplied a "list_id" since it does not required one', () => { const inputPayload: CreateEndpointListItemSchema & { list_id: string } = { ...getCreateEndpointListItemSchemaMock(), list_id: 'list-123', @@ -77,7 +77,7 @@ describe('create_endpoint_list_item_schema', () => { expect(message.schema).toEqual({}); }); - test('it should not validate a "namespace_type" since it does not required one', () => { + test('it should fail validation when supplied a "namespace_type" since it does not required one', () => { const inputPayload: CreateEndpointListItemSchema & { namespace_type: string } = { ...getCreateEndpointListItemSchemaMock(), namespace_type: 'single', @@ -89,7 +89,7 @@ describe('create_endpoint_list_item_schema', () => { expect(message.schema).toEqual({}); }); - test('it should validate an undefined for "meta" but strip it out and generate a correct body not counting the auto generated uuid', () => { + test('it should pass validation when supplied an undefined for "meta" but strip it out and generate a correct body not counting the auto generated uuid', () => { const payload = getCreateEndpointListItemSchemaMock(); const outputPayload = getCreateEndpointListItemSchemaMock(); delete payload.meta; @@ -102,7 +102,7 @@ describe('create_endpoint_list_item_schema', () => { expect(message.schema).toEqual(outputPayload); }); - test('it should validate an undefined for "comments" but return an array and generate a correct body not counting the auto generated uuid', () => { + test('it should pass validation when supplied an undefined for "comments" but return an array and generate a correct body not counting the auto generated uuid', () => { const inputPayload = getCreateEndpointListItemSchemaMock(); const outputPayload = getCreateEndpointListItemSchemaMock(); delete inputPayload.comments; @@ -115,7 +115,7 @@ describe('create_endpoint_list_item_schema', () => { expect(message.schema).toEqual(outputPayload); }); - test('it should validate "comments" array', () => { + test('it should pass validation when supplied "comments" array', () => { const inputPayload = { ...getCreateEndpointListItemSchemaMock(), comments: getCreateCommentsArrayMock(), @@ -128,7 +128,7 @@ describe('create_endpoint_list_item_schema', () => { expect(message.schema).toEqual(inputPayload); }); - test('it should NOT validate "comments" with "created_at" or "created_by" values', () => { + test('it should fail validation when supplied "comments" with "created_at", "created_by", or "id" values', () => { const inputPayload: Omit & { comments?: CommentsArray; } = { @@ -138,11 +138,11 @@ describe('create_endpoint_list_item_schema', () => { const decoded = createEndpointListItemSchema.decode(inputPayload); const checked = exactCheck(inputPayload, decoded); const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['invalid keys "created_at,created_by"']); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "created_at,created_by,id"']); expect(message.schema).toEqual({}); }); - test('it should NOT validate an undefined for "entries"', () => { + test('it should fail validation when supplied an undefined for "entries"', () => { const inputPayload = getCreateEndpointListItemSchemaMock(); const outputPayload = getCreateEndpointListItemSchemaMock(); delete inputPayload.entries; @@ -157,7 +157,7 @@ describe('create_endpoint_list_item_schema', () => { expect(message.schema).toEqual({}); }); - test('it should validate an undefined for "tags" but return an array and generate a correct body not counting the auto generated uuid', () => { + test('it should pass validation when supplied an undefined for "tags" but return an array and generate a correct body not counting the auto generated uuid', () => { const inputPayload = getCreateEndpointListItemSchemaMock(); const outputPayload = getCreateEndpointListItemSchemaMock(); delete inputPayload.tags; @@ -170,7 +170,7 @@ describe('create_endpoint_list_item_schema', () => { expect(message.schema).toEqual(outputPayload); }); - test('it should validate an undefined for "_tags" but return an array and generate a correct body not counting the auto generated uuid', () => { + test('it should pass validation when supplied an undefined for "_tags" but return an array and generate a correct body not counting the auto generated uuid', () => { const inputPayload = getCreateEndpointListItemSchemaMock(); const outputPayload = getCreateEndpointListItemSchemaMock(); delete inputPayload._tags; @@ -183,7 +183,7 @@ describe('create_endpoint_list_item_schema', () => { expect(message.schema).toEqual(outputPayload); }); - test('it should validate an undefined for "item_id" and auto generate a uuid', () => { + test('it should pass validation when supplied an undefined for "item_id" and auto generate a uuid', () => { const inputPayload = getCreateEndpointListItemSchemaMock(); delete inputPayload.item_id; const decoded = createEndpointListItemSchema.decode(inputPayload); @@ -195,7 +195,7 @@ describe('create_endpoint_list_item_schema', () => { ); }); - test('it should validate an undefined for "item_id" and generate a correct body not counting the uuid', () => { + test('it should pass validation when supplied an undefined for "item_id" and generate a correct body not counting the uuid', () => { const inputPayload = getCreateEndpointListItemSchemaMock(); delete inputPayload.item_id; const decoded = createEndpointListItemSchema.decode(inputPayload); diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts index 08f3966af08d9..cf4c1fea0306f 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts @@ -8,8 +8,8 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; -import { getCreateCommentsArrayMock } from '../types/create_comments.mock'; -import { getCommentsMock } from '../types/comments.mock'; +import { getCreateCommentsArrayMock } from '../types/create_comment.mock'; +import { getCommentsMock } from '../types/comment.mock'; import { CommentsArray } from '../types'; import { @@ -19,7 +19,7 @@ import { import { getCreateExceptionListItemSchemaMock } from './create_exception_list_item_schema.mock'; describe('create_exception_list_item_schema', () => { - test('it should validate a typical exception list item request not counting the auto generated uuid', () => { + test('it should pass validation when supplied a typical exception list item request not counting the auto generated uuid', () => { const payload = getCreateExceptionListItemSchemaMock(); const decoded = createExceptionListItemSchema.decode(payload); const checked = exactCheck(payload, decoded); @@ -29,7 +29,7 @@ describe('create_exception_list_item_schema', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate an undefined for "description"', () => { + test('it should fail validation when supplied an undefined for "description"', () => { const payload = getCreateExceptionListItemSchemaMock(); delete payload.description; const decoded = createExceptionListItemSchema.decode(payload); @@ -41,7 +41,7 @@ describe('create_exception_list_item_schema', () => { expect(message.schema).toEqual({}); }); - test('it should not validate an undefined for "name"', () => { + test('it should fail validation when supplied an undefined for "name"', () => { const payload = getCreateExceptionListItemSchemaMock(); delete payload.name; const decoded = createExceptionListItemSchema.decode(payload); @@ -53,7 +53,7 @@ describe('create_exception_list_item_schema', () => { expect(message.schema).toEqual({}); }); - test('it should not validate an undefined for "type"', () => { + test('it should fail validation when supplied an undefined for "type"', () => { const payload = getCreateExceptionListItemSchemaMock(); delete payload.type; const decoded = createExceptionListItemSchema.decode(payload); @@ -65,7 +65,7 @@ describe('create_exception_list_item_schema', () => { expect(message.schema).toEqual({}); }); - test('it should not validate an undefined for "list_id"', () => { + test('it should fail validation when supplied an undefined for "list_id"', () => { const inputPayload = getCreateExceptionListItemSchemaMock(); delete inputPayload.list_id; const decoded = createExceptionListItemSchema.decode(inputPayload); @@ -77,7 +77,7 @@ describe('create_exception_list_item_schema', () => { expect(message.schema).toEqual({}); }); - test('it should validate an undefined for "meta" but strip it out and generate a correct body not counting the auto generated uuid', () => { + test('it should pass validation when supplied an undefined for "meta" but strip it out and generate a correct body not counting the auto generated uuid', () => { const payload = getCreateExceptionListItemSchemaMock(); const outputPayload = getCreateExceptionListItemSchemaMock(); delete payload.meta; @@ -90,7 +90,7 @@ describe('create_exception_list_item_schema', () => { expect(message.schema).toEqual(outputPayload); }); - test('it should validate an undefined for "comments" but return an array and generate a correct body not counting the auto generated uuid', () => { + test('it should pass validation when supplied an undefined for "comments" but return an array and generate a correct body not counting the auto generated uuid', () => { const inputPayload = getCreateExceptionListItemSchemaMock(); const outputPayload = getCreateExceptionListItemSchemaMock(); delete inputPayload.comments; @@ -103,7 +103,7 @@ describe('create_exception_list_item_schema', () => { expect(message.schema).toEqual(outputPayload); }); - test('it should validate "comments" array', () => { + test('it should pass validation when supplied "comments" array', () => { const inputPayload = { ...getCreateExceptionListItemSchemaMock(), comments: getCreateCommentsArrayMock(), @@ -116,7 +116,7 @@ describe('create_exception_list_item_schema', () => { expect(message.schema).toEqual(inputPayload); }); - test('it should NOT validate "comments" with "created_at" or "created_by" values', () => { + test('it should fail validation when supplied "comments" with "created_at" or "created_by" values', () => { const inputPayload: Omit & { comments?: CommentsArray; } = { @@ -126,11 +126,11 @@ describe('create_exception_list_item_schema', () => { const decoded = createExceptionListItemSchema.decode(inputPayload); const checked = exactCheck(inputPayload, decoded); const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['invalid keys "created_at,created_by"']); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "created_at,created_by,id"']); expect(message.schema).toEqual({}); }); - test('it should NOT validate an undefined for "entries"', () => { + test('it should fail validation when supplied an undefined for "entries"', () => { const inputPayload = getCreateExceptionListItemSchemaMock(); const outputPayload = getCreateExceptionListItemSchemaMock(); delete inputPayload.entries; @@ -145,7 +145,7 @@ describe('create_exception_list_item_schema', () => { expect(message.schema).toEqual({}); }); - test('it should validate an undefined for "namespace_type" but return enum "single" and generate a correct body not counting the auto generated uuid', () => { + test('it should pass validation when supplied an undefined for "namespace_type" but return enum "single" and generate a correct body not counting the auto generated uuid', () => { const inputPayload = getCreateExceptionListItemSchemaMock(); const outputPayload = getCreateExceptionListItemSchemaMock(); delete inputPayload.namespace_type; @@ -158,7 +158,7 @@ describe('create_exception_list_item_schema', () => { expect(message.schema).toEqual(outputPayload); }); - test('it should validate an undefined for "tags" but return an array and generate a correct body not counting the auto generated uuid', () => { + test('it should pass validation when supplied an undefined for "tags" but return an array and generate a correct body not counting the auto generated uuid', () => { const inputPayload = getCreateExceptionListItemSchemaMock(); const outputPayload = getCreateExceptionListItemSchemaMock(); delete inputPayload.tags; @@ -171,7 +171,7 @@ describe('create_exception_list_item_schema', () => { expect(message.schema).toEqual(outputPayload); }); - test('it should validate an undefined for "_tags" but return an array and generate a correct body not counting the auto generated uuid', () => { + test('it should pass validation when supplied an undefined for "_tags" but return an array and generate a correct body not counting the auto generated uuid', () => { const inputPayload = getCreateExceptionListItemSchemaMock(); const outputPayload = getCreateExceptionListItemSchemaMock(); delete inputPayload._tags; @@ -184,7 +184,7 @@ describe('create_exception_list_item_schema', () => { expect(message.schema).toEqual(outputPayload); }); - test('it should validate an undefined for "item_id" and auto generate a uuid', () => { + test('it should pass validation when supplied an undefined for "item_id" and auto generate a uuid', () => { const inputPayload = getCreateExceptionListItemSchemaMock(); delete inputPayload.item_id; const decoded = createExceptionListItemSchema.decode(inputPayload); @@ -196,7 +196,7 @@ describe('create_exception_list_item_schema', () => { ); }); - test('it should validate an undefined for "item_id" and generate a correct body not counting the uuid', () => { + test('it should pass validation when supplied an undefined for "item_id" and generate a correct body not counting the uuid', () => { const inputPayload = getCreateExceptionListItemSchemaMock(); delete inputPayload.item_id; const decoded = createExceptionListItemSchema.decode(inputPayload); diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_validation.test.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_validation.test.ts new file mode 100644 index 0000000000000..3358582786cc7 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_validation.test.ts @@ -0,0 +1,43 @@ +/* + * 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 { getUpdateExceptionListItemSchemaMock } from './update_exception_list_item_schema.mock'; +import { validateComments } from './update_exception_list_item_validation'; + +describe('update_exception_list_item_validation', () => { + describe('#validateComments', () => { + test('it returns no errors if comments is undefined', () => { + const payload = getUpdateExceptionListItemSchemaMock(); + delete payload.comments; + const output = validateComments(payload); + + expect(output).toEqual([]); + }); + + test('it returns no errors if new comments are append only', () => { + const payload = getUpdateExceptionListItemSchemaMock(); + payload.comments = [ + { comment: 'Im an old comment', id: '1' }, + { comment: 'Im a new comment' }, + ]; + const output = validateComments(payload); + + expect(output).toEqual([]); + }); + + test('it returns error if comments are not append only', () => { + const payload = getUpdateExceptionListItemSchemaMock(); + payload.comments = [ + { comment: 'Im an old comment', id: '1' }, + { comment: 'Im a new comment modifying the order of existing comments' }, + { comment: 'Im an old comment', id: '2' }, + ]; + const output = validateComments(payload); + + expect(output).toEqual(['item "comments" are append only']); + }); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_validation.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_validation.ts new file mode 100644 index 0000000000000..5e44c4e9f73e7 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_validation.ts @@ -0,0 +1,40 @@ +/* + * 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 { UpdateExceptionListItemSchema } from './update_exception_list_item_schema'; + +export const validateComments = (item: UpdateExceptionListItemSchema): string[] => { + if (item.comments == null) { + return []; + } + + const [appendOnly] = item.comments.reduce( + (acc, comment) => { + const [, hasNewComments] = acc; + if (comment.id == null) { + return [true, true]; + } + + if (hasNewComments && comment.id != null) { + return [false, true]; + } + + return acc; + }, + [true, false] + ); + if (!appendOnly) { + return ['item "comments" are append only']; + } else { + return []; + } +}; + +export const updateExceptionListItemValidate = ( + schema: UpdateExceptionListItemSchema +): string[] => { + return [...validateComments(schema)]; +}; diff --git a/x-pack/plugins/lists/common/schemas/types/comments.mock.ts b/x-pack/plugins/lists/common/schemas/types/comment.mock.ts similarity index 71% rename from x-pack/plugins/lists/common/schemas/types/comments.mock.ts rename to x-pack/plugins/lists/common/schemas/types/comment.mock.ts index 9e56ac292f8b5..213259b3cce29 100644 --- a/x-pack/plugins/lists/common/schemas/types/comments.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/comment.mock.ts @@ -4,14 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DATE_NOW, USER } from '../../constants.mock'; +import { DATE_NOW, ID, USER } from '../../constants.mock'; -import { Comments, CommentsArray } from './comments'; +import { Comment, CommentsArray } from './comment'; -export const getCommentsMock = (): Comments => ({ +export const getCommentsMock = (): Comment => ({ comment: 'some old comment', created_at: DATE_NOW, created_by: USER, + id: ID, }); export const getCommentsArrayMock = (): CommentsArray => [getCommentsMock(), getCommentsMock()]; diff --git a/x-pack/plugins/lists/common/schemas/types/comments.test.ts b/x-pack/plugins/lists/common/schemas/types/comment.test.ts similarity index 56% rename from x-pack/plugins/lists/common/schemas/types/comments.test.ts rename to x-pack/plugins/lists/common/schemas/types/comment.test.ts index 29bfde03abcc8..c7c945277f756 100644 --- a/x-pack/plugins/lists/common/schemas/types/comments.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/comment.test.ts @@ -10,56 +10,79 @@ import { left } from 'fp-ts/lib/Either'; import { DATE_NOW } from '../../constants.mock'; import { foldLeftRight, getPaths } from '../../siem_common_deps'; -import { getCommentsArrayMock, getCommentsMock } from './comments.mock'; +import { getCommentsArrayMock, getCommentsMock } from './comment.mock'; import { - Comments, + Comment, CommentsArray, CommentsArrayOrUndefined, - comments, + comment, commentsArray, commentsArrayOrUndefined, -} from './comments'; +} from './comment'; -describe('Comments', () => { - describe('comments', () => { - test('it should validate a comments', () => { +describe('Comment', () => { + describe('comment', () => { + test('it fails validation when "id" is undefined', () => { + const payload = { ...getCommentsMock(), id: undefined }; + const decoded = comment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it passes validation with a typical comment', () => { const payload = getCommentsMock(); - const decoded = comments.decode(payload); + const decoded = comment.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); expect(message.schema).toEqual(payload); }); - test('it should validate with "updated_at" and "updated_by"', () => { + test('it passes validation with "updated_at" and "updated_by" fields included', () => { const payload = getCommentsMock(); payload.updated_at = DATE_NOW; payload.updated_by = 'someone'; - const decoded = comments.decode(payload); + const decoded = comment.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); expect(message.schema).toEqual(payload); }); - test('it should not validate when undefined', () => { + test('it fails validation when undefined', () => { const payload = undefined; - const decoded = comments.decode(payload); + const decoded = comment.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)"', - 'Invalid value "undefined" supplied to "({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)"', + 'Invalid value "undefined" supplied to "({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)"', + 'Invalid value "undefined" supplied to "({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)"', ]); expect(message.schema).toEqual({}); }); - test('it should not validate when "comment" is not a string', () => { - const payload: Omit & { comment: string[] } = { + test('it fails validation when "comment" is an empty string', () => { + const payload: Omit & { comment: string } = { + ...getCommentsMock(), + comment: '', + }; + const decoded = comment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "comment"']); + expect(message.schema).toEqual({}); + }); + + test('it fails validation when "comment" is not a string', () => { + const payload: Omit & { comment: string[] } = { ...getCommentsMock(), comment: ['some value'], }; - const decoded = comments.decode(payload); + const decoded = comment.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -68,12 +91,12 @@ describe('Comments', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "created_at" is not a string', () => { - const payload: Omit & { created_at: number } = { + test('it fails validation when "created_at" is not a string', () => { + const payload: Omit & { created_at: number } = { ...getCommentsMock(), created_at: 1, }; - const decoded = comments.decode(payload); + const decoded = comment.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -82,12 +105,12 @@ describe('Comments', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "created_by" is not a string', () => { - const payload: Omit & { created_by: number } = { + test('it fails validation when "created_by" is not a string', () => { + const payload: Omit & { created_by: number } = { ...getCommentsMock(), created_by: 1, }; - const decoded = comments.decode(payload); + const decoded = comment.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -96,12 +119,12 @@ describe('Comments', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "updated_at" is not a string', () => { - const payload: Omit & { updated_at: number } = { + test('it fails validation when "updated_at" is not a string', () => { + const payload: Omit & { updated_at: number } = { ...getCommentsMock(), updated_at: 1, }; - const decoded = comments.decode(payload); + const decoded = comment.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -110,12 +133,12 @@ describe('Comments', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "updated_by" is not a string', () => { - const payload: Omit & { updated_by: number } = { + test('it fails validation when "updated_by" is not a string', () => { + const payload: Omit & { updated_by: number } = { ...getCommentsMock(), updated_by: 1, }; - const decoded = comments.decode(payload); + const decoded = comment.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -125,11 +148,11 @@ describe('Comments', () => { }); test('it should strip out extra keys', () => { - const payload: Comments & { + const payload: Comment & { extraKey?: string; } = getCommentsMock(); payload.extraKey = 'some value'; - const decoded = comments.decode(payload); + const decoded = comment.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -138,7 +161,7 @@ describe('Comments', () => { }); describe('commentsArray', () => { - test('it should validate an array of comments', () => { + test('it passes validation an array of Comment', () => { const payload = getCommentsArrayMock(); const decoded = commentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -147,7 +170,7 @@ describe('Comments', () => { expect(message.schema).toEqual(payload); }); - test('it should validate when a comments includes "updated_at" and "updated_by"', () => { + test('it passes validation when a Comment includes "updated_at" and "updated_by"', () => { const commentsPayload = getCommentsMock(); commentsPayload.updated_at = DATE_NOW; commentsPayload.updated_by = 'someone'; @@ -159,32 +182,32 @@ describe('Comments', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate when undefined', () => { + test('it fails validation when undefined', () => { const payload = undefined; const decoded = commentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + 'Invalid value "undefined" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', ]); expect(message.schema).toEqual({}); }); - test('it should not validate when array includes non comments types', () => { + test('it fails validation when array includes non Comment types', () => { const payload = ([1] as unknown) as CommentsArray; const decoded = commentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"', - 'Invalid value "1" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', ]); expect(message.schema).toEqual({}); }); }); describe('commentsArrayOrUndefined', () => { - test('it should validate an array of comments', () => { + test('it passes validation an array of Comment', () => { const payload = getCommentsArrayMock(); const decoded = commentsArrayOrUndefined.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -193,7 +216,7 @@ describe('Comments', () => { expect(message.schema).toEqual(payload); }); - test('it should validate when undefined', () => { + test('it passes validation when undefined', () => { const payload = undefined; const decoded = commentsArrayOrUndefined.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -202,14 +225,14 @@ describe('Comments', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate when array includes non comments types', () => { + test('it fails validation when array includes non Comment types', () => { const payload = ([1] as unknown) as CommentsArrayOrUndefined; const decoded = commentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"', - 'Invalid value "1" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', ]); expect(message.schema).toEqual({}); }); diff --git a/x-pack/plugins/lists/common/schemas/types/comments.ts b/x-pack/plugins/lists/common/schemas/types/comment.ts similarity index 56% rename from x-pack/plugins/lists/common/schemas/types/comments.ts rename to x-pack/plugins/lists/common/schemas/types/comment.ts index 0ee3b05c8102f..6b0b0166b9ee1 100644 --- a/x-pack/plugins/lists/common/schemas/types/comments.ts +++ b/x-pack/plugins/lists/common/schemas/types/comment.ts @@ -3,26 +3,33 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +/* eslint-disable @typescript-eslint/camelcase */ + import * as t from 'io-ts'; -export const comments = t.intersection([ +import { NonEmptyString } from '../../siem_common_deps'; +import { created_at, created_by, id, updated_at, updated_by } from '../common/schemas'; + +export const comment = t.intersection([ t.exact( t.type({ - comment: t.string, - created_at: t.string, // TODO: Make this into an ISO Date string check, - created_by: t.string, + comment: NonEmptyString, + created_at, + created_by, + id, }) ), t.exact( t.partial({ - updated_at: t.string, - updated_by: t.string, + updated_at, + updated_by, }) ), ]); -export const commentsArray = t.array(comments); +export const commentsArray = t.array(comment); export type CommentsArray = t.TypeOf; -export type Comments = t.TypeOf; +export type Comment = t.TypeOf; export const commentsArrayOrUndefined = t.union([commentsArray, t.undefined]); export type CommentsArrayOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/create_comments.mock.ts b/x-pack/plugins/lists/common/schemas/types/create_comment.mock.ts similarity index 73% rename from x-pack/plugins/lists/common/schemas/types/create_comments.mock.ts rename to x-pack/plugins/lists/common/schemas/types/create_comment.mock.ts index 60a59432275ca..689d4ccdc2c2e 100644 --- a/x-pack/plugins/lists/common/schemas/types/create_comments.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/create_comment.mock.ts @@ -3,9 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { CreateComments, CreateCommentsArray } from './create_comments'; +import { CreateComment, CreateCommentsArray } from './create_comment'; -export const getCreateCommentsMock = (): CreateComments => ({ +export const getCreateCommentsMock = (): CreateComment => ({ comment: 'some comments', }); diff --git a/x-pack/plugins/lists/common/schemas/types/create_comments.test.ts b/x-pack/plugins/lists/common/schemas/types/create_comment.test.ts similarity index 72% rename from x-pack/plugins/lists/common/schemas/types/create_comments.test.ts rename to x-pack/plugins/lists/common/schemas/types/create_comment.test.ts index d2680750e05e4..366bf84d48bbf 100644 --- a/x-pack/plugins/lists/common/schemas/types/create_comments.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/create_comment.test.ts @@ -9,44 +9,44 @@ import { left } from 'fp-ts/lib/Either'; import { foldLeftRight, getPaths } from '../../siem_common_deps'; -import { getCreateCommentsArrayMock, getCreateCommentsMock } from './create_comments.mock'; +import { getCreateCommentsArrayMock, getCreateCommentsMock } from './create_comment.mock'; import { - CreateComments, + CreateComment, CreateCommentsArray, CreateCommentsArrayOrUndefined, - createComments, + createComment, createCommentsArray, createCommentsArrayOrUndefined, -} from './create_comments'; +} from './create_comment'; -describe('CreateComments', () => { - describe('createComments', () => { - test('it should validate a comments', () => { +describe('CreateComment', () => { + describe('createComment', () => { + test('it passes validation with a default comment', () => { const payload = getCreateCommentsMock(); - const decoded = createComments.decode(payload); + const decoded = createComment.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); expect(message.schema).toEqual(payload); }); - test('it should not validate when undefined', () => { + test('it fails validation when undefined', () => { const payload = undefined; - const decoded = createComments.decode(payload); + const decoded = createComment.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "{| comment: string |}"', + 'Invalid value "undefined" supplied to "{| comment: NonEmptyString |}"', ]); expect(message.schema).toEqual({}); }); - test('it should not validate when "comment" is not a string', () => { - const payload: Omit & { comment: string[] } = { + test('it fails validation when "comment" is not a string', () => { + const payload: Omit & { comment: string[] } = { ...getCreateCommentsMock(), comment: ['some value'], }; - const decoded = createComments.decode(payload); + const decoded = createComment.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -56,11 +56,11 @@ describe('CreateComments', () => { }); test('it should strip out extra keys', () => { - const payload: CreateComments & { + const payload: CreateComment & { extraKey?: string; } = getCreateCommentsMock(); payload.extraKey = 'some value'; - const decoded = createComments.decode(payload); + const decoded = createComment.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -69,7 +69,7 @@ describe('CreateComments', () => { }); describe('createCommentsArray', () => { - test('it should validate an array of comments', () => { + test('it passes validation an array of comments', () => { const payload = getCreateCommentsArrayMock(); const decoded = createCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -78,31 +78,31 @@ describe('CreateComments', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate when undefined', () => { + test('it fails validation when undefined', () => { const payload = undefined; const decoded = createCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "Array<{| comment: string |}>"', + 'Invalid value "undefined" supplied to "Array<{| comment: NonEmptyString |}>"', ]); expect(message.schema).toEqual({}); }); - test('it should not validate when array includes non comments types', () => { + test('it fails validation when array includes non comments types', () => { const payload = ([1] as unknown) as CreateCommentsArray; const decoded = createCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<{| comment: string |}>"', + 'Invalid value "1" supplied to "Array<{| comment: NonEmptyString |}>"', ]); expect(message.schema).toEqual({}); }); }); describe('createCommentsArrayOrUndefined', () => { - test('it should validate an array of comments', () => { + test('it passes validation an array of comments', () => { const payload = getCreateCommentsArrayMock(); const decoded = createCommentsArrayOrUndefined.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -111,7 +111,7 @@ describe('CreateComments', () => { expect(message.schema).toEqual(payload); }); - test('it should validate when undefined', () => { + test('it passes validation when undefined', () => { const payload = undefined; const decoded = createCommentsArrayOrUndefined.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -120,13 +120,13 @@ describe('CreateComments', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate when array includes non comments types', () => { + test('it fails validation when array includes non comments types', () => { const payload = ([1] as unknown) as CreateCommentsArrayOrUndefined; const decoded = createCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<{| comment: string |}>"', + 'Invalid value "1" supplied to "Array<{| comment: NonEmptyString |}>"', ]); expect(message.schema).toEqual({}); }); diff --git a/x-pack/plugins/lists/common/schemas/types/create_comments.ts b/x-pack/plugins/lists/common/schemas/types/create_comment.ts similarity index 64% rename from x-pack/plugins/lists/common/schemas/types/create_comments.ts rename to x-pack/plugins/lists/common/schemas/types/create_comment.ts index c34419298ef93..fd33313430ce6 100644 --- a/x-pack/plugins/lists/common/schemas/types/create_comments.ts +++ b/x-pack/plugins/lists/common/schemas/types/create_comment.ts @@ -5,14 +5,17 @@ */ import * as t from 'io-ts'; -export const createComments = t.exact( +import { NonEmptyString } from '../../siem_common_deps'; + +export const createComment = t.exact( t.type({ - comment: t.string, + comment: NonEmptyString, }) ); -export const createCommentsArray = t.array(createComments); +export type CreateComment = t.TypeOf; +export const createCommentsArray = t.array(createComment); export type CreateCommentsArray = t.TypeOf; -export type CreateComments = t.TypeOf; +export type CreateComments = t.TypeOf; export const createCommentsArrayOrUndefined = t.union([createCommentsArray, t.undefined]); export type CreateCommentsArrayOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts index 3a4241aaec82d..541b8ab1c799c 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts @@ -10,11 +10,11 @@ import { left } from 'fp-ts/lib/Either'; import { foldLeftRight, getPaths } from '../../siem_common_deps'; import { DefaultCommentsArray } from './default_comments_array'; -import { CommentsArray } from './comments'; -import { getCommentsArrayMock } from './comments.mock'; +import { CommentsArray } from './comment'; +import { getCommentsArrayMock } from './comment.mock'; describe('default_comments_array', () => { - test('it should validate an empty array', () => { + test('it should pass validation when supplied an empty array', () => { const payload: CommentsArray = []; const decoded = DefaultCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('default_comments_array', () => { expect(message.schema).toEqual(payload); }); - test('it should validate an array of comments', () => { + test('it should pass validation when supplied an array of comments', () => { const payload: CommentsArray = getCommentsArrayMock(); const decoded = DefaultCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -32,27 +32,26 @@ describe('default_comments_array', () => { expect(message.schema).toEqual(payload); }); - test('it should NOT validate an array of numbers', () => { + test('it should fail validation when supplied an array of numbers', () => { const payload = [1]; const decoded = DefaultCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); - // TODO: Known weird error formatting that is on our list to address expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"', - 'Invalid value "1" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', ]); expect(message.schema).toEqual({}); }); - test('it should NOT validate an array of strings', () => { + test('it should fail validation when supplied an array of strings', () => { const payload = ['some string']; const decoded = DefaultCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some string" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"', - 'Invalid value "some string" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + 'Invalid value "some string" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + 'Invalid value "some string" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', ]); expect(message.schema).toEqual({}); }); diff --git a/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts b/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts index 342cf8b0d7091..0d7e28e69cf71 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; -import { CommentsArray, comments } from './comments'; +import { CommentsArray, comment } from './comment'; /** * Types the DefaultCommentsArray as: @@ -15,8 +15,8 @@ import { CommentsArray, comments } from './comments'; */ export const DefaultCommentsArray = new t.Type( 'DefaultCommentsArray', - t.array(comments).is, + t.array(comment).is, (input): Either => - input == null ? t.success([]) : t.array(comments).decode(input), + input == null ? t.success([]) : t.array(comment).decode(input), t.identity ); diff --git a/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts index f5ef7d0ad96bd..eb960b5411904 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts @@ -10,11 +10,12 @@ import { left } from 'fp-ts/lib/Either'; import { foldLeftRight, getPaths } from '../../siem_common_deps'; import { DefaultCreateCommentsArray } from './default_create_comments_array'; -import { CreateCommentsArray } from './create_comments'; -import { getCreateCommentsArrayMock } from './create_comments.mock'; +import { CreateCommentsArray } from './create_comment'; +import { getCreateCommentsArrayMock } from './create_comment.mock'; +import { getCommentsArrayMock } from './comment.mock'; describe('default_create_comments_array', () => { - test('it should validate an empty array', () => { + test('it should pass validation when an empty array', () => { const payload: CreateCommentsArray = []; const decoded = DefaultCreateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +24,7 @@ describe('default_create_comments_array', () => { expect(message.schema).toEqual(payload); }); - test('it should validate an array of comments', () => { + test('it should pass validation when an array of comments', () => { const payload: CreateCommentsArray = getCreateCommentsArrayMock(); const decoded = DefaultCreateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -32,25 +33,38 @@ describe('default_create_comments_array', () => { expect(message.schema).toEqual(payload); }); - test('it should NOT validate an array of numbers', () => { + test('it should strip out "created_at" and "created_by" if they are passed in', () => { + const payload = getCommentsArrayMock(); + const decoded = DefaultCreateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + // TODO: Known weird error formatting that is on our list to address + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([ + { comment: 'some old comment' }, + { comment: 'some old comment' }, + ]); + }); + + test('it should not pass validation when an array of numbers', () => { const payload = [1]; const decoded = DefaultCreateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); // TODO: Known weird error formatting that is on our list to address expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<{| comment: string |}>"', + 'Invalid value "1" supplied to "Array<{| comment: NonEmptyString |}>"', ]); expect(message.schema).toEqual({}); }); - test('it should NOT validate an array of strings', () => { + test('it should not pass validation when an array of strings', () => { const payload = ['some string']; const decoded = DefaultCreateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some string" supplied to "Array<{| comment: string |}>"', + 'Invalid value "some string" supplied to "Array<{| comment: NonEmptyString |}>"', ]); expect(message.schema).toEqual({}); }); diff --git a/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.ts b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.ts index 7fd79782836e3..4df888ba728fb 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; -import { CreateCommentsArray, createComments } from './create_comments'; +import { CreateCommentsArray, createComment } from './create_comment'; /** * Types the DefaultCreateComments as: @@ -19,8 +19,8 @@ export const DefaultCreateCommentsArray = new t.Type< unknown >( 'DefaultCreateComments', - t.array(createComments).is, + t.array(createComment).is, (input): Either => - input == null ? t.success([]) : t.array(createComments).decode(input), + input == null ? t.success([]) : t.array(createComment).decode(input), t.identity ); diff --git a/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts index b023e73cb9328..612148dc4ccab 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts @@ -10,11 +10,11 @@ import { left } from 'fp-ts/lib/Either'; import { foldLeftRight, getPaths } from '../../siem_common_deps'; import { DefaultUpdateCommentsArray } from './default_update_comments_array'; -import { UpdateCommentsArray } from './update_comments'; -import { getUpdateCommentsArrayMock } from './update_comments.mock'; +import { UpdateCommentsArray } from './update_comment'; +import { getUpdateCommentsArrayMock } from './update_comment.mock'; describe('default_update_comments_array', () => { - test('it should validate an empty array', () => { + test('it should pass validation when supplied an empty array', () => { const payload: UpdateCommentsArray = []; const decoded = DefaultUpdateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('default_update_comments_array', () => { expect(message.schema).toEqual(payload); }); - test('it should validate an array of comments', () => { + test('it should pass validation when supplied an array of comments', () => { const payload: UpdateCommentsArray = getUpdateCommentsArrayMock(); const decoded = DefaultUpdateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -32,29 +32,26 @@ describe('default_update_comments_array', () => { expect(message.schema).toEqual(payload); }); - test('it should NOT validate an array of numbers', () => { + test('it should fail validation when supplied an array of numbers', () => { const payload = [1]; const decoded = DefaultUpdateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); - // TODO: Known weird error formatting that is on our list to address expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', - 'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', - 'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', ]); expect(message.schema).toEqual({}); }); - test('it should NOT validate an array of strings', () => { + test('it should fail validation when supplied an array of strings', () => { const payload = ['some string']; const decoded = DefaultUpdateCommentsArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some string" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', - 'Invalid value "some string" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', - 'Invalid value "some string" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', + 'Invalid value "some string" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', + 'Invalid value "some string" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', ]); expect(message.schema).toEqual({}); }); diff --git a/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.ts b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.ts index 854b7cf7ada7e..35338dae64387 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; -import { UpdateCommentsArray, updateCommentsArray } from './update_comments'; +import { UpdateCommentsArray, updateCommentsArray } from './update_comment'; /** * Types the DefaultCommentsUpdate as: diff --git a/x-pack/plugins/lists/common/schemas/types/index.ts b/x-pack/plugins/lists/common/schemas/types/index.ts index 463f7cfe51ce3..6b7e9fd17a1af 100644 --- a/x-pack/plugins/lists/common/schemas/types/index.ts +++ b/x-pack/plugins/lists/common/schemas/types/index.ts @@ -3,9 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -export * from './comments'; -export * from './create_comments'; -export * from './update_comments'; +export * from './comment'; +export * from './create_comment'; +export * from './update_comment'; export * from './default_comments_array'; export * from './default_create_comments_array'; export * from './default_update_comments_array'; diff --git a/x-pack/plugins/lists/common/schemas/types/update_comments.mock.ts b/x-pack/plugins/lists/common/schemas/types/update_comment.mock.ts similarity index 54% rename from x-pack/plugins/lists/common/schemas/types/update_comments.mock.ts rename to x-pack/plugins/lists/common/schemas/types/update_comment.mock.ts index 3e963c2607dc5..9b85a24abe40b 100644 --- a/x-pack/plugins/lists/common/schemas/types/update_comments.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/update_comment.mock.ts @@ -4,11 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getCommentsMock } from './comments.mock'; -import { getCreateCommentsMock } from './create_comments.mock'; -import { UpdateCommentsArray } from './update_comments'; +import { ID } from '../../constants.mock'; + +import { UpdateComment, UpdateCommentsArray } from './update_comment'; + +export const getUpdateCommentMock = (): UpdateComment => ({ + comment: 'some comment', + id: ID, +}); export const getUpdateCommentsArrayMock = (): UpdateCommentsArray => [ - getCommentsMock(), - getCreateCommentsMock(), + getUpdateCommentMock(), + getUpdateCommentMock(), ]; diff --git a/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts b/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts new file mode 100644 index 0000000000000..ac7716af40966 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts @@ -0,0 +1,150 @@ +/* + * 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 { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; + +import { foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { getUpdateCommentMock, getUpdateCommentsArrayMock } from './update_comment.mock'; +import { + UpdateComment, + UpdateCommentsArray, + UpdateCommentsArrayOrUndefined, + updateComment, + updateCommentsArray, + updateCommentsArrayOrUndefined, +} from './update_comment'; + +describe('CommentsUpdate', () => { + describe('updateComment', () => { + test('it should pass validation when supplied typical comment update', () => { + const payload = getUpdateCommentMock(); + const decoded = updateComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should fail validation when supplied an undefined for "comment"', () => { + const payload = getUpdateCommentMock(); + delete payload.comment; + const decoded = updateComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "comment"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should fail validation when supplied an empty string for "comment"', () => { + const payload = { ...getUpdateCommentMock(), comment: '' }; + const decoded = updateComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "comment"']); + expect(message.schema).toEqual({}); + }); + + test('it should pass validation when supplied an undefined for "id"', () => { + const payload = getUpdateCommentMock(); + delete payload.id; + const decoded = updateComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should fail validation when supplied an empty string for "id"', () => { + const payload = { ...getUpdateCommentMock(), id: '' }; + const decoded = updateComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "id"']); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra key passed in', () => { + const payload: UpdateComment & { + extraKey?: string; + } = { ...getUpdateCommentMock(), extraKey: 'some new value' }; + const decoded = updateComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getUpdateCommentMock()); + }); + }); + + describe('updateCommentsArray', () => { + test('it should pass validation when supplied an array of comments', () => { + const payload = getUpdateCommentsArrayMock(); + const decoded = updateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should fail validation when undefined', () => { + const payload = undefined; + const decoded = updateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should fail validation when array includes non comments types', () => { + const payload = ([1] as unknown) as UpdateCommentsArray; + const decoded = updateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('updateCommentsArrayOrUndefined', () => { + test('it should pass validation when supplied an array of comments', () => { + const payload = getUpdateCommentsArrayMock(); + const decoded = updateCommentsArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should pass validation when supplied when undefined', () => { + const payload = undefined; + const decoded = updateCommentsArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should fail validation when array includes non comments types', () => { + const payload = ([1] as unknown) as UpdateCommentsArrayOrUndefined; + const decoded = updateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/types/update_comments.ts b/x-pack/plugins/lists/common/schemas/types/update_comment.ts similarity index 58% rename from x-pack/plugins/lists/common/schemas/types/update_comments.ts rename to x-pack/plugins/lists/common/schemas/types/update_comment.ts index 4a21bfa363d45..b95812cb35bf9 100644 --- a/x-pack/plugins/lists/common/schemas/types/update_comments.ts +++ b/x-pack/plugins/lists/common/schemas/types/update_comment.ts @@ -5,10 +5,24 @@ */ import * as t from 'io-ts'; -import { comments } from './comments'; -import { createComments } from './create_comments'; +import { NonEmptyString } from '../../siem_common_deps'; +import { id } from '../common/schemas'; -export const updateCommentsArray = t.array(t.union([comments, createComments])); +export const updateComment = t.intersection([ + t.exact( + t.type({ + comment: NonEmptyString, + }) + ), + t.exact( + t.partial({ + id, + }) + ), +]); + +export type UpdateComment = t.TypeOf; +export const updateCommentsArray = t.array(updateComment); export type UpdateCommentsArray = t.TypeOf; export const updateCommentsArrayOrUndefined = t.union([updateCommentsArray, t.undefined]); export type UpdateCommentsArrayOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/update_comments.test.ts b/x-pack/plugins/lists/common/schemas/types/update_comments.test.ts deleted file mode 100644 index 7668504b031b5..0000000000000 --- a/x-pack/plugins/lists/common/schemas/types/update_comments.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../siem_common_deps'; - -import { getUpdateCommentsArrayMock } from './update_comments.mock'; -import { - UpdateCommentsArray, - UpdateCommentsArrayOrUndefined, - updateCommentsArray, - updateCommentsArrayOrUndefined, -} from './update_comments'; -import { getCommentsMock } from './comments.mock'; -import { getCreateCommentsMock } from './create_comments.mock'; - -describe('CommentsUpdate', () => { - describe('updateCommentsArray', () => { - test('it should validate an array of comments', () => { - const payload = getUpdateCommentsArrayMock(); - const decoded = updateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of existing comments', () => { - const payload = [getCommentsMock()]; - const decoded = updateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of new comments', () => { - const payload = [getCreateCommentsMock()]; - const decoded = updateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate when undefined', () => { - const payload = undefined; - const decoded = updateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate when array includes non comments types', () => { - const payload = ([1] as unknown) as UpdateCommentsArray; - const decoded = updateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', - 'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', - 'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', - ]); - expect(message.schema).toEqual({}); - }); - }); - - describe('updateCommentsArrayOrUndefined', () => { - test('it should validate an array of comments', () => { - const payload = getUpdateCommentsArrayMock(); - const decoded = updateCommentsArrayOrUndefined.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate when undefined', () => { - const payload = undefined; - const decoded = updateCommentsArrayOrUndefined.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate when array includes non comments types', () => { - const payload = ([1] as unknown) as UpdateCommentsArrayOrUndefined; - const decoded = updateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', - 'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', - 'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"', - ]); - expect(message.schema).toEqual({}); - }); - }); -}); diff --git a/x-pack/plugins/lists/common/shared_exports.ts b/x-pack/plugins/lists/common/shared_exports.ts index dc0a9aa5926ef..1f6c65919b063 100644 --- a/x-pack/plugins/lists/common/shared_exports.ts +++ b/x-pack/plugins/lists/common/shared_exports.ts @@ -8,8 +8,8 @@ export { ListSchema, CommentsArray, CreateCommentsArray, - Comments, - CreateComments, + Comment, + CreateComment, ExceptionListSchema, ExceptionListItemSchema, CreateExceptionListSchema, @@ -28,6 +28,7 @@ export { OperatorType, OperatorTypeEnum, ExceptionListTypeEnum, + comment, exceptionListItemSchema, exceptionListType, createExceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts index 293435b3f6202..f5e0e7ae75700 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts @@ -14,6 +14,7 @@ import { exceptionListItemSchema, updateExceptionListItemSchema, } from '../../common/schemas'; +import { updateExceptionListItemValidate } from '../../common/schemas/request/update_exception_list_item_validation'; import { getExceptionListClient } from '.'; @@ -33,6 +34,11 @@ export const updateExceptionListItemRoute = (router: IRouter): void => { }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); + const validationErrors = updateExceptionListItemValidate(request.body); + if (validationErrors.length) { + return siemResponse.error({ body: validationErrors, statusCode: 400 }); + } + try { const { description, diff --git a/x-pack/plugins/lists/server/saved_objects/exception_list.ts b/x-pack/plugins/lists/server/saved_objects/exception_list.ts index 3bde3545837cf..f9e408833e069 100644 --- a/x-pack/plugins/lists/server/saved_objects/exception_list.ts +++ b/x-pack/plugins/lists/server/saved_objects/exception_list.ts @@ -83,6 +83,9 @@ export const exceptionListItemMapping: SavedObjectsType['mappings'] = { created_by: { type: 'keyword', }, + id: { + type: 'keyword', + }, updated_at: { type: 'keyword', }, diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_item.json b/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_item.json index da345fb930c04..81db909277595 100644 --- a/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_item.json +++ b/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_item.json @@ -1,17 +1,18 @@ { - "item_id": "simple_list_item", - "_tags": ["endpoint", "process", "malware", "os:windows"], - "tags": ["user added string for a tag", "malware"], - "type": "simple", - "description": "This is a sample change here this list", - "name": "Sample Endpoint Exception List update change", - "comments": [{ "comment": "this is a newly added comment" }], + "_tags": ["detection"], + "comments": [], + "description": "Test comments - exception list item", "entries": [ { - "field": "event.category", - "operator": "included", - "type": "match_any", - "value": ["process", "malware"] + "field": "host.name", + "type": "match", + "value": "rock01", + "operator": "included" } - ] + ], + "item_id": "993f43f7-325d-4df3-9338-964e77c37053", + "name": "Test comments - exception list item", + "namespace_type": "single", + "tags": [], + "type": "simple" } diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts index a90ec61aef4af..47c21735b45f4 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts @@ -64,7 +64,10 @@ export const createExceptionListItem = async ({ }: CreateExceptionListItemOptions): Promise => { const savedObjectType = getSavedObjectType({ namespaceType }); const dateNow = new Date().toISOString(); - const transformedComments = transformCreateCommentsToComments({ comments, user }); + const transformedComments = transformCreateCommentsToComments({ + incomingComments: comments, + user, + }); const savedObject = await savedObjectsClient.create(savedObjectType, { _tags, comments: transformedComments, diff --git a/x-pack/plugins/lists/server/services/exception_lists/utils.test.ts b/x-pack/plugins/lists/server/services/exception_lists/utils.test.ts index 6f0c5195f2025..e3d96a9c3f6d0 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/utils.test.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/utils.test.ts @@ -5,15 +5,11 @@ */ import sinon from 'sinon'; import moment from 'moment'; +import uuid from 'uuid'; -import { USER } from '../../../common/constants.mock'; +import { transformCreateCommentsToComments, transformUpdateCommentsToComments } from './utils'; -import { - isCommentEqual, - transformCreateCommentsToComments, - transformUpdateComments, - transformUpdateCommentsToComments, -} from './utils'; +jest.mock('uuid/v4'); describe('utils', () => { const oldDate = '2020-03-17T20:34:51.337Z'; @@ -22,59 +18,43 @@ describe('utils', () => { let clock: sinon.SinonFakeTimers; beforeEach(() => { + ((uuid.v4 as unknown) as jest.Mock) + .mockImplementationOnce(() => '123') + .mockImplementationOnce(() => '456'); + clock = sinon.useFakeTimers(unix); }); afterEach(() => { clock.restore(); + jest.clearAllMocks(); + jest.restoreAllMocks(); + jest.resetAllMocks(); }); describe('#transformUpdateCommentsToComments', () => { - test('it returns empty array if "comments" is undefined and no comments exist', () => { + test('it formats new comments', () => { const comments = transformUpdateCommentsToComments({ - comments: undefined, + comments: [{ comment: 'Im a new comment' }], existingComments: [], user: 'lily', }); - expect(comments).toEqual([]); - }); - - test('it formats newly added comments', () => { - const comments = transformUpdateCommentsToComments({ - comments: [ - { comment: 'Im an old comment', created_at: oldDate, created_by: 'bane' }, - { comment: 'Im a new comment' }, - ], - existingComments: [ - { comment: 'Im an old comment', created_at: oldDate, created_by: 'bane' }, - ], - user: 'lily', - }); - expect(comments).toEqual([ - { - comment: 'Im an old comment', - created_at: oldDate, - created_by: 'bane', - }, { comment: 'Im a new comment', created_at: dateNow, created_by: 'lily', + id: '123', }, ]); }); - test('it formats multiple newly added comments', () => { + test('it formats new comments and preserves existing comments', () => { const comments = transformUpdateCommentsToComments({ - comments: [ - { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, - { comment: 'Im a new comment' }, - { comment: 'Im another new comment' }, - ], + comments: [{ comment: 'Im an old comment', id: '1' }, { comment: 'Im a new comment' }], existingComments: [ - { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, + { comment: 'Im an old comment', created_at: oldDate, created_by: 'bane', id: '1' }, ], user: 'lily', }); @@ -83,26 +63,23 @@ describe('utils', () => { { comment: 'Im an old comment', created_at: oldDate, - created_by: 'lily', + created_by: 'bane', + id: '1', }, { comment: 'Im a new comment', created_at: dateNow, created_by: 'lily', - }, - { - comment: 'Im another new comment', - created_at: dateNow, - created_by: 'lily', + id: '123', }, ]); }); - test('it should not throw if comments match existing comments', () => { + test('it returns existing comments if empty array passed for "comments"', () => { const comments = transformUpdateCommentsToComments({ - comments: [{ comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }], + comments: [], existingComments: [ - { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, + { comment: 'Im an old comment', created_at: oldDate, created_by: 'bane', id: '1' }, ], user: 'lily', }); @@ -111,170 +88,42 @@ describe('utils', () => { { comment: 'Im an old comment', created_at: oldDate, - created_by: 'lily', + created_by: 'bane', + id: '1', }, ]); }); - test('it does not throw if user tries to update one of their own existing comments', () => { + test('it acts as append only, only modifying new comments', () => { const comments = transformUpdateCommentsToComments({ - comments: [ - { - comment: 'Im an old comment that is trying to be updated', - created_at: oldDate, - created_by: 'lily', - }, - ], + comments: [{ comment: 'Im a new comment' }], existingComments: [ - { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, + { comment: 'Im an old comment', created_at: oldDate, created_by: 'bane', id: '1' }, ], user: 'lily', }); expect(comments).toEqual([ { - comment: 'Im an old comment that is trying to be updated', + comment: 'Im an old comment', created_at: oldDate, + created_by: 'bane', + id: '1', + }, + { + comment: 'Im a new comment', + created_at: dateNow, created_by: 'lily', - updated_at: dateNow, - updated_by: 'lily', + id: '123', }, ]); }); - - test('it throws an error if user tries to update their comment, without passing in the "created_at" and "created_by" properties', () => { - expect(() => - transformUpdateCommentsToComments({ - comments: [ - { - comment: 'Im an old comment that is trying to be updated', - }, - ], - existingComments: [ - { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, - ], - user: 'lily', - }) - ).toThrowErrorMatchingInlineSnapshot( - `"When trying to update a comment, \\"created_at\\" and \\"created_by\\" must be present"` - ); - }); - - test('it throws an error if user tries to delete comments', () => { - expect(() => - transformUpdateCommentsToComments({ - comments: [], - existingComments: [ - { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, - ], - user: 'lily', - }) - ).toThrowErrorMatchingInlineSnapshot( - `"Comments cannot be deleted, only new comments may be added"` - ); - }); - - test('it throws if user tries to update existing comment timestamp', () => { - expect(() => - transformUpdateCommentsToComments({ - comments: [{ comment: 'Im an old comment', created_at: dateNow, created_by: 'lily' }], - existingComments: [ - { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, - ], - user: 'bane', - }) - ).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit others comments"`); - }); - - test('it throws if user tries to update existing comment author', () => { - expect(() => - transformUpdateCommentsToComments({ - comments: [{ comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }], - existingComments: [ - { comment: 'Im an old comment', created_at: oldDate, created_by: 'me!' }, - ], - user: 'bane', - }) - ).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit others comments"`); - }); - - test('it throws if user tries to update an existing comment that is not their own', () => { - expect(() => - transformUpdateCommentsToComments({ - comments: [ - { - comment: 'Im an old comment that is trying to be updated', - created_at: oldDate, - created_by: 'lily', - }, - ], - existingComments: [ - { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, - ], - user: 'bane', - }) - ).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit others comments"`); - }); - - test('it throws if user tries to update order of comments', () => { - expect(() => - transformUpdateCommentsToComments({ - comments: [ - { comment: 'Im a new comment' }, - { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, - ], - existingComments: [ - { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, - ], - user: 'lily', - }) - ).toThrowErrorMatchingInlineSnapshot( - `"When trying to update a comment, \\"created_at\\" and \\"created_by\\" must be present"` - ); - }); - - test('it throws an error if user tries to add comment formatted as existing comment when none yet exist', () => { - expect(() => - transformUpdateCommentsToComments({ - comments: [ - { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, - { comment: 'Im a new comment' }, - ], - existingComments: [], - user: 'lily', - }) - ).toThrowErrorMatchingInlineSnapshot(`"Only new comments may be added"`); - }); - - test('it throws if empty comment exists', () => { - expect(() => - transformUpdateCommentsToComments({ - comments: [ - { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, - { comment: ' ' }, - ], - existingComments: [ - { comment: 'Im an old comment', created_at: oldDate, created_by: 'lily' }, - ], - user: 'lily', - }) - ).toThrowErrorMatchingInlineSnapshot(`"Empty comments not allowed"`); - }); }); describe('#transformCreateCommentsToComments', () => { - test('it returns "undefined" if "comments" is "undefined"', () => { - const comments = transformCreateCommentsToComments({ - comments: undefined, - user: 'lily', - }); - - expect(comments).toBeUndefined(); - }); - test('it formats newly added comments', () => { const comments = transformCreateCommentsToComments({ - comments: [{ comment: 'Im a new comment' }, { comment: 'Im another new comment' }], + incomingComments: [{ comment: 'Im a new comment' }, { comment: 'Im another new comment' }], user: 'lily', }); @@ -283,178 +132,15 @@ describe('utils', () => { comment: 'Im a new comment', created_at: dateNow, created_by: 'lily', + id: '123', }, { comment: 'Im another new comment', created_at: dateNow, created_by: 'lily', + id: '456', }, ]); }); - - test('it throws an error if user tries to add an empty comment', () => { - expect(() => - transformCreateCommentsToComments({ - comments: [{ comment: ' ' }], - user: 'lily', - }) - ).toThrowErrorMatchingInlineSnapshot(`"Empty comments not allowed"`); - }); - }); - - describe('#transformUpdateComments', () => { - test('it updates comment and adds "updated_at" and "updated_by" if content differs', () => { - const comments = transformUpdateComments({ - comment: { - comment: 'Im an old comment that is trying to be updated', - created_at: oldDate, - created_by: 'lily', - }, - existingComment: { - comment: 'Im an old comment', - created_at: oldDate, - created_by: 'lily', - }, - user: 'lily', - }); - - expect(comments).toEqual({ - comment: 'Im an old comment that is trying to be updated', - created_at: oldDate, - created_by: 'lily', - updated_at: dateNow, - updated_by: 'lily', - }); - }); - - test('it does not update comment and add "updated_at" and "updated_by" if content is the same', () => { - const comments = transformUpdateComments({ - comment: { - comment: 'Im an old comment ', - created_at: oldDate, - created_by: 'lily', - }, - existingComment: { - comment: 'Im an old comment', - created_at: oldDate, - created_by: 'lily', - }, - user: 'lily', - }); - - expect(comments).toEqual({ - comment: 'Im an old comment', - created_at: oldDate, - created_by: 'lily', - }); - }); - - test('it throws if user tries to update an existing comment that is not their own', () => { - expect(() => - transformUpdateComments({ - comment: { - comment: 'Im an old comment that is trying to be updated', - created_at: oldDate, - created_by: 'lily', - }, - existingComment: { - comment: 'Im an old comment', - created_at: oldDate, - created_by: 'lily', - }, - user: 'bane', - }) - ).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit others comments"`); - }); - - test('it throws if user tries to update an existing comments timestamp', () => { - expect(() => - transformUpdateComments({ - comment: { - comment: 'Im an old comment', - created_at: dateNow, - created_by: 'lily', - }, - existingComment: { - comment: 'Im an old comment', - created_at: oldDate, - created_by: 'lily', - }, - user: 'lily', - }) - ).toThrowErrorMatchingInlineSnapshot(`"Unable to update comment"`); - }); - }); - - describe('#isCommentEqual', () => { - test('it returns false if "comment" values differ', () => { - const result = isCommentEqual( - { - comment: 'some old comment', - created_at: oldDate, - created_by: USER, - }, - { - comment: 'some older comment', - created_at: oldDate, - created_by: USER, - } - ); - - expect(result).toBeFalsy(); - }); - - test('it returns false if "created_at" values differ', () => { - const result = isCommentEqual( - { - comment: 'some old comment', - created_at: oldDate, - created_by: USER, - }, - { - comment: 'some old comment', - created_at: dateNow, - created_by: USER, - } - ); - - expect(result).toBeFalsy(); - }); - - test('it returns false if "created_by" values differ', () => { - const result = isCommentEqual( - { - comment: 'some old comment', - created_at: oldDate, - created_by: USER, - }, - { - comment: 'some old comment', - created_at: oldDate, - created_by: 'lily', - } - ); - - expect(result).toBeFalsy(); - }); - - test('it returns true if comment values are equivalent', () => { - const result = isCommentEqual( - { - comment: 'some old comment', - created_at: oldDate, - created_by: USER, - }, - { - created_at: oldDate, - created_by: USER, - // Disabling to assure that order doesn't matter - // eslint-disable-next-line sort-keys - comment: 'some old comment', - } - ); - - expect(result).toBeTruthy(); - }); }); }); diff --git a/x-pack/plugins/lists/server/services/exception_lists/utils.ts b/x-pack/plugins/lists/server/services/exception_lists/utils.ts index b168fae741822..836f642899086 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/utils.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/utils.ts @@ -3,17 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import uuid from 'uuid'; import { SavedObject, SavedObjectsFindResponse, SavedObjectsUpdateResponse } from 'kibana/server'; import { NamespaceTypeArray } from '../../../common/schemas/types/default_namespace_array'; -import { ErrorWithStatusCode } from '../../error_with_status_code'; import { - Comments, CommentsArray, - CommentsArrayOrUndefined, - CreateComments, - CreateCommentsArrayOrUndefined, + CreateComment, + CreateCommentsArray, ExceptionListItemSchema, ExceptionListSchema, ExceptionListSoSchema, @@ -21,7 +18,6 @@ import { FoundExceptionListSchema, NamespaceType, UpdateCommentsArrayOrUndefined, - comments as commentsSchema, exceptionListItemType, exceptionListType, } from '../../../common/schemas'; @@ -296,17 +292,6 @@ export const transformSavedObjectsToFoundExceptionList = ({ }; }; -/* - * Determines whether two comments are equal, this is a very - * naive implementation, not meant to be used for deep equality of complex objects - */ -export const isCommentEqual = (commentA: Comments, commentB: Comments): boolean => { - const a = Object.values(commentA).sort().join(); - const b = Object.values(commentB).sort().join(); - - return a === b; -}; - export const transformUpdateCommentsToComments = ({ comments, existingComments, @@ -316,90 +301,28 @@ export const transformUpdateCommentsToComments = ({ existingComments: CommentsArray; user: string; }): CommentsArray => { - const newComments = comments ?? []; + const incomingComments = comments ?? []; + const newComments = incomingComments.filter((comment) => comment.id == null); + const newCommentsFormatted = transformCreateCommentsToComments({ + incomingComments: newComments, + user, + }); - if (newComments.length < existingComments.length) { - throw new ErrorWithStatusCode( - 'Comments cannot be deleted, only new comments may be added', - 403 - ); - } else { - return newComments.flatMap((c, index) => { - const existingComment = existingComments[index]; - - if (commentsSchema.is(existingComment) && !commentsSchema.is(c)) { - throw new ErrorWithStatusCode( - 'When trying to update a comment, "created_at" and "created_by" must be present', - 403 - ); - } else if (existingComment == null && commentsSchema.is(c)) { - throw new ErrorWithStatusCode('Only new comments may be added', 403); - } else if ( - commentsSchema.is(c) && - existingComment != null && - isCommentEqual(c, existingComment) - ) { - return existingComment; - } else if (commentsSchema.is(c) && existingComment != null) { - return transformUpdateComments({ comment: c, existingComment, user }); - } else { - return transformCreateCommentsToComments({ comments: [c], user }) ?? []; - } - }); - } -}; - -export const transformUpdateComments = ({ - comment, - existingComment, - user, -}: { - comment: Comments; - existingComment: Comments; - user: string; -}): Comments => { - if (comment.created_by !== user) { - // existing comment is being edited, can only be edited by author - throw new ErrorWithStatusCode('Not authorized to edit others comments', 401); - } else if (existingComment.created_at !== comment.created_at) { - throw new ErrorWithStatusCode('Unable to update comment', 403); - } else if (comment.comment.trim().length === 0) { - throw new ErrorWithStatusCode('Empty comments not allowed', 403); - } else if (comment.comment.trim() !== existingComment.comment) { - const dateNow = new Date().toISOString(); - - return { - ...existingComment, - comment: comment.comment, - updated_at: dateNow, - updated_by: user, - }; - } else { - return existingComment; - } + return [...existingComments, ...newCommentsFormatted]; }; export const transformCreateCommentsToComments = ({ - comments, + incomingComments, user, }: { - comments: CreateCommentsArrayOrUndefined; + incomingComments: CreateCommentsArray; user: string; -}): CommentsArrayOrUndefined => { +}): CommentsArray => { const dateNow = new Date().toISOString(); - if (comments != null) { - return comments.map((c: CreateComments) => { - if (c.comment.trim().length === 0) { - throw new ErrorWithStatusCode('Empty comments not allowed', 403); - } else { - return { - comment: c.comment, - created_at: dateNow, - created_by: user, - }; - } - }); - } else { - return comments; - } + return incomingComments.map((comment: CreateComment) => ({ + comment: comment.comment, + created_at: dateNow, + created_by: user, + id: uuid.v4(), + })); }; diff --git a/x-pack/plugins/security_solution/common/shared_imports.ts b/x-pack/plugins/security_solution/common/shared_imports.ts index 7fb94cea7b612..e28d1969b3976 100644 --- a/x-pack/plugins/security_solution/common/shared_imports.ts +++ b/x-pack/plugins/security_solution/common/shared_imports.ts @@ -8,8 +8,8 @@ export { ListSchema, CommentsArray, CreateCommentsArray, - Comments, - CreateComments, + Comment, + CreateComment, ExceptionListSchema, ExceptionListItemSchema, CreateExceptionListSchema, @@ -30,6 +30,7 @@ export { ExceptionListTypeEnum, exceptionListItemSchema, exceptionListType, + comment, createExceptionListItemSchema, listSchema, entry, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx index db2d0540971de..22d14ec6bedb1 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx @@ -16,13 +16,13 @@ import { EuiCommentProps, EuiText, } from '@elastic/eui'; -import { Comments } from '../../../lists_plugin_deps'; +import { Comment } from '../../../shared_imports'; import * as i18n from './translations'; import { useCurrentUser } from '../../lib/kibana'; import { getFormattedComments } from './helpers'; interface AddExceptionCommentsProps { - exceptionItemComments?: Comments[]; + exceptionItemComments?: Comment[]; newCommentValue: string; newCommentOnChange: (value: string) => void; } diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index a4fe52eaacf4e..0f7e5b24ed8f9 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -38,7 +38,7 @@ import { useSignalIndex } from '../../../../detections/containers/detection_engi import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_exception_list'; import { AddExceptionComments } from '../add_exception_comments'; import { - enrichExceptionItemsWithComments, + enrichNewExceptionItemsWithComments, enrichExceptionItemsWithOS, defaultEndpointExceptionItems, entryHasListType, @@ -251,7 +251,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ let enriched: Array = []; enriched = comment !== '' - ? enrichExceptionItemsWithComments(exceptionItemsToAdd, [{ comment }]) + ? enrichNewExceptionItemsWithComments(exceptionItemsToAdd, [{ comment }]) : exceptionItemsToAdd; if (exceptionListType === 'endpoint') { const osTypes = retrieveAlertOsTypes(); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx index 1ec49425ce8fd..734434484fb4c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx @@ -392,7 +392,7 @@ export const ExceptionBuilder = ({ )} { - addError(error, { title: i18n.EDIT_EXCEPTION_ERROR }); - onCancel(); + if (error.message.includes('Conflict')) { + setHasVersionConflict(true); + } else { + addError(error, { title: i18n.EDIT_EXCEPTION_ERROR }); + onCancel(); + } }, [addError, onCancel] ); @@ -147,8 +153,8 @@ export const EditExceptionModal = memo(function EditExceptionModal({ }, [shouldDisableBulkClose]); const isSubmitButtonDisabled = useMemo( - () => exceptionItemsToAdd.every((item) => item.entries.length === 0), - [exceptionItemsToAdd] + () => exceptionItemsToAdd.every((item) => item.entries.length === 0) || hasVersionConflict, + [exceptionItemsToAdd, hasVersionConflict] ); const handleBuilderOnChange = useCallback( @@ -177,11 +183,15 @@ export const EditExceptionModal = memo(function EditExceptionModal({ ); const enrichExceptionItems = useCallback(() => { - let enriched: Array = []; - enriched = enrichExceptionItemsWithComments(exceptionItemsToAdd, [ - ...(exceptionItem.comments ? exceptionItem.comments : []), - ...(comment !== '' ? [{ comment }] : []), - ]); + const [exceptionItemToEdit] = exceptionItemsToAdd; + let enriched: Array = [ + { + ...enrichExistingExceptionItemWithComments(exceptionItemToEdit, [ + ...exceptionItem.comments, + ...(comment.trim() !== '' ? [{ comment }] : []), + ]), + }, + ]; if (exceptionListType === 'endpoint') { const osTypes = exceptionItem._tags ? getOperatingSystems(exceptionItem._tags) : []; enriched = enrichExceptionItemsWithOS(enriched, osTypes); @@ -222,7 +232,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({ listId={exceptionItem.list_id} listNamespaceType={exceptionItem.namespace_type} ruleName={ruleName} - isOrDisabled={false} + isOrDisabled isAndDisabled={false} isNestedDisabled={false} data-test-subj="edit-exception-modal-builder" @@ -263,6 +273,14 @@ export const EditExceptionModal = memo(function EditExceptionModal({ )} + {hasVersionConflict && ( + + +

      {i18n.VERSION_CONFLICT_ERROR_DESCRIPTION}

      +
      +
      + )} + {i18n.CANCEL} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/translations.ts index 6c5cb733b7a73..d09f0158b2e1d 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/translations.ts @@ -67,3 +67,18 @@ export const EXCEPTION_BUILDER_INFO = i18n.translate( defaultMessage: "Alerts are generated when the rule's conditions are met, except when:", } ); + +export const VERSION_CONFLICT_ERROR_TITLE = i18n.translate( + 'xpack.securitySolution.exceptions.editException.versionConflictTitle', + { + defaultMessage: 'Sorry, there was an error', + } +); + +export const VERSION_CONFLICT_ERROR_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.exceptions.editException.versionConflictDescription', + { + defaultMessage: + "It appears this exception was updated since you first selected to edit it. Try clicking 'Cancel' and editing the exception again.", + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 78936d5d0da6f..5cb65ee6db8ff 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -18,7 +18,8 @@ import { formatOperatingSystems, getEntryValue, formatExceptionItemForUpdate, - enrichExceptionItemsWithComments, + enrichNewExceptionItemsWithComments, + enrichExistingExceptionItemWithComments, enrichExceptionItemsWithOS, entryHasListType, entryHasNonEcsType, @@ -35,14 +36,14 @@ import { existsOperator, doesNotExistOperator, } from '../autocomplete/operators'; -import { OperatorTypeEnum, OperatorEnum, EntryNested } from '../../../lists_plugin_deps'; +import { OperatorTypeEnum, OperatorEnum, EntryNested } from '../../../shared_imports'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getEntryMatchMock } from '../../../../../lists/common/schemas/types/entry_match.mock'; import { getEntryMatchAnyMock } from '../../../../../lists/common/schemas/types/entry_match_any.mock'; import { getEntryExistsMock } from '../../../../../lists/common/schemas/types/entry_exists.mock'; import { getEntryListMock } from '../../../../../lists/common/schemas/types/entry_list.mock'; -import { getCommentsArrayMock } from '../../../../../lists/common/schemas/types/comments.mock'; -import { ENTRIES } from '../../../../../lists/common/constants.mock'; +import { getCommentsArrayMock } from '../../../../../lists/common/schemas/types/comment.mock'; +import { ENTRIES, OLD_DATE_RELATIVE_TO_DATE_NOW } from '../../../../../lists/common/constants.mock'; import { CreateExceptionListItemSchema, ExceptionListItemSchema, @@ -410,12 +411,52 @@ describe('Exception helpers', () => { expect(result).toEqual(expected); }); }); + describe('#enrichExistingExceptionItemWithComments', () => { + test('it should return exception item with comments stripped of "created_by", "created_at", "updated_by", "updated_at" fields', () => { + const payload = getExceptionListItemSchemaMock(); + const comments = [ + { + comment: 'Im an existing comment', + created_at: OLD_DATE_RELATIVE_TO_DATE_NOW, + created_by: 'lily', + id: '1', + }, + { + comment: 'Im another existing comment', + created_at: OLD_DATE_RELATIVE_TO_DATE_NOW, + created_by: 'lily', + id: '2', + }, + { + comment: 'Im a new comment', + }, + ]; + const result = enrichExistingExceptionItemWithComments(payload, comments); + const expected = { + ...getExceptionListItemSchemaMock(), + comments: [ + { + comment: 'Im an existing comment', + id: '1', + }, + { + comment: 'Im another existing comment', + id: '2', + }, + { + comment: 'Im a new comment', + }, + ], + }; + expect(result).toEqual(expected); + }); + }); - describe('#enrichExceptionItemsWithComments', () => { + describe('#enrichNewExceptionItemsWithComments', () => { test('it should add comments to an exception item', () => { const payload = [getExceptionListItemSchemaMock()]; const comments = getCommentsArrayMock(); - const result = enrichExceptionItemsWithComments(payload, comments); + const result = enrichNewExceptionItemsWithComments(payload, comments); const expected = [ { ...getExceptionListItemSchemaMock(), @@ -428,7 +469,7 @@ describe('Exception helpers', () => { test('it should add comments to multiple exception items', () => { const payload = [getExceptionListItemSchemaMock(), getExceptionListItemSchemaMock()]; const comments = getCommentsArrayMock(); - const result = enrichExceptionItemsWithComments(payload, comments); + const result = enrichNewExceptionItemsWithComments(payload, comments); const expected = [ { ...getExceptionListItemSchemaMock(), diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index a54f20f56d56f..ee45f9b5de1fa 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -20,13 +20,14 @@ import { EXCEPTION_OPERATORS, isOperator } from '../autocomplete/operators'; import { OperatorOption } from '../autocomplete/types'; import { CommentsArray, - Comments, - CreateComments, + Comment, + CreateComment, Entry, ExceptionListItemSchema, NamespaceType, OperatorTypeEnum, CreateExceptionListItemSchema, + comment, entry, entriesNested, createExceptionListItemSchema, @@ -34,7 +35,7 @@ import { UpdateExceptionListItemSchema, ExceptionListType, EntryNested, -} from '../../../lists_plugin_deps'; +} from '../../../shared_imports'; import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { validate } from '../../../../common/validate'; import { TimelineNonEcsData } from '../../../graphql/types'; @@ -140,16 +141,16 @@ export const getTagsInclude = ({ * @param comments ExceptionItem.comments */ export const getFormattedComments = (comments: CommentsArray): EuiCommentProps[] => - comments.map((comment) => ({ - username: comment.created_by, - timestamp: moment(comment.created_at).format('on MMM Do YYYY @ HH:mm:ss'), + comments.map((commentItem) => ({ + username: commentItem.created_by, + timestamp: moment(commentItem.created_at).format('on MMM Do YYYY @ HH:mm:ss'), event: i18n.COMMENT_EVENT, - timelineIcon: , - children: {comment.comment}, + timelineIcon: , + children: {commentItem.comment}, actions: ( ), @@ -271,11 +272,11 @@ export const prepareExceptionItemsForBulkClose = ( /** * Adds new and existing comments to all new exceptionItems if not present already * @param exceptionItems new or existing ExceptionItem[] - * @param comments new Comments + * @param comments new Comment */ -export const enrichExceptionItemsWithComments = ( +export const enrichNewExceptionItemsWithComments = ( exceptionItems: Array, - comments: Array + comments: Array ): Array => { return exceptionItems.map((item: ExceptionListItemSchema | CreateExceptionListItemSchema) => { return { @@ -285,6 +286,36 @@ export const enrichExceptionItemsWithComments = ( }); }; +/** + * Adds new and existing comments to exceptionItem + * @param exceptionItem existing ExceptionItem + * @param comments array of comments that can include existing + * and new comments + */ +export const enrichExistingExceptionItemWithComments = ( + exceptionItem: ExceptionListItemSchema | CreateExceptionListItemSchema, + comments: Array +): ExceptionListItemSchema | CreateExceptionListItemSchema => { + const formattedComments = comments.map((item) => { + if (comment.is(item)) { + const { id, comment: existingComment } = item; + return { + id, + comment: existingComment, + }; + } else { + return { + comment: item.comment, + }; + } + }); + + return { + ...exceptionItem, + comments: formattedComments, + }; +}; + /** * Adds provided osTypes to all exceptionItems if not present already * @param exceptionItems new or existing ExceptionItem[] diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx index 8df7b51bb9d31..ab6588b67d5ba 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx @@ -12,7 +12,7 @@ import moment from 'moment-timezone'; import { ExceptionDetails } from './exception_details'; import { getExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { getCommentsArrayMock } from '../../../../../../../lists/common/schemas/types/comments.mock'; +import { getCommentsArrayMock } from '../../../../../../../lists/common/schemas/types/comment.mock'; describe('ExceptionDetails', () => { beforeEach(() => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx index 56b029aaee81e..fec7354855935 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx @@ -11,7 +11,7 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { ExceptionItem } from './'; import { getExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { getCommentsArrayMock } from '../../../../../../../lists/common/schemas/types/comments.mock'; +import { getCommentsArrayMock } from '../../../../../../../lists/common/schemas/types/comment.mock'; addDecorator((storyFn) => ( ({ eui: euiLightVars, darkMode: false })}>{storyFn()} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx index 90752f9450e4c..c9def092fda47 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx @@ -11,7 +11,7 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { ExceptionItem } from './'; import { getExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { getCommentsArrayMock } from '../../../../../../../lists/common/schemas/types/comments.mock'; +import { getCommentsArrayMock } from '../../../../../../../lists/common/schemas/types/comment.mock'; jest.mock('../../../../lib/kibana'); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx index 34dc47b9cd411..16eaef4136983 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx @@ -190,7 +190,8 @@ const ExceptionsViewerComponent = ({ const handleOnCancelExceptionModal = useCallback((): void => { setCurrentModal(null); - }, [setCurrentModal]); + handleFetchList(); + }, [setCurrentModal, handleFetchList]); const handleOnConfirmExceptionModal = useCallback((): void => { setCurrentModal(null); From ddff1c9ab9b0a36824ac0fdac97a957827cb8496 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 27 Jul 2020 17:50:46 -0600 Subject: [PATCH 59/96] [Security solution] Threat hunting test coverage improvements (#73276) --- .../components/markdown_editor/index.test.tsx | 49 ++++++ .../components/markdown_editor/index.tsx | 1 - .../navigation/breadcrumbs/index.test.ts | 74 +++++++++ .../utils/timeline/use_show_timeline.test.tsx | 33 ++++ .../components/manage_timeline/index.test.tsx | 145 ++++++++++++++++++ .../components/manage_timeline/index.tsx | 12 +- 6 files changed, 308 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/markdown_editor/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.test.tsx new file mode 100644 index 0000000000000..b5e5b01189418 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.test.tsx @@ -0,0 +1,49 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; + +import { MarkdownEditor } from '.'; +import { TestProviders } from '../../mock'; + +describe('Markdown Editor', () => { + const onChange = jest.fn(); + const onCursorPositionUpdate = jest.fn(); + const defaultProps = { + content: 'hello world', + onChange, + onCursorPositionUpdate, + }; + beforeEach(() => { + jest.clearAllMocks(); + }); + test('it calls onChange with correct value', () => { + const wrapper = mount( + + + + ); + const newValue = 'a new string'; + wrapper + .find(`[data-test-subj="textAreaInput"]`) + .first() + .simulate('change', { target: { value: newValue } }); + expect(onChange).toBeCalledWith(newValue); + }); + test('it calls onCursorPositionUpdate with correct args', () => { + const wrapper = mount( + + + + ); + wrapper.find(`[data-test-subj="textAreaInput"]`).first().simulate('blur'); + expect(onCursorPositionUpdate).toBeCalledWith({ + start: 0, + end: 0, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.tsx index c40b3910ec152..d4ad4a11b60a3 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.tsx @@ -103,7 +103,6 @@ export const MarkdownEditor = React.memo<{ end: e!.target!.selectionEnd ?? 0, }); } - return false; }, [onCursorPositionUpdate] ); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts index 7e508c28c62df..89aa77106933e 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts @@ -36,6 +36,13 @@ const getMockObject = ( ): RouteSpyState & TabNavigationProps => ({ detailName, navTabs: { + case: { + disabled: false, + href: '/app/security/cases', + id: 'case', + name: 'Cases', + urlKey: 'case', + }, hosts: { disabled: false, href: '/app/security/hosts', @@ -227,6 +234,73 @@ describe('Navigation Breadcrumbs', () => { { text: 'Flows', href: '' }, ]); }); + + test('should return Alerts breadcrumbs when supplied detection pathname', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('detections', '/', undefined), + getUrlForAppMock + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: '/app/security/overview' }, + { + text: 'Detections', + href: + "securitySolution:detections?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + }, + ]); + }); + test('should return Cases breadcrumbs when supplied case pathname', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('case', '/', undefined), + getUrlForAppMock + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: '/app/security/overview' }, + { + text: 'Cases', + href: + "securitySolution:case?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + }, + ]); + }); + test('should return Case details breadcrumbs when supplied case details pathname', () => { + const sampleCase = { + id: 'my-case-id', + name: 'Case name', + }; + const breadcrumbs = getBreadcrumbsForRoute( + { + ...getMockObject('case', `/${sampleCase.id}`, sampleCase.id), + state: { caseTitle: sampleCase.name }, + }, + getUrlForAppMock + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: '/app/security/overview' }, + { + text: 'Cases', + href: + "securitySolution:case?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + }, + { + text: sampleCase.name, + href: `securitySolution:case/${sampleCase.id}?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, + }, + ]); + }); + test('should return Admin breadcrumbs when supplied admin pathname', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('administration', '/', undefined), + getUrlForAppMock + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: '/app/security/overview' }, + { + text: 'Administration', + href: 'securitySolution:administration', + }, + ]); + }); }); describe('setBreadcrumbs()', () => { diff --git a/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx b/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx new file mode 100644 index 0000000000000..db6e2536ce558 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx @@ -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. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { useShowTimeline } from './use_show_timeline'; +import { globalNode } from '../../mock'; + +describe('use show timeline', () => { + it('shows timeline for routes on default', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useShowTimeline()); + await waitForNextUpdate(); + const uninitializedTimeline = result.current; + expect(uninitializedTimeline).toEqual([true]); + }); + }); + it('hides timeline for blacklist routes', async () => { + await act(async () => { + Object.defineProperty(globalNode.window, 'location', { + value: { + pathname: `/cases/configure`, + }, + }); + const { result, waitForNextUpdate } = renderHook(() => useShowTimeline()); + await waitForNextUpdate(); + const uninitializedTimeline = result.current; + expect(uninitializedTimeline).toEqual([false]); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx new file mode 100644 index 0000000000000..b918e5abc652b --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx @@ -0,0 +1,145 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { getTimelineDefaults, useTimelineManager, UseTimelineManager } from './'; +import { FilterManager } from '../../../../../../../src/plugins/data/public/query/filter_manager'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { TimelineRowAction } from '../timeline/body/actions'; + +const isStringifiedComparisonEqual = (a: {}, b: {}): boolean => + JSON.stringify(a) === JSON.stringify(b); + +describe('useTimelineManager', () => { + const setupMock = coreMock.createSetup(); + const testId = 'coolness'; + const timelineDefaults = getTimelineDefaults(testId); + const timelineRowActions = () => []; + const mockFilterManager = new FilterManager(setupMock.uiSettings); + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + it('initilizes an undefined timeline', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useTimelineManager() + ); + await waitForNextUpdate(); + const uninitializedTimeline = result.current.getManageTimelineById(testId); + expect(isStringifiedComparisonEqual(uninitializedTimeline, timelineDefaults)).toBeTruthy(); + }); + }); + it('getIndexToAddById', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useTimelineManager() + ); + await waitForNextUpdate(); + const data = result.current.getIndexToAddById(testId); + expect(data).toEqual(timelineDefaults.indexToAdd); + }); + }); + it('setIndexToAdd', async () => { + await act(async () => { + const indexToAddArgs = { id: testId, indexToAdd: ['example'] }; + const { result, waitForNextUpdate } = renderHook(() => + useTimelineManager() + ); + await waitForNextUpdate(); + result.current.initializeTimeline({ + id: testId, + timelineRowActions, + }); + result.current.setIndexToAdd(indexToAddArgs); + const data = result.current.getIndexToAddById(testId); + expect(data).toEqual(indexToAddArgs.indexToAdd); + }); + }); + it('setIsTimelineLoading', async () => { + await act(async () => { + const isLoadingArgs = { id: testId, isLoading: true }; + const { result, waitForNextUpdate } = renderHook(() => + useTimelineManager() + ); + await waitForNextUpdate(); + result.current.initializeTimeline({ + id: testId, + timelineRowActions, + }); + let timeline = result.current.getManageTimelineById(testId); + expect(timeline.isLoading).toBeFalsy(); + result.current.setIsTimelineLoading(isLoadingArgs); + timeline = result.current.getManageTimelineById(testId); + expect(timeline.isLoading).toBeTruthy(); + }); + }); + it('setTimelineRowActions', async () => { + await act(async () => { + const timelineRowActionsEx = () => [ + { id: 'wow', content: 'hey', displayType: 'icon', onClick: () => {} } as TimelineRowAction, + ]; + const { result, waitForNextUpdate } = renderHook(() => + useTimelineManager() + ); + await waitForNextUpdate(); + result.current.initializeTimeline({ + id: testId, + timelineRowActions, + }); + let timeline = result.current.getManageTimelineById(testId); + expect(timeline.timelineRowActions).toEqual(timelineRowActions); + result.current.setTimelineRowActions({ + id: testId, + timelineRowActions: timelineRowActionsEx, + }); + timeline = result.current.getManageTimelineById(testId); + expect(timeline.timelineRowActions).toEqual(timelineRowActionsEx); + }); + }); + it('getTimelineFilterManager undefined on uninitialized', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useTimelineManager() + ); + await waitForNextUpdate(); + const data = result.current.getTimelineFilterManager(testId); + expect(data).toEqual(undefined); + }); + }); + it('getTimelineFilterManager defined at initialize', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useTimelineManager() + ); + await waitForNextUpdate(); + result.current.initializeTimeline({ + id: testId, + timelineRowActions, + filterManager: mockFilterManager, + }); + const data = result.current.getTimelineFilterManager(testId); + expect(data).toEqual(mockFilterManager); + }); + }); + it('isManagedTimeline returns false when unset and then true when set', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useTimelineManager() + ); + await waitForNextUpdate(); + let data = result.current.isManagedTimeline(testId); + expect(data).toBeFalsy(); + result.current.initializeTimeline({ + id: testId, + timelineRowActions, + filterManager: mockFilterManager, + }); + data = result.current.isManagedTimeline(testId); + expect(data).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx index dba8506add0ad..a425f9b49add0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx @@ -137,7 +137,7 @@ const reducerManageTimeline = ( } }; -interface UseTimelineManager { +export interface UseTimelineManager { getIndexToAddById: (id: string) => string[] | null; getManageTimelineById: (id: string) => ManageTimeline; getTimelineFilterManager: (id: string) => FilterManager | undefined; @@ -152,7 +152,9 @@ interface UseTimelineManager { }) => void; } -const useTimelineManager = (manageTimelineForTesting?: ManageTimelineById): UseTimelineManager => { +export const useTimelineManager = ( + manageTimelineForTesting?: ManageTimelineById +): UseTimelineManager => { const [state, dispatch] = useReducer< (state: ManageTimelineById, action: ActionManageTimeline) => ManageTimelineById >(reducerManageTimeline, manageTimelineForTesting ?? initManageTimeline); @@ -241,12 +243,12 @@ const useTimelineManager = (manageTimelineForTesting?: ManageTimelineById): UseT }; const init = { - getManageTimelineById: (id: string) => getTimelineDefaults(id), getIndexToAddById: (id: string) => null, + getManageTimelineById: (id: string) => getTimelineDefaults(id), getTimelineFilterManager: () => undefined, - setIndexToAdd: () => undefined, - isManagedTimeline: () => false, initializeTimeline: () => noop, + isManagedTimeline: () => false, + setIndexToAdd: () => undefined, setIsTimelineLoading: () => noop, setTimelineRowActions: () => noop, }; From ef83e772ca0357932c53dedfbb3ce68dc2361f55 Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Mon, 27 Jul 2020 20:03:23 -0400 Subject: [PATCH 60/96] [Security Solution][Resolver] Show origin node details in panel on load (#73313) * show origin node details in panel on load * added comment Co-authored-by: Elastic Machine --- .../public/resolver/view/map.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/map.tsx b/x-pack/plugins/security_solution/public/resolver/view/map.tsx index 30aa4b63a138d..19c403f1257be 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/map.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/map.tsx @@ -8,7 +8,7 @@ /* eslint-disable react/display-name */ -import React, { useContext } from 'react'; +import React, { useContext, useEffect } from 'react'; import { useSelector } from 'react-redux'; import { useEffectOnce } from 'react-use'; import { EuiLoadingSpinner } from '@elastic/eui'; @@ -68,11 +68,25 @@ export const ResolverMap = React.memo(function ({ const hasError = useSelector(selectors.hasError); const activeDescendantId = useSelector(selectors.ariaActiveDescendant); const { colorMap } = useResolverTheme(); - const { cleanUpQueryParams } = useResolverQueryParams(); + const { + cleanUpQueryParams, + queryParams: { crumbId }, + pushToQueryParams, + } = useResolverQueryParams(); + useEffectOnce(() => { return () => cleanUpQueryParams(); }); + useEffect(() => { + // When you refresh the page after selecting a process in the table view (not the timeline view) + // The old crumbId still exists in the query string even though a resolver is no longer visible + // This just makes sure the activeDescendant and crumbId are in sync on load for that view as well as the timeline + if (activeDescendantId && crumbId !== activeDescendantId) { + pushToQueryParams({ crumbId: activeDescendantId, crumbEvent: '' }); + } + }, [crumbId, activeDescendantId, pushToQueryParams]); + return ( {isLoading ? ( From 8c52d39b9e757471f472a36eea30cdace30fd3ff Mon Sep 17 00:00:00 2001 From: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com> Date: Mon, 27 Jul 2020 20:34:08 -0400 Subject: [PATCH 61/96] [Security Solution] Show proper icon for termination status of all processes (#73235) * Show proper icon for termination status of all processes * Add basic test for isProcessTerminated selector --- .../resolver/store/data/selectors.test.ts | 29 +++++++++ .../public/resolver/store/data/selectors.ts | 13 ++++ .../resolver/store/mocks/endpoint_event.ts | 4 +- .../resolver/store/mocks/resolver_tree.ts | 63 +++++++++++++++++++ .../public/resolver/store/selectors.ts | 8 +++ .../public/resolver/view/panel.tsx | 20 +----- .../panels/panel_content_process_detail.tsx | 17 +++-- .../panels/panel_content_process_list.tsx | 14 ++--- .../view/panels/process_cube_icon.tsx | 4 +- 9 files changed, 131 insertions(+), 41 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts index 9e1c396723a27..0826391a10688 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts @@ -13,6 +13,7 @@ import { mockTreeWithNoAncestorsAnd2Children, mockTreeWith2AncestorsAndNoChildren, mockTreeWith1AncestorAnd2ChildrenAndAllNodesHave2GraphableEvents, + mockTreeWithAllProcessesTerminated, } from '../mocks/resolver_tree'; import { uniquePidForProcess } from '../../models/process_event'; import { EndpointEvent } from '../../../../common/endpoint/types'; @@ -299,6 +300,34 @@ describe('data state', () => { expect(selectors.ariaFlowtoCandidate(state())(secondAncestorID)).toBe(null); }); }); + describe('with a tree with all processes terminated', () => { + const originID = 'c'; + const firstAncestorID = 'b'; + const secondAncestorID = 'a'; + beforeEach(() => { + actions.push({ + type: 'serverReturnedResolverData', + payload: { + result: mockTreeWithAllProcessesTerminated({ + originID, + firstAncestorID, + secondAncestorID, + }), + // this value doesn't matter + databaseDocumentID: '', + }, + }); + }); + it('should have origin as terminated', () => { + expect(selectors.isProcessTerminated(state())(originID)).toBe(true); + }); + it('should have first ancestor as termianted', () => { + expect(selectors.isProcessTerminated(state())(firstAncestorID)).toBe(true); + }); + it('should have second ancestor as terminated', () => { + expect(selectors.isProcessTerminated(state())(secondAncestorID)).toBe(true); + }); + }); describe('with a tree with 2 children and no ancestors', () => { const originID = 'c'; const firstChildID = 'd'; diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index 1d65b406306a3..ea0cb8663d11d 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -105,6 +105,19 @@ export const terminatedProcesses = createSelector(resolverTreeResponse, function ); }); +/** + * A function that given an entity id returns a boolean indicating if the id is in the set of terminated processes. + */ +export const isProcessTerminated = createSelector(terminatedProcesses, function ( + /* eslint-disable no-shadow */ + terminatedProcesses + /* eslint-enable no-shadow */ +) { + return (entityId: string) => { + return terminatedProcesses.has(entityId); + }; +}); + /** * Process events that will be graphed. */ diff --git a/x-pack/plugins/security_solution/public/resolver/store/mocks/endpoint_event.ts b/x-pack/plugins/security_solution/public/resolver/store/mocks/endpoint_event.ts index b58ea73e1fdc7..8f2e0ad3a6d85 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/mocks/endpoint_event.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/mocks/endpoint_event.ts @@ -14,16 +14,18 @@ export function mockEndpointEvent({ name, parentEntityId, timestamp, + lifecycleType, }: { entityID: string; name: string; parentEntityId: string | undefined; timestamp: number; + lifecycleType?: string; }): EndpointEvent { return { '@timestamp': timestamp, event: { - type: 'start', + type: lifecycleType ? lifecycleType : 'start', category: 'process', }, process: { diff --git a/x-pack/plugins/security_solution/public/resolver/store/mocks/resolver_tree.ts b/x-pack/plugins/security_solution/public/resolver/store/mocks/resolver_tree.ts index 2860eec5a6ab6..ae43955f4c47c 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/mocks/resolver_tree.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/mocks/resolver_tree.ts @@ -46,6 +46,69 @@ export function mockTreeWith2AncestorsAndNoChildren({ } as unknown) as ResolverTree; } +export function mockTreeWithAllProcessesTerminated({ + originID, + firstAncestorID, + secondAncestorID, +}: { + secondAncestorID: string; + firstAncestorID: string; + originID: string; +}): ResolverTree { + const secondAncestor: ResolverEvent = mockEndpointEvent({ + entityID: secondAncestorID, + name: 'a', + parentEntityId: 'none', + timestamp: 0, + }); + const firstAncestor: ResolverEvent = mockEndpointEvent({ + entityID: firstAncestorID, + name: 'b', + parentEntityId: secondAncestorID, + timestamp: 1, + }); + const originEvent: ResolverEvent = mockEndpointEvent({ + entityID: originID, + name: 'c', + parentEntityId: firstAncestorID, + timestamp: 2, + }); + const secondAncestorTermination: ResolverEvent = mockEndpointEvent({ + entityID: secondAncestorID, + name: 'a', + parentEntityId: 'none', + timestamp: 0, + lifecycleType: 'end', + }); + const firstAncestorTermination: ResolverEvent = mockEndpointEvent({ + entityID: firstAncestorID, + name: 'b', + parentEntityId: secondAncestorID, + timestamp: 1, + lifecycleType: 'end', + }); + const originEventTermination: ResolverEvent = mockEndpointEvent({ + entityID: originID, + name: 'c', + parentEntityId: firstAncestorID, + timestamp: 2, + lifecycleType: 'end', + }); + return ({ + entityID: originID, + children: { + childNodes: [], + }, + ancestry: { + ancestors: [ + { lifecycle: [secondAncestor, secondAncestorTermination] }, + { lifecycle: [firstAncestor, firstAncestorTermination] }, + ], + }, + lifecycle: [originEvent, originEventTermination], + } as unknown) as ResolverTree; +} + export function mockTreeWithNoAncestorsAnd2Children({ originID, firstChildID, diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index 66d7e04d118ed..87ef8d5d095ef 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -53,6 +53,14 @@ export const userIsPanning = composeSelectors(cameraStateSelector, cameraSelecto */ export const isAnimating = composeSelectors(cameraStateSelector, cameraSelectors.isAnimating); +/** + * Whether or not a given entity id is in the set of termination events. + */ +export const isProcessTerminated = composeSelectors( + dataStateSelector, + dataSelectors.isProcessTerminated +); + /** * Given a nodeID (aka entity_id) get the indexed process event. * Legacy functions take process events instead of nodeID, use this to get diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx index cb0acdc29ceb1..83d3930065da6 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx @@ -162,19 +162,10 @@ const PanelContent = memo(function PanelContent() { return 'processListWithCounts'; }, [uiSelectedEvent, crumbEvent, crumbId, graphableProcessEntityIds]); - const terminatedProcesses = useSelector(selectors.terminatedProcesses); - const processEntityId = uiSelectedEvent ? event.entityId(uiSelectedEvent) : undefined; - const isProcessTerminated = processEntityId ? terminatedProcesses.has(processEntityId) : false; - const panelInstance = useMemo(() => { if (panelToShow === 'processDetails') { return ( - + ); } @@ -213,13 +204,7 @@ const PanelContent = memo(function PanelContent() { ); } // The default 'Event List' / 'List of all processes' view - return ( - - ); + return ; }, [ uiSelectedEvent, crumbEvent, @@ -227,7 +212,6 @@ const PanelContent = memo(function PanelContent() { pushToQueryParams, relatedStatsForIdFromParams, panelToShow, - isProcessTerminated, ]); return <>{panelInstance}; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_detail.tsx index 5d90cd11d31af..29c7676d2167d 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_detail.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { memo, useMemo } from 'react'; +import { useSelector } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { htmlIdGenerator, @@ -15,6 +16,7 @@ import { } from '@elastic/eui'; import styled from 'styled-components'; import { FormattedMessage } from 'react-intl'; +import * as selectors from '../../store/selectors'; import * as event from '../../../../common/endpoint/models/event'; import { CrumbInfo, formatDate, StyledBreadcrumbs } from './panel_content_utilities'; import { @@ -41,16 +43,14 @@ const StyledDescriptionList = styled(EuiDescriptionList)` */ export const ProcessDetails = memo(function ProcessDetails({ processEvent, - isProcessTerminated, - isProcessOrigin, pushToQueryParams, }: { processEvent: ResolverEvent; - isProcessTerminated: boolean; - isProcessOrigin: boolean; pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown; }) { const processName = event.eventName(processEvent); + const entityId = event.entityId(processEvent); + const isProcessTerminated = useSelector(selectors.isProcessTerminated)(entityId); const processInfoEntry = useMemo(() => { const eventTime = event.eventTimestamp(processEvent); const dateTime = eventTime ? formatDate(eventTime) : ''; @@ -151,8 +151,8 @@ export const ProcessDetails = memo(function ProcessDetails({ if (!processEvent) { return { descriptionText: '' }; } - return cubeAssetsForNode(isProcessTerminated, isProcessOrigin); - }, [processEvent, cubeAssetsForNode, isProcessTerminated, isProcessOrigin]); + return cubeAssetsForNode(isProcessTerminated, false); + }, [processEvent, cubeAssetsForNode, isProcessTerminated]); const titleId = useMemo(() => htmlIdGenerator('resolverTable')(), []); return ( @@ -161,10 +161,7 @@ export const ProcessDetails = memo(function ProcessDetails({

      - + {processName}

      diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx index 6f9bfad8c08c2..efb96cde431e5 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx @@ -50,12 +50,8 @@ const StyledLimitWarning = styled(LimitWarning)` */ export const ProcessListWithCounts = memo(function ProcessListWithCounts({ pushToQueryParams, - isProcessTerminated, - isProcessOrigin, }: { pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown; - isProcessTerminated: boolean; - isProcessOrigin: boolean; }) { interface ProcessTableView { name: string; @@ -65,6 +61,7 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({ const dispatch = useResolverDispatch(); const { timestamp } = useContext(SideEffectContext); + const isProcessTerminated = useSelector(selectors.isProcessTerminated); const handleBringIntoViewClick = useCallback( (processTableViewItem) => { dispatch({ @@ -92,6 +89,8 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({ sortable: true, truncateText: true, render(name: string, item: ProcessTableView) { + const entityId = event.entityId(item.event); + const isTerminated = isProcessTerminated(entityId); return name === '' ? ( {i18n.translate( @@ -108,10 +107,7 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({ pushToQueryParams({ crumbId: event.entityId(item.event), crumbEvent: '' }); }} > - + {name} ); @@ -143,7 +139,7 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({ }, }, ], - [pushToQueryParams, handleBringIntoViewClick, isProcessOrigin, isProcessTerminated] + [pushToQueryParams, handleBringIntoViewClick, isProcessTerminated] ); const { processNodePositions } = useSelector(selectors.layout); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/process_cube_icon.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/process_cube_icon.tsx index 98eea51a011b6..b073324b27f9b 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/process_cube_icon.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/process_cube_icon.tsx @@ -13,13 +13,11 @@ import { useResolverTheme } from '../assets'; */ export const CubeForProcess = memo(function CubeForProcess({ isProcessTerminated, - isProcessOrigin, }: { isProcessTerminated: boolean; - isProcessOrigin: boolean; }) { const { cubeAssetsForNode } = useResolverTheme(); - const { cubeSymbol, descriptionText } = cubeAssetsForNode(isProcessTerminated, isProcessOrigin); + const { cubeSymbol, descriptionText } = cubeAssetsForNode(isProcessTerminated, false); return ( <> From 765c2d1ad3308a3c3af50f8d67b80579aeb13a9a Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Mon, 27 Jul 2020 19:52:28 -0600 Subject: [PATCH 62/96] [Security Solution][ML] Updates siem group name to security (#73218) ## Summary Resolves https://github.com/elastic/kibana/issues/69319 Updates `siem` grouping to `security`, and enables cloudtrail module, fixing mis-match between the newly updated modules (https://github.com/elastic/kibana/pull/71696).

      Also updates all module icons to be consistent: Auditbeat (Before/After):

      Packetbeat (Before/After):

      Winlogbeat (Before/After):

      - [X] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) - [X] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials - Working w/ @benskelker on updated ML Jobs & nomenclature --- .../models/data_recognizer/modules/siem_auditbeat/logo.json | 2 +- .../data_recognizer/modules/siem_auditbeat_auth/logo.json | 4 ++-- .../data_recognizer/modules/siem_packetbeat/logo.json | 4 ++-- .../data_recognizer/modules/siem_winlogbeat/logo.json | 2 +- .../data_recognizer/modules/siem_winlogbeat_auth/logo.json | 4 ++-- .../public/common/components/ml_popover/api.tsx | 2 +- .../common/components/ml_popover/hooks/translations.ts | 2 +- .../components/ml_popover/hooks/use_siem_jobs_helpers.tsx | 2 +- .../ml_popover/jobs_table/filters/groups_filter_popover.tsx | 6 +++--- .../public/common/components/ml_popover/ml_modules.tsx | 1 + .../detections/components/rules/ml_job_select/index.tsx | 2 +- .../server/usage/detections/detections_helpers.ts | 4 +++- 12 files changed, 19 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/logo.json index 40a5c59677147..dfd22f6b1140b 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/logo.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/logo.json @@ -1,3 +1,3 @@ { - "icon": "securityAnalyticsApp" + "icon": "logoSecurity" } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/logo.json index 6b02648ccf287..dfd22f6b1140b 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/logo.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/logo.json @@ -1,3 +1,3 @@ { - "icon": "securityAnalyticsApp" -} \ No newline at end of file + "icon": "logoSecurity" +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/logo.json index 6b02648ccf287..dfd22f6b1140b 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/logo.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/logo.json @@ -1,3 +1,3 @@ { - "icon": "securityAnalyticsApp" -} \ No newline at end of file + "icon": "logoSecurity" +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/logo.json index 40a5c59677147..dfd22f6b1140b 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/logo.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/logo.json @@ -1,3 +1,3 @@ { - "icon": "securityAnalyticsApp" + "icon": "logoSecurity" } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/logo.json index 6b02648ccf287..dfd22f6b1140b 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/logo.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/logo.json @@ -1,3 +1,3 @@ { - "icon": "securityAnalyticsApp" -} \ No newline at end of file + "icon": "logoSecurity" +} diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/api.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/api.tsx index b4da4fa79e035..7c72098209a06 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/api.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/api.tsx @@ -71,7 +71,7 @@ export const setupMlJob = async ({ configTemplate, indexPatternName = 'auditbeat-*', jobIdErrorFilter = [], - groups = ['siem'], + groups = ['security'], prefix = '', }: MlSetupArgs): Promise => { const response = await KibanaServices.get().http.fetch( diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/translations.ts b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/translations.ts index 2b37c437866e0..7b29bab2e38f3 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/translations.ts @@ -9,6 +9,6 @@ import { i18n } from '@kbn/i18n'; export const SIEM_JOB_FETCH_FAILURE = i18n.translate( 'xpack.securitySolution.components.mlPopup.hooks.errors.siemJobFetchFailureTitle', { - defaultMessage: 'SIEM job fetch failure', + defaultMessage: 'Security job fetch failure', } ); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.tsx index 658d2659282ce..adbd712ffeb3e 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.tsx @@ -104,7 +104,7 @@ export const getInstalledJobs = ( compatibleModuleIds: string[] ): SiemJob[] => jobSummaryData - .filter(({ groups }) => groups.includes('siem')) + .filter(({ groups }) => groups.includes('siem') || groups.includes('security')) .map((jobSummary) => ({ ...jobSummary, ...getAugmentedFields(jobSummary.id, moduleJobs, compatibleModuleIds), diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx index 1aa3ad630306e..d879942b8b101 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx @@ -25,8 +25,8 @@ interface GroupsFilterPopoverProps { /** * Popover for selecting which SiemJob groups to filter on. Component extracts unique groups and - * their counts from the provided SiemJobs. The 'siem' group is filtered out as all jobs will be - * siem jobs + * their counts from the provided SiemJobs. The 'siem' & 'security' groups are filtered out as all jobs will be + * siem/security jobs * * @param siemJobs jobs to fetch groups from to display for filtering * @param onSelectedGroupsChanged change listener to be notified when group selection changes @@ -41,7 +41,7 @@ export const GroupsFilterPopoverComponent = ({ const groups = siemJobs .map((j) => j.groups) .flat() - .filter((g) => g !== 'siem'); + .filter((g) => g !== 'siem' && g !== 'security'); const uniqueGroups = Array.from(new Set(groups)); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_modules.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_modules.tsx index b956cf2c1494c..4dccba08590a4 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_modules.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_modules.tsx @@ -12,6 +12,7 @@ export const mlModules: string[] = [ 'siem_auditbeat', 'siem_auditbeat_auth', + 'siem_cloudtrail', 'siem_packetbeat', 'siem_winlogbeat', 'siem_winlogbeat_auth', diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.tsx index cb084d4daa782..cdfdf4ca6b66b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.tsx @@ -41,7 +41,7 @@ const HelpText: React.FC<{ href: string; showEnableWarning: boolean }> = ({ <> diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts index e9d4f3aa426f4..f9905c373291c 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts @@ -176,7 +176,9 @@ export const getMlJobsUsage = async (ml: MlPluginSetup | undefined): Promise module.jobs); - const jobs = await ml.jobServiceProvider(internalMlClient, fakeRequest).jobsSummary(['siem']); + const jobs = await ml + .jobServiceProvider(internalMlClient, fakeRequest) + .jobsSummary(['siem', 'security']); jobsUsage = jobs.reduce((usage, job) => { const isElastic = moduleJobs.some((moduleJob) => moduleJob.id === job.id); From 5af2c1080a85b247324d7b1fd36428c6d561ac55 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Mon, 27 Jul 2020 19:21:14 -0700 Subject: [PATCH 63/96] Exclude `version` from package config attributes that are copied, add safeguard to package config bulk create (#73128) Co-authored-by: Elastic Machine --- .../ingest_manager/server/services/agent_config.ts | 12 +++++------- .../ingest_manager/server/services/package_config.ts | 5 ++++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.ts index 0a9adc1f1c593..3886146e28806 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts @@ -233,16 +233,14 @@ class AgentConfigService { if (baseAgentConfig.package_configs.length) { const newPackageConfigs = (baseAgentConfig.package_configs as PackageConfig[]).map( (packageConfig: PackageConfig) => { - const { id: packageConfigId, ...newPackageConfig } = packageConfig; + const { id: packageConfigId, version, ...newPackageConfig } = packageConfig; return newPackageConfig; } ); - await packageConfigService.bulkCreate( - soClient, - newPackageConfigs, - newAgentConfig.id, - options - ); + await packageConfigService.bulkCreate(soClient, newPackageConfigs, newAgentConfig.id, { + ...options, + bumpConfigRevision: false, + }); } // Get updated config diff --git a/x-pack/plugins/ingest_manager/server/services/package_config.ts b/x-pack/plugins/ingest_manager/server/services/package_config.ts index c2d465cf7c73f..5d1c5d1717714 100644 --- a/x-pack/plugins/ingest_manager/server/services/package_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/package_config.ts @@ -121,7 +121,7 @@ class PackageConfigService { options?: { user?: AuthenticatedUser; bumpConfigRevision?: boolean } ): Promise { const isoDate = new Date().toISOString(); - const { saved_objects: newSos } = await soClient.bulkCreate( + const { saved_objects } = await soClient.bulkCreate( packageConfigs.map((packageConfig) => ({ type: SAVED_OBJECT_TYPE, attributes: { @@ -136,6 +136,9 @@ class PackageConfigService { })) ); + // Filter out invalid SOs + const newSos = saved_objects.filter((so) => !so.error && so.attributes); + // Assign it to the given agent config await agentConfigService.assignPackageConfigs( soClient, From 82d7e7db699bbe961da5eb8b2218de5d2c2e7e18 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Mon, 27 Jul 2020 19:21:41 -0700 Subject: [PATCH 64/96] [Ingest Manager] Convert select agent config step to use combo box (#73172) * Initial pass at using combo box instead of selectable for agent configs * Hide agent count messaging if fleet isn't set up * Fix types * Fix i18n * Fix i18n again * Add comment explaining styling Co-authored-by: Elastic Machine --- .../step_select_config.tsx | 227 +++++++++++------- .../list_page/components/create_config.tsx | 2 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 4 files changed, 145 insertions(+), 86 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx index 91c80b7eee4c8..6f06530100d71 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx @@ -3,17 +3,19 @@ * 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, Fragment } from 'react'; +import React, { useEffect, useState } from 'react'; +import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, - EuiSelectable, - EuiSpacer, + EuiComboBox, + EuiComboBoxOptionOption, EuiTextColor, EuiPortal, - EuiButtonEmpty, + EuiFormRow, + EuiLink, } from '@elastic/eui'; import { Error } from '../../../components'; import { AgentConfig, PackageInfo, GetAgentConfigsResponseItem } from '../../../types'; @@ -23,9 +25,30 @@ import { useGetAgentConfigs, sendGetOneAgentConfig, useCapabilities, + useFleetStatus, } from '../../../hooks'; import { CreateAgentConfigFlyout } from '../list_page/components'; +const AgentConfigWrapper = styled(EuiFormRow)` + .euiFormRow__label { + width: 100%; + } +`; + +// Custom styling for drop down list items due to: +// 1) the max-width and overflow properties is added to prevent long config +// names/descriptions from overflowing the flex items +// 2) max-width is built from the grow property on the flex items because the value +// changes based on if Fleet is enabled/setup or not +const AgentConfigNameColumn = styled(EuiFlexItem)` + max-width: ${(props) => `${((props.grow as number) / 9) * 100}%`}; + overflow: hidden; +`; +const AgentConfigDescriptionColumn = styled(EuiFlexItem)` + max-width: ${(props) => `${((props.grow as number) / 9) * 100}%`}; + overflow: hidden; +`; + export const StepSelectConfig: React.FunctionComponent<{ pkgkey: string; updatePackageInfo: (packageInfo: PackageInfo | undefined) => void; @@ -33,6 +56,8 @@ export const StepSelectConfig: React.FunctionComponent<{ updateAgentConfig: (config: AgentConfig | undefined) => void; setIsLoadingSecondStep: (isLoading: boolean) => void; }> = ({ pkgkey, updatePackageInfo, agentConfig, updateAgentConfig, setIsLoadingSecondStep }) => { + const { isReady: isFleetReady } = useFleetStatus(); + // Selected config state const [selectedConfigId, setSelectedConfigId] = useState( agentConfig ? agentConfig.id : undefined @@ -106,6 +131,40 @@ export const StepSelectConfig: React.FunctionComponent<{ } }, [selectedConfigId, agentConfig, updateAgentConfig, setIsLoadingSecondStep]); + const agentConfigOptions: Array> = packageInfoData + ? agentConfigs.map((agentConf) => { + const alreadyHasLimitedPackage = + (isLimitedPackage && + doesAgentConfigAlreadyIncludePackage(agentConf, packageInfoData.response.name)) || + false; + return { + label: agentConf.name, + value: agentConf.id, + disabled: alreadyHasLimitedPackage, + 'data-test-subj': 'agentConfigItem', + }; + }) + : []; + + const selectedConfigOption = agentConfigOptions.find( + (option) => option.value === selectedConfigId + ); + + // Try to select default agent config + useEffect(() => { + if (!selectedConfigId && agentConfigs.length && agentConfigOptions.length) { + const defaultAgentConfig = agentConfigs.find((config) => config.is_default); + if (defaultAgentConfig) { + const defaultAgentConfigOption = agentConfigOptions.find( + (option) => option.value === defaultAgentConfig.id + ); + if (defaultAgentConfigOption && !defaultAgentConfigOption.disabled) { + setSelectedConfigId(defaultAgentConfig.id); + } + } + } + }, [agentConfigs, agentConfigOptions, selectedConfigId]); + // Display package error if there is one if (packageInfoError) { return ( @@ -154,77 +213,95 @@ export const StepSelectConfig: React.FunctionComponent<{ ) : null} - { - const alreadyHasLimitedPackage = - (isLimitedPackage && - packageInfoData && - doesAgentConfigAlreadyIncludePackage(agentConf, packageInfoData.response.name)) || - false; - return { - label: agentConf.name, - key: agentConf.id, - checked: selectedConfigId === agentConf.id ? 'on' : undefined, - disabled: alreadyHasLimitedPackage, - 'data-test-subj': 'agentConfigItem', - }; - })} - renderOption={(option) => ( - - {option.label} + - - {agentConfigsById[option.key!].description} - + - - - +
      + setIsCreateAgentConfigFlyoutOpen(true)} + > + + +
      - )} - listProps={{ - bordered: true, - }} - searchProps={{ - placeholder: i18n.translate( - 'xpack.ingestManager.createPackageConfig.StepSelectConfig.filterAgentConfigsInputPlaceholder', + } + helpText={ + isFleetReady && selectedConfigId ? ( + + ) : null + } + > + { - const selectedOption = options.find((option) => option.checked === 'on'); - if (selectedOption) { - if (selectedOption.key !== selectedConfigId) { - setSelectedConfigId(selectedOption.key); + )} + singleSelection={{ asPlainText: true }} + isClearable={false} + fullWidth={true} + isLoading={isAgentConfigsLoading || isPackageInfoLoading} + options={agentConfigOptions} + renderOption={(option: EuiComboBoxOptionOption) => { + return ( + + + {option.label} + + + + {agentConfigsById[option.value!].description} + + + {isFleetReady ? ( + + + + + + ) : null} + + ); + }} + selectedOptions={selectedConfigOption ? [selectedConfigOption] : []} + onChange={(options) => { + const selectedOption = options[0] || undefined; + if (selectedOption) { + if (selectedOption.value !== selectedConfigId) { + setSelectedConfigId(selectedOption.value); + } + } else { + setSelectedConfigId(undefined); } - } else { - setSelectedConfigId(undefined); - } - }} - > - {(list, search) => ( - - {search} - - {list} - - )} -
      + }} + /> +
      {/* Display selected agent config error if there is one */} {selectedConfigError ? ( @@ -240,22 +317,6 @@ export const StepSelectConfig: React.FunctionComponent<{ />
      ) : null} - -
      - setIsCreateAgentConfigFlyoutOpen(true)} - flush="left" - size="s" - > - - -
      -
      ); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx index fc593705a4e1b..749716b473c85 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx @@ -160,7 +160,7 @@ export const CreateAgentConfigFlyout: React.FunctionComponent = ({ ); return ( - + onClose()} size="l" maxWidth={400} {...restOfProps}> {header} {body} {footer} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index cf79f463b35cb..ee7d1e0298d00 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8108,7 +8108,6 @@ "xpack.ingestManager.createPackageConfig.StepSelectConfig.errorLoadingAgentConfigsTitle": "エージェント構成の読み込みエラー", "xpack.ingestManager.createPackageConfig.StepSelectConfig.errorLoadingPackageTitle": "パッケージ情報の読み込みエラー", "xpack.ingestManager.createPackageConfig.StepSelectConfig.errorLoadingSelectedAgentConfigTitle": "選択したエージェント構成の読み込みエラー", - "xpack.ingestManager.createPackageConfig.StepSelectConfig.filterAgentConfigsInputPlaceholder": "エージェント構成の検索", "xpack.ingestManager.createPackageConfig.stepSelectPackage.errorLoadingConfigTitle": "エージェント構成情報の読み込みエラー", "xpack.ingestManager.createPackageConfig.stepSelectPackage.errorLoadingPackagesTitle": "統合の読み込みエラー", "xpack.ingestManager.createPackageConfig.stepSelectPackage.errorLoadingSelectedPackageTitle": "選択した統合の読み込みエラー", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b45fe1baa9e9a..30c932c362a4f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8113,7 +8113,6 @@ "xpack.ingestManager.createPackageConfig.StepSelectConfig.errorLoadingAgentConfigsTitle": "加载代理配置时出错", "xpack.ingestManager.createPackageConfig.StepSelectConfig.errorLoadingPackageTitle": "加载软件包信息时出错", "xpack.ingestManager.createPackageConfig.StepSelectConfig.errorLoadingSelectedAgentConfigTitle": "加载选定代理配置时出错", - "xpack.ingestManager.createPackageConfig.StepSelectConfig.filterAgentConfigsInputPlaceholder": "搜索代理配置", "xpack.ingestManager.createPackageConfig.stepSelectPackage.errorLoadingConfigTitle": "加载代理配置信息时出错", "xpack.ingestManager.createPackageConfig.stepSelectPackage.errorLoadingPackagesTitle": "加载集成时出错", "xpack.ingestManager.createPackageConfig.stepSelectPackage.errorLoadingSelectedPackageTitle": "加载选定集成时出错", From cc84ee31856c8eb70d5a2d1093b21678d5842f88 Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Mon, 27 Jul 2020 21:28:39 -0500 Subject: [PATCH 65/96] [Metrics UI] Saved views bugs (#72518) * Add test for logs and metrics telemetry * wait before you go * Remove kubenetes * Fix type check * Add back kubernetes test * Remove kubernetes * Don't allow deleting default default view. * Fix bug with duplicate loads of data. Because the load data function takes options.source and the source of options can change, we need to remove it from deps * Remove unused variable * Reload when loadData function is changed * Don't send the request immediately Co-authored-by: Elastic Machine --- .../public/components/saved_views/manage_views_flyout.tsx | 4 ++++ .../public/components/saved_views/toolbar_control.tsx | 2 +- .../infra/public/containers/saved_view/saved_view.tsx | 8 +++----- .../pages/metrics/inventory_view/components/layout.tsx | 3 ++- .../infra/public/pages/metrics/metrics_explorer/index.tsx | 3 ++- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/infra/public/components/saved_views/manage_views_flyout.tsx b/x-pack/plugins/infra/public/components/saved_views/manage_views_flyout.tsx index fa9b45558e491..698034f8154d1 100644 --- a/x-pack/plugins/infra/public/components/saved_views/manage_views_flyout.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/manage_views_flyout.tsx @@ -96,6 +96,10 @@ export function SavedViewManageViewsFlyout({ const renderDeleteAction = useCallback( (item: SavedView) => { + if (item.id === '0') { + return <>; + } + return ( (props: Props) { /> - + { const { data, loading, find, error: errorOnFind, hasView } = useFindSavedObject< SavedViewSavedObject >(viewType); - + const [shouldLoadDefault] = useState(props.shouldLoadDefault); const [currentView, setCurrentView] = useState | null>(null); const [loadingDefaultView, setLoadingDefaultView] = useState(null); const { create, error: errorOnCreate, data: createdViewData, createdId } = useCreateSavedObject( @@ -211,8 +211,6 @@ export const useSavedView = (props: Props) => { }, [setCurrentView, defaultViewId, defaultViewState]); useEffect(() => { - const shouldLoadDefault = props.shouldLoadDefault; - if (loadingDefaultView || currentView || !shouldLoadDefault) { return; } @@ -225,7 +223,7 @@ export const useSavedView = (props: Props) => { } }, [ loadDefaultView, - props.shouldLoadDefault, + shouldLoadDefault, setDefault, loadingDefaultView, currentView, @@ -246,7 +244,7 @@ export const useSavedView = (props: Props) => { errorOnUpdate, errorOnFind, errorOnCreate: createError, - shouldLoadDefault: props.shouldLoadDefault, + shouldLoadDefault, makeDefault, sourceIsLoading, deleteView, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx index fddd92128708a..ad92c054ee459 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx @@ -55,7 +55,8 @@ export const Layout = () => { sourceId, currentTime, accountId, - region + region, + false ); const options = { diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx index cd875ae54071c..20efca79650a1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx @@ -57,7 +57,8 @@ export const MetricsExplorerPage = ({ source, derivedIndexPattern }: MetricsExpl // load metrics explorer data after default view loaded, unless we're not loading a view loadData(); } - }, [loadData, currentView, shouldLoadDefault]); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + }, [loadData, shouldLoadDefault]); return ( From 281c76767b21c458a237474d77211f18883d8d68 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 28 Jul 2020 09:23:28 +0200 Subject: [PATCH 66/96] updates cypress to v4.11.0 (#73327) Co-authored-by: Elastic Machine --- x-pack/package.json | 2 +- yarn.lock | 170 +++++++++++++++----------------------------- 2 files changed, 60 insertions(+), 112 deletions(-) diff --git a/x-pack/package.json b/x-pack/package.json index dee99d6f0ddac..76655f75cadcc 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -131,7 +131,7 @@ "cheerio": "0.22.0", "commander": "3.0.2", "copy-webpack-plugin": "^6.0.2", - "cypress": "4.5.0", + "cypress": "4.11.0", "cypress-multi-reporters": "^1.2.3", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.2", diff --git a/yarn.lock b/yarn.lock index 899bc45fbe3fb..c1328731db150 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4717,21 +4717,11 @@ resolved "https://registry.yarnpkg.com/@types/base64-js/-/base64-js-1.2.5.tgz#582b2476169a6cba460a214d476c744441d873d5" integrity sha1-WCskdhaabLpGCiFNR2x0REHYc9U= -"@types/blob-util@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@types/blob-util/-/blob-util-1.3.3.tgz#adba644ae34f88e1dd9a5864c66ad651caaf628a" - integrity sha512-4ahcL/QDnpjWA2Qs16ZMQif7HjGP2cw3AGjHabybjw7Vm1EKu+cfQN1D78BaZbS1WJNa1opSMF5HNMztx7lR0w== - "@types/bluebird@*", "@types/bluebird@^3.1.1": version "3.5.30" resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.30.tgz#ee034a0eeea8b84ed868b1aa60d690b08a6cfbc5" integrity sha512-8LhzvcjIoqoi1TghEkRMkbbmM+jhHnBokPGkJWjclMK+Ks0MxEBow3/p2/iFTZ+OIbJHQDSfpgdZEb+af3gfVw== -"@types/bluebird@3.5.29": - version "3.5.29" - resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.29.tgz#7cd933c902c4fc83046517a1bef973886d00bdb6" - integrity sha512-kmVtnxTuUuhCET669irqQmPAez4KFnFVKvpleVRyfC3g+SHD1hIkFZcWLim9BVcwUBLO59o8VZE4yGCmTif8Yw== - "@types/boom@*", "@types/boom@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@types/boom/-/boom-7.2.0.tgz#19c36cbb5811a7493f0f2e37f31d42b28df1abc1" @@ -4762,15 +4752,7 @@ resolved "https://registry.yarnpkg.com/@types/catbox/-/catbox-10.0.1.tgz#266679017749041fe9873fee1131dd2aaa04a07e" integrity sha512-ECuJ+f5gGHiLeiE4RlE/xdqv/0JVDToegPV1aTb10tQStYa0Ycq2OJfQukDv3IFaw3B+CMV46jHc5bXe6QXEQg== -"@types/chai-jquery@1.1.40": - version "1.1.40" - resolved "https://registry.yarnpkg.com/@types/chai-jquery/-/chai-jquery-1.1.40.tgz#445bedcbbb2ae4e3027f46fa2c1733c43481ffa1" - integrity sha512-mCNEZ3GKP7T7kftKeIs7QmfZZQM7hslGSpYzKbOlR2a2HCFf9ph4nlMRA9UnuOETeOQYJVhJQK7MwGqNZVyUtQ== - dependencies: - "@types/chai" "*" - "@types/jquery" "*" - -"@types/chai@*", "@types/chai@4.2.7", "@types/chai@^4.2.11": +"@types/chai@^4.2.11": version "4.2.11" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.11.tgz#d3614d6c5f500142358e6ed24e1bf16657536c50" integrity sha512-t7uW6eFafjO+qJ3BIV2gGUyZs27egcNRkUdalkud+Qa3+kg//f129iuOFivHDXQ+vnU3fDXuwgv0cqMCbcE8sw== @@ -5260,7 +5242,7 @@ resolved "https://registry.yarnpkg.com/@types/joi/-/joi-13.6.1.tgz#325486a397504f8e22c8c551dc8b0e1d41d5d5ae" integrity sha512-JxZ0NP8NuB0BJOXi1KvAA6rySLTPmhOy4n2gzSFq/IFM3LNFm0h+2Vn/bPPgEYlWqzS2NPeLgKqfm75baX+Hog== -"@types/jquery@*", "@types/jquery@3.3.31", "@types/jquery@^3.3.31": +"@types/jquery@*", "@types/jquery@^3.3.31": version "3.3.31" resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.31.tgz#27c706e4bf488474e1cb54a71d8303f37c93451b" integrity sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg== @@ -5346,11 +5328,6 @@ "@types/node" "*" "@types/webpack" "*" -"@types/lodash@4.14.149", "@types/lodash@^4.14.155": - version "4.14.156" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.156.tgz#cbe30909c89a1feeb7c60803e785344ea0ec82d1" - integrity sha512-l2AgHXcKUwx2DsvP19wtRPqZ4NkONjmorOdq4sMcxIjqdIuuV/ULo2ftuv4NUpevwfW7Ju/UKLqo0ZXuEt/8lQ== - "@types/lodash@^3.10.1": version "3.10.3" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-3.10.3.tgz#aaddec6a3c93bf03b402db3acf5d4c77bce8bdff" @@ -5361,6 +5338,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.150.tgz#649fe44684c3f1fcb6164d943c5a61977e8cf0bd" integrity sha512-kMNLM5JBcasgYscD9x/Gvr6lTAv2NVgsKtet/hm93qMyf/D1pt+7jeEZklKJKxMVmXjxbRVQQGfqDSfipYCO6w== +"@types/lodash@^4.14.155": + version "4.14.156" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.156.tgz#cbe30909c89a1feeb7c60803e785344ea0ec82d1" + integrity sha512-l2AgHXcKUwx2DsvP19wtRPqZ4NkONjmorOdq4sMcxIjqdIuuV/ULo2ftuv4NUpevwfW7Ju/UKLqo0ZXuEt/8lQ== + "@types/log-symbols@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/log-symbols/-/log-symbols-2.0.0.tgz#7919e2ec3c8d13879bfdcab310dd7a3f7fc9466d" @@ -5419,7 +5401,7 @@ dependencies: "@types/mime-db" "*" -"@types/minimatch@*", "@types/minimatch@3.0.3", "@types/minimatch@^3.0.3": +"@types/minimatch@*", "@types/minimatch@^3.0.3": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== @@ -5441,11 +5423,6 @@ dependencies: "@types/node" "*" -"@types/mocha@5.2.7": - version "5.2.7" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea" - integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ== - "@types/mocha@^7.0.2": version "7.0.2" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-7.0.2.tgz#b17f16cf933597e10d6d78eae3251e692ce8b0ce" @@ -5859,32 +5836,12 @@ dependencies: "@types/node" "*" -"@types/sinon-chai@3.2.3": - version "3.2.3" - resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.3.tgz#afe392303dda95cc8069685d1e537ff434fa506e" - integrity sha512-TOUFS6vqS0PVL1I8NGVSNcFaNJtFoyZPXZ5zur+qlhDfOmQECZZM4H4kKgca6O8L+QceX/ymODZASfUfn+y4yQ== - dependencies: - "@types/chai" "*" - "@types/sinon" "*" - -"@types/sinon@*": - version "9.0.4" - resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.4.tgz#e934f904606632287a6e7f7ab0ce3f08a0dad4b1" - integrity sha512-sJmb32asJZY6Z2u09bl0G2wglSxDlROlAejCjsnor+LzBMz17gu8IU7vKC/vWDnv9zEq2wqADHVXFjf4eE8Gdw== - dependencies: - "@types/sinonjs__fake-timers" "*" - -"@types/sinon@7.5.1": - version "7.5.1" - resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.5.1.tgz#d27b81af0d1cfe1f9b24eebe7a24f74ae40f5b7c" - integrity sha512-EZQUP3hSZQyTQRfiLqelC9NMWd1kqLcmQE0dMiklxBkgi84T+cHOhnKpgk4NnOWpGX863yE6+IaGnOXUNFqDnQ== - "@types/sinon@^7.0.13": version "7.0.13" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.0.13.tgz#ca039c23a9e27ebea53e0901ef928ea2a1a6d313" integrity sha512-d7c/C/+H/knZ3L8/cxhicHUiTDxdgap0b/aNJfsmLwFu/iOP17mdgbQsbHA3SJmrzsjD0l3UEE5SN4xxuz5ung== -"@types/sinonjs__fake-timers@*": +"@types/sinonjs__fake-timers@6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e" integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA== @@ -7378,10 +7335,10 @@ aproba@^1.0.3, aproba@^1.1.1: resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== -arch@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.1.tgz#8f5c2731aa35a30929221bb0640eed65175ec84e" - integrity sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg== +arch@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.2.tgz#0c52bbe7344bb4fa260c443d2cbad9c00ff2f0bf" + integrity sha512-NTBIIbAfkJeIletyABbVtdPgeKfDafR+1mZV/AyyfC1UkVkp9iUjV+wwmqtUgphHYajbI86jejBJp5e+jkGTiQ== archiver-utils@^2.1.0: version "2.1.0" @@ -7849,7 +7806,7 @@ async@^2.6.3: dependencies: lodash "^4.17.14" -async@^3.1.0: +async@^3.1.0, async@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== @@ -10499,10 +10456,10 @@ commander@3.0.2: resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== -commander@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.0.tgz#545983a0603fe425bc672d66c9e3c89c42121a83" - integrity sha512-NIQrwvv9V39FHgGFm36+U9SMQzbiHvU79k+iADraJTpmrFFfx7Ds0IvDoAdZsDrknlkRk14OYoWXb57uTh7/sw== +commander@4.1.1, commander@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== commander@^2.13.0, commander@^2.15.1, commander@^2.16.0, commander@^2.19.0: version "2.20.0" @@ -10524,11 +10481,6 @@ commander@^3.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.0.tgz#0641ea00838c7a964627f04cddc336a2deddd60a" integrity sha512-pl3QrGOBa9RZaslQiqnnKX2J068wcQw7j9AIaBQ9/JEp5RY6je4jKTImg0Bd+rpoONSe7GUFSgkxLeo17m3Pow== -commander@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" - integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== - commander@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/commander/-/commander-5.0.0.tgz#dbf1909b49e5044f8fdaf0adc809f0c0722bdfd0" @@ -11489,48 +11441,39 @@ cypress-multi-reporters@^1.2.3: debug "^4.1.1" lodash "^4.17.11" -cypress@4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.5.0.tgz#01940d085f6429cec3c87d290daa47bb976a7c7b" - integrity sha512-2A4g5FW5d2fHzq8HKUGAMVTnW6P8nlWYQALiCoGN4bqBLvgwhYM/oG9oKc2CS6LnvgHFiKivKzpm9sfk3uU3zQ== +cypress@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.11.0.tgz#054b0b85fd3aea793f186249ee1216126d5f0a7e" + integrity sha512-6Yd598+KPATM+dU1Ig0g2hbA+R/o1MAKt0xIejw4nZBVLSplCouBzqeKve6XsxGU6n4HMSt/+QYsWfFcoQeSEw== dependencies: "@cypress/listr-verbose-renderer" "0.4.1" "@cypress/request" "2.88.5" "@cypress/xvfb" "1.2.4" - "@types/blob-util" "1.3.3" - "@types/bluebird" "3.5.29" - "@types/chai" "4.2.7" - "@types/chai-jquery" "1.1.40" - "@types/jquery" "3.3.31" - "@types/lodash" "4.14.149" - "@types/minimatch" "3.0.3" - "@types/mocha" "5.2.7" - "@types/sinon" "7.5.1" - "@types/sinon-chai" "3.2.3" + "@types/sinonjs__fake-timers" "6.0.1" "@types/sizzle" "2.3.2" - arch "2.1.1" + arch "2.1.2" bluebird "3.7.2" cachedir "2.3.0" chalk "2.4.2" check-more-types "2.24.0" cli-table3 "0.5.1" - commander "4.1.0" + commander "4.1.1" common-tags "1.8.0" debug "4.1.1" - eventemitter2 "4.1.2" + eventemitter2 "6.4.2" execa "1.0.0" executable "4.1.1" extract-zip "1.7.0" fs-extra "8.1.0" - getos "3.1.4" + getos "3.2.1" is-ci "2.0.0" - is-installed-globally "0.1.0" + is-installed-globally "0.3.2" lazy-ass "1.6.0" listr "0.14.3" - lodash "4.17.15" + lodash "4.17.19" log-symbols "3.0.0" minimist "1.2.5" - moment "2.24.0" + moment "2.26.0" ospath "1.2.2" pretty-bytes "5.3.0" ramda "0.26.1" @@ -13890,10 +13833,10 @@ event-target-shim@^5.0.0: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== -eventemitter2@4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-4.1.2.tgz#0e1a8477af821a6ef3995b311bf74c23a5247f15" - integrity sha1-DhqEd6+CGm7zmVsxG/dMI6UkfxU= +eventemitter2@6.4.2: + version "6.4.2" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.2.tgz#f31f8b99d45245f0edbc5b00797830ff3b388970" + integrity sha512-r/Pwupa5RIzxIHbEKCkNXqpEQIIT4uQDxmP4G/Lug/NokVUWj0joz/WzWl3OxRpC5kDrH/WdiUJoR+IrwvXJEw== eventemitter2@~0.4.13: version "0.4.14" @@ -15515,12 +15458,12 @@ getopts@^2.2.5: resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.2.5.tgz#67a0fe471cacb9c687d817cab6450b96dde8313b" integrity sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA== -getos@3.1.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/getos/-/getos-3.1.4.tgz#29cdf240ed10a70c049add7b6f8cb08c81876faf" - integrity sha512-UORPzguEB/7UG5hqiZai8f0vQ7hzynMQyJLxStoQ8dPGAcmgsfXOPA4iE/fGtweHYkK+z4zc9V0g+CIFRf5HYw== +getos@3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5" + integrity sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q== dependencies: - async "^3.1.0" + async "^3.2.0" getos@^3.1.0: version "3.1.0" @@ -18256,15 +18199,7 @@ is-hexadecimal@^1.0.0: resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.1.tgz#6e084bbc92061fbb0971ec58b6ce6d404e24da69" integrity sha1-bghLvJIGH7sJcexYts5tQE4k2mk= -is-installed-globally@0.1.0, is-installed-globally@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" - integrity sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA= - dependencies: - global-dirs "^0.1.0" - is-path-inside "^1.0.0" - -is-installed-globally@^0.3.1: +is-installed-globally@0.3.2, is-installed-globally@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== @@ -18272,6 +18207,14 @@ is-installed-globally@^0.3.1: global-dirs "^2.0.1" is-path-inside "^3.0.1" +is-installed-globally@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" + integrity sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA= + dependencies: + global-dirs "^0.1.0" + is-path-inside "^1.0.0" + is-integer@^1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/is-integer/-/is-integer-1.0.7.tgz#6bde81aacddf78b659b6629d629cadc51a886d5c" @@ -20799,16 +20742,16 @@ lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== +lodash@4.17.19, lodash@^4.17.16: + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== + lodash@^3.10.1: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y= -lodash@^4.17.16: - version "4.17.19" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" - integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== - "lodash@npm:@elastic/lodash@3.10.1-kibana4": version "3.10.1-kibana4" resolved "https://registry.yarnpkg.com/@elastic/lodash/-/lodash-3.10.1-kibana4.tgz#d491228fd659b4a1b0dfa08ba9c67a4979b9746d" @@ -21974,7 +21917,12 @@ moment-timezone@^0.5.27: dependencies: moment ">= 2.9.0" -moment@2.24.0, "moment@>= 2.9.0", moment@>=1.6.0, moment@>=2.14.0, moment@^2.10.6, moment@^2.24.0: +moment@2.26.0: + version "2.26.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a" + integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw== + +"moment@>= 2.9.0", moment@>=1.6.0, moment@>=2.14.0, moment@^2.10.6, moment@^2.24.0: version "2.24.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== From 7b29ecf0b51a835394b0c45fe0623cc978455520 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 28 Jul 2020 10:29:33 +0300 Subject: [PATCH 67/96] [Functional Tests] Fix flakiness on TSVB chart on switching index patterns test (#73238) --- test/functional/services/combo_box.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/services/combo_box.ts b/test/functional/services/combo_box.ts index 60fea7ea86cf9..ac7a40361d065 100644 --- a/test/functional/services/combo_box.ts +++ b/test/functional/services/combo_box.ts @@ -90,7 +90,7 @@ export function ComboBoxProvider({ getService, getPageObjects }: FtrProviderCont await this.clickOption(options.clickWithMouse, selectOptions[0]); } else { // if it doesn't find the item which text starts with value, it will choose the first option - const firstOption = await find.byCssSelector('.euiFilterSelectItem'); + const firstOption = await find.byCssSelector('.euiFilterSelectItem', 5000); await this.clickOption(options.clickWithMouse, firstOption); } } else { From a696f6c79b3fe20d60712faeb21e31d1f4538de4 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 28 Jul 2020 10:29:47 +0300 Subject: [PATCH 68/96] [Functional Tests] Increase waitTime for timelion to fetch the results (#73255) Co-authored-by: Elastic Machine --- test/functional/page_objects/timelion_page.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/functional/page_objects/timelion_page.ts b/test/functional/page_objects/timelion_page.ts index f025fc946bef1..23a9cc514a444 100644 --- a/test/functional/page_objects/timelion_page.ts +++ b/test/functional/page_objects/timelion_page.ts @@ -47,7 +47,7 @@ export function TimelionPageProvider({ getService, getPageObjects }: FtrProvider public async updateExpression(updates: string) { const input = await testSubjects.find('timelionExpressionTextArea'); await input.type(updates); - await PageObjects.common.sleep(500); + await PageObjects.common.sleep(1000); } public async getExpression() { @@ -60,7 +60,7 @@ export function TimelionPageProvider({ getService, getPageObjects }: FtrProvider return await Promise.all(elements.map(async (element) => await element.getVisibleText())); } - public async clickSuggestion(suggestionIndex = 0, waitTime = 500) { + public async clickSuggestion(suggestionIndex = 0, waitTime = 1000) { const elements = await testSubjects.findAll('timelionSuggestionListItem'); if (suggestionIndex > elements.length) { throw new Error( From 9b570a9bf1262428661695179fee801345017efc Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 28 Jul 2020 09:46:36 +0200 Subject: [PATCH 69/96] fix dashboard index pattern race condition (#72899) * fix dashboard index pattern race condition * improve Co-authored-by: Elastic Machine --- .../application/dashboard_app_controller.tsx | 63 ++++++++++++------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index 8138e1c7f4dfd..2a0e2889575f3 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -25,8 +25,8 @@ import React, { useState, ReactElement } from 'react'; import ReactDOM from 'react-dom'; import angular from 'angular'; -import { Subscription } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { Observable, pipe, Subscription } from 'rxjs'; +import { filter, map, mapTo, startWith, switchMap } from 'rxjs/operators'; import { History } from 'history'; import { SavedObjectSaveOpts } from 'src/plugins/saved_objects/public'; import { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public'; @@ -253,11 +253,7 @@ export class DashboardAppController { navActions[TopNavIds.VISUALIZE](); }; - const updateIndexPatterns = (container?: DashboardContainer) => { - if (!container || isErrorEmbeddable(container)) { - return; - } - + function getDashboardIndexPatterns(container: DashboardContainer): IndexPattern[] { let panelIndexPatterns: IndexPattern[] = []; Object.values(container.getChildIds()).forEach((id) => { const embeddableInstance = container.getChild(id); @@ -267,19 +263,34 @@ export class DashboardAppController { panelIndexPatterns.push(...embeddableIndexPatterns); }); panelIndexPatterns = uniqBy(panelIndexPatterns, 'id'); + return panelIndexPatterns; + } - if (panelIndexPatterns && panelIndexPatterns.length > 0) { - $scope.$evalAsync(() => { - $scope.indexPatterns = panelIndexPatterns; - }); - } else { - indexPatterns.getDefault().then((defaultIndexPattern) => { - $scope.$evalAsync(() => { - $scope.indexPatterns = [defaultIndexPattern as IndexPattern]; - }); + const updateIndexPatternsOperator = pipe( + filter((container: DashboardContainer) => !!container && !isErrorEmbeddable(container)), + map(getDashboardIndexPatterns), + // using switchMap for previous task cancellation + switchMap((panelIndexPatterns: IndexPattern[]) => { + return new Observable((observer) => { + if (panelIndexPatterns && panelIndexPatterns.length > 0) { + $scope.$evalAsync(() => { + if (observer.closed) return; + $scope.indexPatterns = panelIndexPatterns; + observer.complete(); + }); + } else { + indexPatterns.getDefault().then((defaultIndexPattern) => { + if (observer.closed) return; + $scope.$evalAsync(() => { + if (observer.closed) return; + $scope.indexPatterns = [defaultIndexPattern as IndexPattern]; + observer.complete(); + }); + }); + } }); - } - }; + }) + ); const getEmptyScreenProps = ( shouldShowEditHelp: boolean, @@ -384,11 +395,17 @@ export class DashboardAppController { ) : null; }; - updateIndexPatterns(dashboardContainer); - - outputSubscription = dashboardContainer.getOutput$().subscribe(() => { - updateIndexPatterns(dashboardContainer); - }); + outputSubscription = new Subscription(); + outputSubscription.add( + dashboardContainer + .getOutput$() + .pipe( + mapTo(dashboardContainer), + startWith(dashboardContainer), // to trigger initial index pattern update + updateIndexPatternsOperator + ) + .subscribe() + ); inputSubscription = dashboardContainer.getInput$().subscribe(() => { let dirty = false; From abfda1f79273111b581a14b1a43fe134c6053e6c Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 28 Jul 2020 09:57:04 +0200 Subject: [PATCH 70/96] Use "Apply_filter_trigger" in dashboard drilldown (#71468) * attach dashboard drilldown to apply filter trigger * fix types Co-authored-by: Elastic Machine --- ...na-plugin-plugins-data-public.esfilters.md | 1 + src/plugins/dashboard/public/index.ts | 6 +- src/plugins/dashboard/public/plugin.tsx | 7 +- src/plugins/dashboard/public/url_generator.ts | 6 +- src/plugins/data/public/index.ts | 2 + src/plugins/data/public/public.api.md | 94 ++++++------- .../data/public/query/timefilter/index.ts | 2 +- .../timefilter/lib/extract_time_filter.ts | 15 ++- x-pack/plugins/dashboard_enhanced/kibana.json | 3 +- .../flyout_create_drilldown.tsx | 11 +- .../constants.ts | 7 + .../drilldown.test.tsx | 54 ++------ .../drilldown.tsx | 125 +++++++----------- .../dashboard_to_dashboard_drilldown/index.ts | 5 +- .../dashboard_to_dashboard_drilldown/types.ts | 10 -- .../embeddable_action_storage.test.ts | 41 ++++++ .../embeddables/embeddable_action_storage.ts | 30 ++++- .../connected_flyout_manage_drilldowns.tsx | 6 +- 18 files changed, 227 insertions(+), 198 deletions(-) diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md index 37142cf1794c3..bc34d4113f847 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md @@ -52,5 +52,6 @@ esFilters: { convertRangeFilterToTimeRangeString: typeof convertRangeFilterToTimeRangeString; mapAndFlattenFilters: (filters: import("../common").Filter[]) => import("../common").Filter[]; extractTimeFilter: typeof extractTimeFilter; + extractTimeRange: typeof extractTimeRange; } ``` diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts index 17968dd0281e6..dcfde67cd9f13 100644 --- a/src/plugins/dashboard/public/index.ts +++ b/src/plugins/dashboard/public/index.ts @@ -32,7 +32,11 @@ export { export { DashboardConstants, createDashboardEditUrl } from './dashboard_constants'; export { DashboardStart, DashboardUrlGenerator } from './plugin'; -export { DASHBOARD_APP_URL_GENERATOR, createDashboardUrlGenerator } from './url_generator'; +export { + DASHBOARD_APP_URL_GENERATOR, + createDashboardUrlGenerator, + DashboardUrlGeneratorState, +} from './url_generator'; export { addEmbeddableToDashboardUrl } from './url_utils/url_helper'; export { SavedObjectDashboard } from './saved_dashboards'; export { SavedDashboardPanel } from './types'; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 041a02a251e8a..f0b57fec169fd 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -65,6 +65,7 @@ import { ACTION_REPLACE_PANEL, ClonePanelAction, ClonePanelActionContext, + createDashboardContainerByValueRenderer, DASHBOARD_CONTAINER_TYPE, DashboardContainerFactory, DashboardContainerFactoryDefinition, @@ -77,17 +78,17 @@ import { import { createDashboardUrlGenerator, DASHBOARD_APP_URL_GENERATOR, - DashboardAppLinkGeneratorState, + DashboardUrlGeneratorState, } from './url_generator'; import { createSavedDashboardLoader } from './saved_dashboards'; import { DashboardConstants } from './dashboard_constants'; import { addEmbeddableToDashboardUrl } from './url_utils/url_helper'; import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder'; -import { createDashboardContainerByValueRenderer } from './application'; +import { UrlGeneratorState } from '../../share/public'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { - [DASHBOARD_APP_URL_GENERATOR]: DashboardAppLinkGeneratorState; + [DASHBOARD_APP_URL_GENERATOR]: UrlGeneratorState; } } diff --git a/src/plugins/dashboard/public/url_generator.ts b/src/plugins/dashboard/public/url_generator.ts index 188de7fd857be..68a50396e00d6 100644 --- a/src/plugins/dashboard/public/url_generator.ts +++ b/src/plugins/dashboard/public/url_generator.ts @@ -26,7 +26,7 @@ import { RefreshInterval, } from '../../data/public'; import { setStateToKbnUrl } from '../../kibana_utils/public'; -import { UrlGeneratorsDefinition, UrlGeneratorState } from '../../share/public'; +import { UrlGeneratorsDefinition } from '../../share/public'; import { SavedObjectLoader } from '../../saved_objects/public'; import { ViewMode } from '../../embeddable/public'; @@ -35,7 +35,7 @@ export const GLOBAL_STATE_STORAGE_KEY = '_g'; export const DASHBOARD_APP_URL_GENERATOR = 'DASHBOARD_APP_URL_GENERATOR'; -export type DashboardAppLinkGeneratorState = UrlGeneratorState<{ +export interface DashboardUrlGeneratorState { /** * If given, the dashboard saved object with this id will be loaded. If not given, * a new, unsaved dashboard will be loaded up. @@ -79,7 +79,7 @@ export type DashboardAppLinkGeneratorState = UrlGeneratorState<{ * View mode of the dashboard. */ viewMode?: ViewMode; -}>; +} export const createDashboardUrlGenerator = ( getStartServices: () => Promise<{ diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 846471420327f..e95150e8f6f73 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -58,6 +58,7 @@ import { changeTimeFilter, mapAndFlattenFilters, extractTimeFilter, + extractTimeRange, convertRangeFilterToTimeRangeString, } from './query'; @@ -99,6 +100,7 @@ export const esFilters = { convertRangeFilterToTimeRangeString, mapAndFlattenFilters, extractTimeFilter, + extractTimeRange, }; export { diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index a8868c07061c3..65670bc1cf83e 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -499,6 +499,7 @@ export const esFilters: { convertRangeFilterToTimeRangeString: typeof convertRangeFilterToTimeRangeString; mapAndFlattenFilters: (filters: import("../common").Filter[]) => import("../common").Filter[]; extractTimeFilter: typeof extractTimeFilter; + extractTimeRange: typeof extractTimeRange; }; // Warning: (ae-missing-release-tag) "esKuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1973,52 +1974,53 @@ export const UI_SETTINGS: { // src/plugins/data/common/es_query/filters/match_all_filter.ts:28:3 - (ae-forgotten-export) The symbol "MatchAllFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrase_filter.ts:33:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrases_filter.ts:31:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "FilterLabel" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "getDisplayValueFromFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "generateFilters" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "changeTimeFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "convertRangeFilterToTimeRangeString" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "extractTimeFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:136:21 - (ae-forgotten-export) The symbol "buildEsQuery" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:136:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:136:21 - (ae-forgotten-export) The symbol "luceneStringToDsl" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:136:21 - (ae-forgotten-export) The symbol "decorateQuery" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:176:26 - (ae-forgotten-export) The symbol "FieldFormatsRegistry" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:176:26 - (ae-forgotten-export) The symbol "BoolFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:176:26 - (ae-forgotten-export) The symbol "BytesFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:176:26 - (ae-forgotten-export) The symbol "ColorFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:176:26 - (ae-forgotten-export) The symbol "DurationFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:176:26 - (ae-forgotten-export) The symbol "IpFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:176:26 - (ae-forgotten-export) The symbol "NumberFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:176:26 - (ae-forgotten-export) The symbol "PercentFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:176:26 - (ae-forgotten-export) The symbol "RelativeDateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:176:26 - (ae-forgotten-export) The symbol "SourceFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:176:26 - (ae-forgotten-export) The symbol "StaticLookupFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:176:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:176:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:176:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:232:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:232:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:232:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:232:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:232:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:232:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:369:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:369:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:369:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:369:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:371:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:372:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:381:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:382:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:383:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:384:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:392:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:393:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:396:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FilterLabel" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "getDisplayValueFromFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "generateFilters" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "changeTimeFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "convertRangeFilterToTimeRangeString" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "extractTimeFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "extractTimeRange" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:138:21 - (ae-forgotten-export) The symbol "buildEsQuery" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:138:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:138:21 - (ae-forgotten-export) The symbol "luceneStringToDsl" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:138:21 - (ae-forgotten-export) The symbol "decorateQuery" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "FieldFormatsRegistry" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "BoolFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "BytesFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "ColorFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "DurationFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "IpFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "NumberFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "PercentFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "RelativeDateFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "SourceFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "StaticLookupFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:371:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:371:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:371:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:371:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:373:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:374:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:383:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:384:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:385:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:386:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:394:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:395:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:398:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:41:60 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/types.ts:54:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts // src/plugins/data/public/types.ts:55:5 - (ae-forgotten-export) The symbol "createFiltersFromRangeSelectAction" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/query/timefilter/index.ts b/src/plugins/data/public/query/timefilter/index.ts index 19386c10ab59f..dc9a4ef8c21a6 100644 --- a/src/plugins/data/public/query/timefilter/index.ts +++ b/src/plugins/data/public/query/timefilter/index.ts @@ -23,5 +23,5 @@ export * from './types'; export { Timefilter, TimefilterContract } from './timefilter'; export { TimeHistory, TimeHistoryContract } from './time_history'; export { changeTimeFilter, convertRangeFilterToTimeRangeString } from './lib/change_time_filter'; -export { extractTimeFilter } from './lib/extract_time_filter'; +export { extractTimeFilter, extractTimeRange } from './lib/extract_time_filter'; export { validateTimeRange } from './lib/validate_timerange'; diff --git a/src/plugins/data/public/query/timefilter/lib/extract_time_filter.ts b/src/plugins/data/public/query/timefilter/lib/extract_time_filter.ts index 23dd1547baf10..2f93196e3218b 100644 --- a/src/plugins/data/public/query/timefilter/lib/extract_time_filter.ts +++ b/src/plugins/data/public/query/timefilter/lib/extract_time_filter.ts @@ -18,7 +18,8 @@ */ import { keys, partition } from 'lodash'; -import { Filter, isRangeFilter, RangeFilter } from '../../../../common'; +import { Filter, isRangeFilter, RangeFilter, TimeRange } from '../../../../common'; +import { convertRangeFilterToTimeRangeString } from './change_time_filter'; export function extractTimeFilter(timeFieldName: string, filters: Filter[]) { const [timeRangeFilter, restOfFilters] = partition(filters, (obj: Filter) => { @@ -36,3 +37,15 @@ export function extractTimeFilter(timeFieldName: string, filters: Filter[]) { timeRangeFilter: timeRangeFilter[0] as RangeFilter | undefined, }; } + +export function extractTimeRange( + filters: Filter[], + timeFieldName?: string +): { restOfFilters: Filter[]; timeRange?: TimeRange } { + if (!timeFieldName) return { restOfFilters: filters, timeRange: undefined }; + const { timeRangeFilter, restOfFilters } = extractTimeFilter(timeFieldName, filters); + return { + restOfFilters, + timeRange: timeRangeFilter ? convertRangeFilterToTimeRangeString(timeRangeFilter) : undefined, + }; +} diff --git a/x-pack/plugins/dashboard_enhanced/kibana.json b/x-pack/plugins/dashboard_enhanced/kibana.json index ba5d8052ca787..264fa0438ea11 100644 --- a/x-pack/plugins/dashboard_enhanced/kibana.json +++ b/x-pack/plugins/dashboard_enhanced/kibana.json @@ -8,6 +8,7 @@ "requiredBundles": [ "kibanaUtils", "embeddableEnhanced", - "kibanaReact" + "kibanaReact", + "uiActions" ] } diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx index 4804a700c6cff..2de862a6708a8 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx @@ -6,7 +6,12 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { ActionByType } from '../../../../../../../../src/plugins/ui_actions/public'; +import { + ActionByType, + APPLY_FILTER_TRIGGER, + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, +} from '../../../../../../../../src/plugins/ui_actions/public'; import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; import { isEnhancedEmbeddable } from '../../../../../../embeddable_enhanced/public'; import { EmbeddableContext } from '../../../../../../../../src/plugins/embeddable/public'; @@ -42,7 +47,9 @@ export class FlyoutCreateDrilldownAction implements ActionByType -1; + return supportedTriggers.some((trigger) => + [VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER, APPLY_FILTER_TRIGGER].includes(trigger) + ); } public async isCompatible(context: EmbeddableContext) { diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/constants.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/constants.ts index e2a530b156da5..daefcf2d68cc5 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/constants.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/constants.ts @@ -4,4 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +/** + * note: + * don't change this string without carefull consideration, + * because it is stored in saved objects. + * Also temporary dashboard drilldown migration code inside embeddable plugin relies on it + * x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts + */ export const DASHBOARD_TO_DASHBOARD_DRILLDOWN = 'DASHBOARD_TO_DASHBOARD_DRILLDOWN'; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx index 52b232afa9410..40fa469feb34b 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx @@ -5,9 +5,8 @@ */ import { DashboardToDashboardDrilldown } from './drilldown'; -import { savedObjectsServiceMock, coreMock } from '../../../../../../../src/core/public/mocks'; -import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; -import { ActionContext, Config } from './types'; +import { Config } from './types'; +import { coreMock, savedObjectsServiceMock } from '../../../../../../../src/core/public/mocks'; import { Filter, FilterStateStore, @@ -15,16 +14,13 @@ import { RangeFilter, TimeRange, } from '../../../../../../../src/plugins/data/common'; -import { esFilters } from '../../../../../../../src/plugins/data/public'; - +import { + ApplyGlobalFilterActionContext, + esFilters, +} from '../../../../../../../src/plugins/data/public'; // convenient to use real implementation here. import { createDashboardUrlGenerator } from '../../../../../../../src/plugins/dashboard/public/url_generator'; import { UrlGeneratorsService } from '../../../../../../../src/plugins/share/public/url_generators'; -import { VisualizeEmbeddableContract } from '../../../../../../../src/plugins/visualizations/public'; -import { - RangeSelectContext, - ValueClickContext, -} from '../../../../../../../src/plugins/embeddable/public'; import { StartDependencies } from '../../../plugin'; import { SavedObjectLoader } from '../../../../../../../src/plugins/saved_objects/public'; import { StartServicesGetter } from '../../../../../../../src/plugins/kibana_utils/public/core'; @@ -82,11 +78,10 @@ describe('.execute() & getHref', () => { config: Partial, embeddableInput: { filters?: Filter[]; timeRange?: TimeRange; query?: Query }, filtersFromEvent: Filter[], - useRangeEvent = false + timeFieldName?: string ) { const navigateToApp = jest.fn(); const getUrlForApp = jest.fn((app, opt) => `${app}/${opt.path}`); - const dataPluginActions = dataPluginMock.createStartContract().actions; const savedObjectsClient = savedObjectsServiceMock.createStartContract().client; const drilldown = new DashboardToDashboardDrilldown({ @@ -102,9 +97,6 @@ describe('.execute() & getHref', () => { }, plugins: { uiActionsEnhanced: {}, - data: { - actions: dataPluginActions, - }, }, self: {}, })) as unknown) as StartServicesGetter>, @@ -119,12 +111,6 @@ describe('.execute() & getHref', () => { ) ), }); - const selectRangeFiltersSpy = jest - .spyOn(dataPluginActions, 'createFiltersFromRangeSelectAction') - .mockImplementation(() => Promise.resolve(filtersFromEvent)); - const valueClickFiltersSpy = jest - .spyOn(dataPluginActions, 'createFiltersFromValueClickAction') - .mockImplementation(() => Promise.resolve(filtersFromEvent)); const completeConfig: Config = { dashboardId: 'id', @@ -134,12 +120,7 @@ describe('.execute() & getHref', () => { }; const context = ({ - data: { - ...(useRangeEvent - ? ({ range: {} } as RangeSelectContext['data']) - : ({ data: [] } as ValueClickContext['data'])), - timeFieldName: 'order_date', - }, + filters: filtersFromEvent, embeddable: { getInput: () => ({ filters: [], @@ -148,18 +129,11 @@ describe('.execute() & getHref', () => { ...embeddableInput, }), }, - } as unknown) as ActionContext; + timeFieldName, + } as unknown) as ApplyGlobalFilterActionContext; await drilldown.execute(completeConfig, context); - if (useRangeEvent) { - expect(selectRangeFiltersSpy).toBeCalledTimes(1); - expect(valueClickFiltersSpy).toBeCalledTimes(0); - } else { - expect(selectRangeFiltersSpy).toBeCalledTimes(0); - expect(valueClickFiltersSpy).toBeCalledTimes(1); - } - expect(navigateToApp).toBeCalledTimes(1); expect(navigateToApp.mock.calls[0][0]).toBe('dashboards'); @@ -180,8 +154,7 @@ describe('.execute() & getHref', () => { dashboardId: testDashboardId, }, {}, - [], - false + [] ); expect(href).toEqual(expect.stringContaining(`view/${testDashboardId}`)); @@ -289,8 +262,7 @@ describe('.execute() & getHref', () => { to: 'now', }, }, - [], - false + [] ); expect(href).not.toEqual(expect.stringContaining('now-300m')); @@ -308,7 +280,7 @@ describe('.execute() & getHref', () => { }, }, [getMockTimeRangeFilter()], - true + getMockTimeRangeFilter().meta.key ); expect(href).not.toEqual(expect.stringContaining('now-300m')); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx index 26a69132cffb1..703acbc8d9d59 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx @@ -6,20 +6,24 @@ import React from 'react'; import { reactToUiComponent } from '../../../../../../../src/plugins/kibana_react/public'; -import { DashboardUrlGenerator } from '../../../../../../../src/plugins/dashboard/public'; -import { ActionContext, Config } from './types'; +import { + DashboardUrlGenerator, + DashboardUrlGeneratorState, +} from '../../../../../../../src/plugins/dashboard/public'; import { CollectConfigContainer } from './components'; import { DASHBOARD_TO_DASHBOARD_DRILLDOWN } from './constants'; import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../../ui_actions_enhanced/public'; import { txtGoToDashboard } from './i18n'; -import { esFilters } from '../../../../../../../src/plugins/data/public'; -import { VisualizeEmbeddableContract } from '../../../../../../../src/plugins/visualizations/public'; import { - isRangeSelectTriggerContext, - isValueClickTriggerContext, -} from '../../../../../../../src/plugins/embeddable/public'; + ApplyGlobalFilterActionContext, + esFilters, + isFilters, + isQuery, + isTimeRange, +} from '../../../../../../../src/plugins/data/public'; import { StartServicesGetter } from '../../../../../../../src/plugins/kibana_utils/public'; import { StartDependencies } from '../../../plugin'; +import { Config } from './types'; export interface Params { start: StartServicesGetter>; @@ -27,7 +31,7 @@ export interface Params { } export class DashboardToDashboardDrilldown - implements Drilldown> { + implements Drilldown { constructor(protected readonly params: Params) {} public readonly id = DASHBOARD_TO_DASHBOARD_DRILLDOWN; @@ -57,15 +61,12 @@ export class DashboardToDashboardDrilldown public readonly getHref = async ( config: Config, - context: ActionContext + context: ApplyGlobalFilterActionContext ): Promise => { return this.getDestinationUrl(config, context); }; - public readonly execute = async ( - config: Config, - context: ActionContext - ) => { + public readonly execute = async (config: Config, context: ApplyGlobalFilterActionContext) => { const dashboardPath = await this.getDestinationUrl(config, context); const dashboardHash = dashboardPath.split('#')[1]; @@ -76,73 +77,43 @@ export class DashboardToDashboardDrilldown private getDestinationUrl = async ( config: Config, - context: ActionContext + context: ApplyGlobalFilterActionContext ): Promise => { + const state: DashboardUrlGeneratorState = { + dashboardId: config.dashboardId, + }; + + if (context.embeddable) { + const input = context.embeddable.getInput(); + if (isQuery(input.query) && config.useCurrentFilters) state.query = input.query; + + // if useCurrentDashboardDataRange is enabled, then preserve current time range + // if undefined is passed, then destination dashboard will figure out time range itself + // for brush event this time range would be overwritten + if (isTimeRange(input.timeRange) && config.useCurrentDateRange) + state.timeRange = input.timeRange; + + // if useCurrentDashboardFilters enabled, then preserve all the filters (pinned and unpinned) + // otherwise preserve only pinned + if (isFilters(input.filters)) + state.filters = config.useCurrentFilters + ? input.filters + : input.filters?.filter((f) => esFilters.isFilterPinned(f)); + } + const { - createFiltersFromRangeSelectAction, - createFiltersFromValueClickAction, - } = this.params.start().plugins.data.actions; - const { - timeRange: currentTimeRange, - query, - filters: currentFilters, - } = context.embeddable!.getInput(); - - // if useCurrentDashboardFilters enabled, then preserve all the filters (pinned and unpinned) - // otherwise preserve only pinned - const existingFilters = - (config.useCurrentFilters - ? currentFilters - : currentFilters?.filter((f) => esFilters.isFilterPinned(f))) ?? []; - - // if useCurrentDashboardDataRange is enabled, then preserve current time range - // if undefined is passed, then destination dashboard will figure out time range itself - // for brush event this time range would be overwritten - let timeRange = config.useCurrentDateRange ? currentTimeRange : undefined; - let filtersFromEvent = await (async () => { - try { - if (isRangeSelectTriggerContext(context)) - return await createFiltersFromRangeSelectAction(context.data); - if (isValueClickTriggerContext(context)) - return await createFiltersFromValueClickAction(context.data); - - // eslint-disable-next-line no-console - console.warn( - ` - DashboardToDashboard drilldown: can't extract filters from action. - Is it not supported action?`, - context - ); - - return []; - } catch (e) { - // eslint-disable-next-line no-console - console.warn( - ` - DashboardToDashboard drilldown: error extracting filters from action. - Continuing without applying filters from event`, - e - ); - return []; - } - })(); - - if (context.data.timeFieldName) { - const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( - context.data.timeFieldName, - filtersFromEvent - ); - filtersFromEvent = restOfFilters; - if (timeRangeFilter) { - timeRange = esFilters.convertRangeFilterToTimeRangeString(timeRangeFilter); - } + restOfFilters: filtersFromEvent, + timeRange: timeRangeFromEvent, + } = esFilters.extractTimeRange(context.filters, context.timeFieldName); + + if (filtersFromEvent) { + state.filters = [...(state.filters ?? []), ...filtersFromEvent]; } - return this.params.getDashboardUrlGenerator().createUrl({ - dashboardId: config.dashboardId, - query: config.useCurrentFilters ? query : undefined, - timeRange, - filters: [...existingFilters, ...filtersFromEvent], - }); + if (timeRangeFromEvent) { + state.timeRange = timeRangeFromEvent; + } + + return this.params.getDashboardUrlGenerator().createUrl(state); }; } diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/index.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/index.ts index 914f34980a272..49065a96b4f7b 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/index.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/index.ts @@ -9,7 +9,4 @@ export { DashboardToDashboardDrilldown, Params as DashboardToDashboardDrilldownParams, } from './drilldown'; -export { - ActionContext as DashboardToDashboardActionContext, - Config as DashboardToDashboardConfig, -} from './types'; +export { Config } from './types'; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts index 6be2e2a77269f..426e250499de0 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts @@ -4,16 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - ValueClickContext, - RangeSelectContext, - IEmbeddable, -} from '../../../../../../../src/plugins/embeddable/public'; - -export type ActionContext = - | ValueClickContext - | RangeSelectContext; - export interface Config { dashboardId?: string; useCurrentFilters: boolean; diff --git a/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.test.ts b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.test.ts index 5c5d98d75295d..fffb75451f8ac 100644 --- a/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.test.ts +++ b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.test.ts @@ -11,6 +11,9 @@ import { } from './embeddable_action_storage'; import { UiActionsEnhancedSerializedEvent } from '../../../ui_actions_enhanced/public'; import { of } from '../../../../../src/plugins/kibana_utils/public'; +// use real const to make test fail in case someone accidentally changes it +import { DASHBOARD_TO_DASHBOARD_DRILLDOWN } from '../../../dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown'; +import { APPLY_FILTER_TRIGGER } from '../../../../../src/plugins/ui_actions/public'; class TestEmbeddable extends Embeddable { public readonly type = 'test'; @@ -539,4 +542,42 @@ describe('EmbeddableActionStorage', () => { expect(await storage.list()).toEqual([]); }); }); + + describe('migrate', () => { + test('DASHBOARD_TO_DASHBOARD_DRILLDOWN triggers migration', async () => { + const embeddable = new TestEmbeddable(); + const OTHER_TRIGGER = 'OTHER_TRIGGER'; + embeddable.updateInput({ + enhancements: { + dynamicActions: { + events: [ + { + eventId: '1', + triggers: [OTHER_TRIGGER], + action: { + factoryId: DASHBOARD_TO_DASHBOARD_DRILLDOWN, + name: '', + config: {}, + }, + }, + { + eventId: '2', + triggers: [OTHER_TRIGGER], + action: { + factoryId: 'SOME_OTHER', + name: '', + config: {}, + }, + }, + ], + }, + }, + }); + const storage = new EmbeddableActionStorage(embeddable); + + const [event1, event2] = await storage.list(); + expect(event1.triggers).toEqual([APPLY_FILTER_TRIGGER]); + expect(event2.triggers).toEqual([OTHER_TRIGGER]); + }); + }); }); diff --git a/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts index fdc42585a80ce..8881b2063c8db 100644 --- a/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts +++ b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts @@ -46,7 +46,7 @@ export class EmbeddableActionStorage extends AbstractActionStorage { public async create(event: SerializedEvent) { const input = this.embbeddable.getInput(); - const events = input.enhancements?.dynamicActions?.events || []; + const events = this.getEventsFromEmbeddable(); const exists = !!events.find(({ eventId }) => eventId === event.eventId); if (exists) { @@ -61,7 +61,7 @@ export class EmbeddableActionStorage extends AbstractActionStorage { public async update(event: SerializedEvent) { const input = this.embbeddable.getInput(); - const events = input.enhancements?.dynamicActions?.events || []; + const events = this.getEventsFromEmbeddable(); const index = events.findIndex(({ eventId }) => eventId === event.eventId); if (index === -1) { @@ -77,7 +77,7 @@ export class EmbeddableActionStorage extends AbstractActionStorage { public async remove(eventId: string) { const input = this.embbeddable.getInput(); - const events = input.enhancements?.dynamicActions?.events || []; + const events = this.getEventsFromEmbeddable(); const index = events.findIndex((event) => eventId === event.eventId); if (index === -1) { @@ -93,7 +93,7 @@ export class EmbeddableActionStorage extends AbstractActionStorage { public async read(eventId: string): Promise { const input = this.embbeddable.getInput(); - const events = input.enhancements?.dynamicActions?.events || []; + const events = this.getEventsFromEmbeddable(); const event = events.find((ev) => eventId === ev.eventId); if (!event) { @@ -107,8 +107,28 @@ export class EmbeddableActionStorage extends AbstractActionStorage { } public async list(): Promise { + return this.getEventsFromEmbeddable(); + } + + private getEventsFromEmbeddable() { const input = this.embbeddable.getInput(); const events = input.enhancements?.dynamicActions?.events || []; - return events; + return this.migrate(events); + } + + // TODO: https://github.com/elastic/kibana/issues/71431 + // Migration implementation should use registry + // Action factories implementations should register own migrations + private migrate(events: SerializedEvent[]): SerializedEvent[] { + return events.map((event) => { + // Initially dashboard drilldown relied on VALUE_CLICK & RANGE_SELECT + if (event.action.factoryId === 'DASHBOARD_TO_DASHBOARD_DRILLDOWN') { + return { + ...event, + triggers: ['FILTER_TRIGGER'], + }; + } + return event; + }); } } diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx index 20d15b4f4d2bd..283464b137ff9 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx @@ -11,9 +11,8 @@ import { DrilldownWizardConfig, FlyoutDrilldownWizard } from '../flyout_drilldow import { FlyoutListManageDrilldowns } from '../flyout_list_manage_drilldowns'; import { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/public'; import { - VALUE_CLICK_TRIGGER, - SELECT_RANGE_TRIGGER, TriggerContextMapping, + APPLY_FILTER_TRIGGER, } from '../../../../../../../src/plugins/ui_actions/public'; import { useContainerState } from '../../../../../../../src/plugins/kibana_utils/public'; import { DrilldownListItem } from '../list_manage_drilldowns'; @@ -67,8 +66,9 @@ export function createFlyoutManageDrilldowns({ return (props: ConnectedFlyoutManageDrilldownsProps) => { const isCreateOnly = props.viewMode === 'create'; + // TODO: https://github.com/elastic/kibana/issues/59569 const selectedTriggers: Array = React.useMemo( - () => [VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER], + () => [APPLY_FILTER_TRIGGER], [] ); From 5ea28702f6a2aa3e0592a79fa5ea396ff68fd972 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 28 Jul 2020 11:15:58 +0300 Subject: [PATCH 71/96] [Functional Tests] Increase the timeout when locating the tableview] (#73243) --- test/functional/page_objects/visual_builder_page.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index 0db8cac0f0758..8488eb8cd2749 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -408,7 +408,7 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro * @memberof VisualBuilderPage */ public async getViewTable(): Promise { - const tableView = await testSubjects.find('tableView'); + const tableView = await testSubjects.find('tableView', 20000); return await tableView.getVisibleText(); } From c0826a32730ba55c0192e81fc23788be5966fdcd Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 28 Jul 2020 11:37:37 +0300 Subject: [PATCH 72/96] Fix App status flaky test (#72853) * wait for link to be updated * await, please! Co-authored-by: Elastic Machine --- .../plugins/core_app_status/public/plugin.tsx | 3 +-- .../core_plugins/application_status.ts | 16 +++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/test/plugin_functional/plugins/core_app_status/public/plugin.tsx b/test/plugin_functional/plugins/core_app_status/public/plugin.tsx index af23bfbe1f8f5..bdc08c03c1912 100644 --- a/test/plugin_functional/plugins/core_app_status/public/plugin.tsx +++ b/test/plugin_functional/plugins/core_app_status/public/plugin.tsx @@ -26,6 +26,7 @@ import { CoreStart, AppMountParameters, } from 'kibana/public'; +import { renderApp } from './application'; import './types'; export class CoreAppStatusPlugin implements Plugin<{}, CoreAppStatusPluginStart> { @@ -36,7 +37,6 @@ export class CoreAppStatusPlugin implements Plugin<{}, CoreAppStatusPluginStart> id: 'app_status_start', title: 'App Status Start Page', async mount(params: AppMountParameters) { - const { renderApp } = await import('./application'); return renderApp('app_status_start', params); }, }); @@ -47,7 +47,6 @@ export class CoreAppStatusPlugin implements Plugin<{}, CoreAppStatusPluginStart> euiIconType: 'snowflake', updater$: this.appUpdater, async mount(params: AppMountParameters) { - const { renderApp } = await import('./application'); return renderApp('app_status', params); }, }); diff --git a/test/plugin_functional/test_suites/core_plugins/application_status.ts b/test/plugin_functional/test_suites/core_plugins/application_status.ts index 31a1c28b50842..a4c2db733b894 100644 --- a/test/plugin_functional/test_suites/core_plugins/application_status.ts +++ b/test/plugin_functional/test_suites/core_plugins/application_status.ts @@ -41,6 +41,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const PageObjects = getPageObjects(['common']); const browser = getService('browser'); const appsMenu = getService('appsMenu'); + const retry = getService('retry'); const testSubjects = getService('testSubjects'); const setAppStatus = async (s: Partial) => { @@ -50,15 +51,14 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide }, s); }; - const navigateToApp = async (i: string) => { + const navigateToApp = async (id: string) => { return await browser.executeAsync(async (appId, cb) => { await window.__coreAppStatus.navigateToApp(appId); cb(); - }, i); + }, id); }; - // FLAKY: https://github.com/elastic/kibana/issues/65423 - describe.skip('application status management', () => { + describe('application status management', () => { beforeEach(async () => { await PageObjects.common.navigateToApp('app_status_start'); }); @@ -101,15 +101,17 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide }); it('allows to change the defaultPath of an application', async () => { - let link = await appsMenu.getLink('App Status'); + const link = await appsMenu.getLink('App Status'); expect(link!.href).to.eql(getKibanaUrl('/app/app_status')); await setAppStatus({ defaultPath: '/arbitrary/path', }); - link = await appsMenu.getLink('App Status'); - expect(link!.href).to.eql(getKibanaUrl('/app/app_status/arbitrary/path')); + await retry.waitFor('link url updated with "defaultPath"', async () => { + const updatedLink = await appsMenu.getLink('App Status'); + return updatedLink?.href === getKibanaUrl('/app/app_status/arbitrary/path'); + }); await navigateToApp('app_status'); expect(await testSubjects.exists('appStatusApp')).to.eql(true); From 1c791f39dac906f3a46b3703a82ba33e8f263a4b Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 28 Jul 2020 10:48:14 +0200 Subject: [PATCH 73/96] [SIEM][Timelines] Updates timeline template callout text (#73334) * updates timeline template callout text * fixes typo in constant Co-authored-by: Elastic Machine --- .../timelines/components/timeline/header/index.test.tsx | 2 +- .../public/timelines/components/timeline/header/index.tsx | 2 +- .../timelines/components/timeline/header/translations.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx index e0043f3b232da..e7b0ce7b7428e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx @@ -177,7 +177,7 @@ describe('Header', () => { expect( wrapper.find('[data-test-subj="timelineImmutableCallOut"]').first().prop('title') ).toEqual( - 'This timeline is immutable, therefore not allowed to save it within the security application, though you may continue to use the timeline to search and filter security events' + 'This prebuilt timeline template cannot be modified. To make changes, please duplicate this template and make modifications to the duplicate template.' ); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx index 75bfb52f2756b..e50a6ed1e45fe 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx @@ -73,7 +73,7 @@ const TimelineHeaderComponent: React.FC = ({ {status === TimelineStatus.immutable && ( Date: Tue, 28 Jul 2020 13:00:16 +0300 Subject: [PATCH 74/96] [Search] add server logs (#72454) * improve test stability * logs and scope search function * uncomment * fix ts * ts Co-authored-by: Elastic Machine --- src/plugins/data/server/plugin.ts | 4 ++-- .../es_search/es_search_strategy.test.ts | 11 +++++---- .../search/es_search/es_search_strategy.ts | 6 +++-- .../data/server/search/search_service.test.ts | 5 +++- .../data/server/search/search_service.ts | 24 +++++++++++++++---- x-pack/plugins/data_enhanced/server/plugin.ts | 12 ++++++++-- .../server/search/es_search_strategy.test.ts | 13 ++++++---- .../server/search/es_search_strategy.ts | 6 ++++- 8 files changed, 60 insertions(+), 21 deletions(-) diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index 8fa32f9bd564f..61d8e566d2d2b 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -62,11 +62,11 @@ export class DataServerPlugin implements Plugin) { - this.searchService = new SearchService(initializerContext); + this.logger = initializerContext.logger.get('data'); + this.searchService = new SearchService(initializerContext, this.logger); this.scriptsService = new ScriptsService(); this.kqlTelemetryService = new KqlTelemetryService(initializerContext); this.autocompleteService = new AutocompleteService(initializerContext); - this.logger = initializerContext.logger.get('data'); } public setup( diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts index 1155a5491e8f3..bc59bdee6a40a 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts @@ -22,6 +22,9 @@ import { pluginInitializerContextConfigMock } from '../../../../../core/server/m import { esSearchStrategyProvider } from './es_search_strategy'; describe('ES search strategy', () => { + const mockLogger: any = { + info: () => {}, + }; const mockApiCaller = jest.fn().mockResolvedValue({ _shards: { total: 10, @@ -40,14 +43,14 @@ describe('ES search strategy', () => { }); it('returns a strategy with `search`', async () => { - const esSearch = await esSearchStrategyProvider(mockConfig$); + const esSearch = await esSearchStrategyProvider(mockConfig$, mockLogger); expect(typeof esSearch.search).toBe('function'); }); it('calls the API caller with the params with defaults', async () => { const params = { index: 'logstash-*' }; - const esSearch = await esSearchStrategyProvider(mockConfig$); + const esSearch = await esSearchStrategyProvider(mockConfig$, mockLogger); await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params }); @@ -63,7 +66,7 @@ describe('ES search strategy', () => { it('calls the API caller with overridden defaults', async () => { const params = { index: 'logstash-*', ignoreUnavailable: false, timeout: '1000ms' }; - const esSearch = await esSearchStrategyProvider(mockConfig$); + const esSearch = await esSearchStrategyProvider(mockConfig$, mockLogger); await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params }); @@ -77,7 +80,7 @@ describe('ES search strategy', () => { it('returns total, loaded, and raw response', async () => { const params = { index: 'logstash-*' }; - const esSearch = await esSearchStrategyProvider(mockConfig$); + const esSearch = await esSearchStrategyProvider(mockConfig$, mockLogger); const response = await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params, diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.ts b/src/plugins/data/server/search/es_search/es_search_strategy.ts index 82f8ef21ebb38..b8010f735c327 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.ts @@ -17,16 +17,18 @@ * under the License. */ import { first } from 'rxjs/operators'; -import { SharedGlobalConfig } from 'kibana/server'; +import { SharedGlobalConfig, Logger } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { Observable } from 'rxjs'; import { ISearchStrategy, getDefaultSearchParams, getTotalLoaded } from '..'; export const esSearchStrategyProvider = ( - config$: Observable + config$: Observable, + logger: Logger ): ISearchStrategy => { return { search: async (context, request, options) => { + logger.info(`search ${JSON.stringify(request.params)}`); const config = await config$.pipe(first()).toPromise(); const defaultParams = getDefaultSearchParams(config); diff --git a/src/plugins/data/server/search/search_service.test.ts b/src/plugins/data/server/search/search_service.test.ts index 8c2ed96503003..be00b7409fe4a 100644 --- a/src/plugins/data/server/search/search_service.test.ts +++ b/src/plugins/data/server/search/search_service.test.ts @@ -28,7 +28,10 @@ describe('Search service', () => { let mockCoreSetup: MockedKeys>; beforeEach(() => { - plugin = new SearchService(coreMock.createPluginInitializerContext({})); + const mockLogger: any = { + info: () => {}, + }; + plugin = new SearchService(coreMock.createPluginInitializerContext({}), mockLogger); mockCoreSetup = coreMock.createSetup(); }); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 5686023e9a667..bbd0671754749 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -22,6 +22,7 @@ import { PluginInitializerContext, CoreSetup, RequestHandlerContext, + Logger, } from '../../../../core/server'; import { ISearchSetup, ISearchStart, ISearchStrategy } from './types'; import { registerSearchRoute } from './routes'; @@ -41,7 +42,10 @@ interface StrategyMap { export class SearchService implements Plugin { private searchStrategies: StrategyMap = {}; - constructor(private initializerContext: PluginInitializerContext) {} + constructor( + private initializerContext: PluginInitializerContext, + private readonly logger: Logger + ) {} public setup( core: CoreSetup, @@ -49,7 +53,7 @@ export class SearchService implements Plugin { ): ISearchSetup { this.registerSearchStrategy( ES_SEARCH_STRATEGY, - esSearchStrategyProvider(this.initializerContext.config.legacy.globalConfig$) + esSearchStrategyProvider(this.initializerContext.config.legacy.globalConfig$, this.logger) ); core.savedObjects.registerType(searchTelemetry); @@ -65,7 +69,11 @@ export class SearchService implements Plugin { return { registerSearchStrategy: this.registerSearchStrategy, usage }; } - private search(context: RequestHandlerContext, searchRequest: IEsSearchRequest, options: any) { + private search( + context: RequestHandlerContext, + searchRequest: IEsSearchRequest, + options: Record + ) { return this.getSearchStrategy(options.strategy || ES_SEARCH_STRATEGY).search( context, searchRequest, @@ -76,17 +84,25 @@ export class SearchService implements Plugin { public start(): ISearchStart { return { getSearchStrategy: this.getSearchStrategy, - search: this.search, + search: ( + context: RequestHandlerContext, + searchRequest: IEsSearchRequest, + options: Record + ) => { + return this.search(context, searchRequest, options); + }, }; } public stop() {} private registerSearchStrategy = (name: string, strategy: ISearchStrategy) => { + this.logger.info(`Register strategy ${name}`); this.searchStrategies[name] = strategy; }; private getSearchStrategy = (name: string): ISearchStrategy => { + this.logger.info(`Get strategy ${name}`); const strategy = this.searchStrategies[name]; if (!strategy) { throw new Error(`Search strategy ${name} not found`); diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts index 4f6756231912c..9c3a0edf7e733 100644 --- a/x-pack/plugins/data_enhanced/server/plugin.ts +++ b/x-pack/plugins/data_enhanced/server/plugin.ts @@ -9,6 +9,7 @@ import { CoreSetup, CoreStart, Plugin, + Logger, } from '../../../../src/core/server'; import { ES_SEARCH_STRATEGY } from '../../../../src/plugins/data/common'; import { PluginSetup as DataPluginSetup } from '../../../../src/plugins/data/server'; @@ -19,12 +20,19 @@ interface SetupDependencies { } export class EnhancedDataServerPlugin implements Plugin { - constructor(private initializerContext: PluginInitializerContext) {} + private readonly logger: Logger; + + constructor(private initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get('data_enhanced'); + } public setup(core: CoreSetup, deps: SetupDependencies) { deps.data.search.registerSearchStrategy( ES_SEARCH_STRATEGY, - enhancedEsSearchStrategyProvider(this.initializerContext.config.legacy.globalConfig$) + enhancedEsSearchStrategyProvider( + this.initializerContext.config.legacy.globalConfig$, + this.logger + ) ); } diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts index 1eec941466b73..faa4f2ee499e5 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts @@ -31,6 +31,9 @@ const mockRollupResponse = { describe('ES search strategy', () => { const mockApiCaller = jest.fn(); + const mockLogger: any = { + info: () => {}, + }; const mockContext = { core: { elasticsearch: { legacy: { client: { callAsCurrentUser: mockApiCaller } } } }, }; @@ -41,7 +44,7 @@ describe('ES search strategy', () => { }); it('returns a strategy with `search`', async () => { - const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$); + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); expect(typeof esSearch.search).toBe('function'); }); @@ -50,7 +53,7 @@ describe('ES search strategy', () => { mockApiCaller.mockResolvedValueOnce(mockAsyncResponse); const params = { index: 'logstash-*', body: { query: {} } }; - const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$); + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params }); @@ -66,7 +69,7 @@ describe('ES search strategy', () => { mockApiCaller.mockResolvedValueOnce(mockAsyncResponse); const params = { index: 'logstash-*', body: { query: {} } }; - const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$); + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); await esSearch.search((mockContext as unknown) as RequestHandlerContext, { id: 'foo', params }); @@ -82,7 +85,7 @@ describe('ES search strategy', () => { mockApiCaller.mockResolvedValueOnce(mockAsyncResponse); const params = { index: 'foo-程', body: {} }; - const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$); + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params }); @@ -97,7 +100,7 @@ describe('ES search strategy', () => { mockApiCaller.mockResolvedValueOnce(mockRollupResponse); const params = { index: 'foo-程', body: {} }; - const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$); + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); await esSearch.search((mockContext as unknown) as RequestHandlerContext, { indexType: 'rollup', diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 7b29117495a67..358335a2a4d60 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -12,6 +12,7 @@ import { LegacyAPICaller, SharedGlobalConfig, RequestHandlerContext, + Logger, } from '../../../../../src/core/server'; import { ISearchOptions, @@ -30,13 +31,15 @@ export interface AsyncSearchResponse { } export const enhancedEsSearchStrategyProvider = ( - config$: Observable + config$: Observable, + logger: Logger ): ISearchStrategy => { const search = async ( context: RequestHandlerContext, request: IEnhancedEsSearchRequest, options?: ISearchOptions ) => { + logger.info(`search ${JSON.stringify(request.params) || request.id}`); const config = await config$.pipe(first()).toPromise(); const caller = context.core.elasticsearch.legacy.client.callAsCurrentUser; const defaultParams = getDefaultSearchParams(config); @@ -48,6 +51,7 @@ export const enhancedEsSearchStrategyProvider = ( }; const cancel = async (context: RequestHandlerContext, id: string) => { + logger.info(`cancel ${id}`); const method = 'DELETE'; const path = encodeURI(`/_async_search/${id}`); await context.core.elasticsearch.legacy.client.callAsCurrentUser('transport.request', { From 12d5b8d2f95cc085065def98e614590828adfa7e Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 28 Jul 2020 13:13:01 +0200 Subject: [PATCH 75/96] executes cypress tests when there is a change in parts of alerting team code we use (#73256) --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 69c61b5bfa988..818ba748ee165 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -43,7 +43,7 @@ kibanaPipeline(timeoutMinutes: 155, checkPrChanges: true, setCommitStatus: true) 'xpack-accessibility': kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh'), 'xpack-savedObjectsFieldMetrics': kibanaPipeline.functionalTestProcess('xpack-savedObjectsFieldMetrics', './test/scripts/jenkins_xpack_saved_objects_field_metrics.sh'), 'xpack-securitySolutionCypress': { processNumber -> - whenChanged(['x-pack/plugins/security_solution/', 'x-pack/test/security_solution_cypress/']) { + whenChanged(['x-pack/plugins/security_solution/', 'x-pack/test/security_solution_cypress/', 'x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/', 'x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx']) { kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypress', './test/scripts/jenkins_security_solution_cypress.sh')(processNumber) } }, From 46fb8475f382cff56d10783798d6a3c2d1f3dda2 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 28 Jul 2020 14:38:14 +0300 Subject: [PATCH 76/96] [Security Solutions] Show popovers inside modals (#73264) --- .../security_solution/public/common/components/page/index.tsx | 4 ++-- .../public/common/components/with_hover_actions/index.tsx | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/page/index.tsx b/x-pack/plugins/security_solution/public/common/components/page/index.tsx index 9a5654ed6475f..8737fa95c94a2 100644 --- a/x-pack/plugins/security_solution/public/common/components/page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/page/index.tsx @@ -49,8 +49,8 @@ export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimar border: none; } - /* hide open popovers when a modal is being displayed to prevent them from covering the modal */ - body.euiBody-hasOverlayMask .euiPopover__panel-isOpen { + /* hide open draggable popovers when a modal is being displayed to prevent them from covering the modal */ + body.euiBody-hasOverlayMask .withHoverActions__popover.euiPopover__panel-isOpen{ visibility: hidden !important; } diff --git a/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx b/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx index e6577bd040e25..9e28345ffbbcf 100644 --- a/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx @@ -90,6 +90,7 @@ export const WithHoverActions = React.memo( hasArrow={false} isOpen={isOpen} panelPaddingSize={!alwaysShow ? 's' : 'none'} + panelClassName="withHoverActions__popover" > {isOpen ? <>{hoverContent} : null} From 09b11b61f0fefb736847f5000713bcf6ebfae0b0 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Tue, 28 Jul 2020 07:44:37 -0400 Subject: [PATCH 77/96] Introduce reserved ml privilege for the apm_user role (#72266) Co-authored-by: Elastic Machine --- x-pack/plugins/infra/server/features.ts | 12 ++++----- .../plugins/ml/common/types/capabilities.ts | 16 ++++++++++++ x-pack/plugins/ml/server/plugin.ts | 6 ++++- .../disable_ui_capabilities.test.ts | 8 +++--- .../authorization/disable_ui_capabilities.ts | 9 +++---- .../feature_privilege_builder/navlink.ts | 5 +--- .../privileges/privileges.test.ts | 25 +++---------------- .../capabilities_switcher.test.ts | 2 +- .../capabilities/capabilities_switcher.ts | 3 +-- .../apis/security/privileges.ts | 2 +- .../apis/security/privileges_basic.ts | 2 +- .../infrastructure_security.ts | 24 +++++++++--------- .../feature_controls/infrastructure_spaces.ts | 22 ++++++++-------- .../infra/feature_controls/logs_security.ts | 24 +++++++++--------- .../infra/feature_controls/logs_spaces.ts | 22 ++++++++-------- .../test/ui_capabilities/common/features.ts | 2 +- .../plugins/foo_plugin/server/index.ts | 6 ++--- .../common/nav_links_builder.ts | 13 ++++++---- .../common/services/features.ts | 2 +- .../common/services/ui_capabilities.ts | 2 +- .../security_only/tests/nav_links.ts | 2 +- 21 files changed, 102 insertions(+), 107 deletions(-) diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index 0de431186b151..fdbd1ec894022 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -17,12 +17,12 @@ export const METRICS_FEATURE = { order: 700, icon: 'metricsApp', navLinkId: 'metrics', - app: ['infra', 'kibana'], + app: ['infra', 'metrics', 'kibana'], catalogue: ['infraops'], alerting: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], privileges: { all: { - app: ['infra', 'kibana'], + app: ['infra', 'metrics', 'kibana'], catalogue: ['infraops'], api: ['infra'], savedObject: { @@ -35,7 +35,7 @@ export const METRICS_FEATURE = { ui: ['show', 'configureSource', 'save', 'alerting:show'], }, read: { - app: ['infra', 'kibana'], + app: ['infra', 'metrics', 'kibana'], catalogue: ['infraops'], api: ['infra'], savedObject: { @@ -58,12 +58,12 @@ export const LOGS_FEATURE = { order: 800, icon: 'logsApp', navLinkId: 'logs', - app: ['infra', 'kibana'], + app: ['infra', 'logs', 'kibana'], catalogue: ['infralogging'], alerting: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], privileges: { all: { - app: ['infra', 'kibana'], + app: ['infra', 'logs', 'kibana'], catalogue: ['infralogging'], api: ['infra'], savedObject: { @@ -76,7 +76,7 @@ export const LOGS_FEATURE = { ui: ['show', 'configureSource', 'save'], }, read: { - app: ['infra', 'kibana'], + app: ['infra', 'logs', 'kibana'], catalogue: ['infralogging'], api: ['infra'], alerting: { diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index 504cd28b8fa14..58a2043502d27 100644 --- a/x-pack/plugins/ml/common/types/capabilities.ts +++ b/x-pack/plugins/ml/common/types/capabilities.ts @@ -7,6 +7,10 @@ import { KibanaRequest } from 'kibana/server'; import { PLUGIN_ID } from '../constants/app'; +export const apmUserMlCapabilities = { + canGetJobs: false, +}; + export const userMlCapabilities = { canAccessML: false, // Anomaly Detection @@ -68,6 +72,7 @@ export function getDefaultCapabilities(): MlCapabilities { } export function getPluginPrivileges() { + const apmUserMlCapabilitiesKeys = Object.keys(apmUserMlCapabilities); const userMlCapabilitiesKeys = Object.keys(userMlCapabilities); const adminMlCapabilitiesKeys = Object.keys(adminMlCapabilities); const allMlCapabilitiesKeys = [...adminMlCapabilitiesKeys, ...userMlCapabilitiesKeys]; @@ -101,6 +106,17 @@ export function getPluginPrivileges() { read: savedObjects, }, }, + apmUser: { + excludeFromBasePrivileges: true, + app: [], + catalogue: [], + savedObject: { + all: [], + read: [], + }, + api: apmUserMlCapabilitiesKeys.map((k) => `ml:${k}`), + ui: apmUserMlCapabilitiesKeys, + }, }; } diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 812db744d1bda..3c3824a785032 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -75,7 +75,7 @@ export class MlServerPlugin implements Plugin { new Feature({ id: 'fooFeature', name: 'Foo Feature', - app: ['fooApp'], + app: ['fooApp', 'foo'], navLinkId: 'foo', privileges: null, }), @@ -129,7 +129,7 @@ describe('usingPrivileges', () => { new Feature({ id: 'fooFeature', name: 'Foo Feature', - app: [], + app: ['foo'], navLinkId: 'foo', privileges: null, }), @@ -262,7 +262,7 @@ describe('usingPrivileges', () => { id: 'barFeature', name: 'Bar Feature', navLinkId: 'bar', - app: [], + app: ['bar'], privileges: null, }), ], @@ -412,7 +412,7 @@ describe('all', () => { new Feature({ id: 'fooFeature', name: 'Foo Feature', - app: [], + app: ['foo'], navLinkId: 'foo', privileges: null, }), diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts index a9b3fa54d3617..c126be1b07f6e 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts @@ -18,12 +18,11 @@ export function disableUICapabilitiesFactory( logger: Logger, authz: AuthorizationServiceSetup ) { - // nav links are sourced from two places: - // 1) The `navLinkId` property. This is deprecated and will be removed (https://github.com/elastic/kibana/issues/66217) - // 2) The apps property. The Kibana Platform associates nav links to the app which registers it, in a 1:1 relationship. - // This behavior is replacing the `navLinkId` property above. + // nav links are sourced from the apps property. + // The Kibana Platform associates nav links to the app which registers it, in a 1:1 relationship. + // This behavior is replacing the `navLinkId` property. const featureNavLinkIds = features - .flatMap((feature) => [feature.navLinkId, ...feature.app]) + .flatMap((feature) => feature.app) .filter((navLinkId) => navLinkId != null); const shouldDisableFeatureUICapability = ( diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/navlink.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/navlink.ts index f25632407be86..a6e5a01c7dba8 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/navlink.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/navlink.ts @@ -9,9 +9,6 @@ import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export class FeaturePrivilegeNavlinkBuilder extends BaseFeaturePrivilegeBuilder { public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] { - const appNavLinks = feature.app.map((app) => this.actions.ui.get('navLinks', app)); - return feature.navLinkId - ? [this.actions.ui.get('navLinks', feature.navLinkId), ...appNavLinks] - : appNavLinks; + return (privilegeDefinition.app ?? []).map((app) => this.actions.ui.get('navLinks', app)); } } diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts index d8ece8f68d425..89ac73c220756 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts @@ -54,20 +54,8 @@ describe('features', () => { const actual = privileges.get(); expect(actual).toHaveProperty('features.foo-feature', { - all: [ - actions.login, - actions.version, - actions.ui.get('navLinks', 'kibana:foo'), - actions.ui.get('navLinks', 'app-1'), - actions.ui.get('navLinks', 'app-2'), - ], - read: [ - actions.login, - actions.version, - actions.ui.get('navLinks', 'kibana:foo'), - actions.ui.get('navLinks', 'app-1'), - actions.ui.get('navLinks', 'app-2'), - ], + all: [actions.login, actions.version], + read: [actions.login, actions.version], }); }); @@ -275,7 +263,6 @@ describe('features', () => { actions.ui.get('catalogue', 'all-catalogue-2'), actions.ui.get('management', 'all-management', 'all-management-1'), actions.ui.get('management', 'all-management', 'all-management-2'), - actions.ui.get('navLinks', 'kibana:foo'), actions.savedObject.get('all-savedObject-all-1', 'bulk_get'), actions.savedObject.get('all-savedObject-all-1', 'get'), actions.savedObject.get('all-savedObject-all-1', 'find'), @@ -386,7 +373,6 @@ describe('features', () => { actions.ui.get('catalogue', 'read-catalogue-2'), actions.ui.get('management', 'read-management', 'read-management-1'), actions.ui.get('management', 'read-management', 'read-management-2'), - actions.ui.get('navLinks', 'kibana:foo'), actions.savedObject.get('read-savedObject-all-1', 'bulk_get'), actions.savedObject.get('read-savedObject-all-1', 'get'), actions.savedObject.get('read-savedObject-all-1', 'find'), @@ -644,12 +630,7 @@ describe('reserved', () => { const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); const actual = privileges.get(); - expect(actual).toHaveProperty('reserved.foo', [ - actions.version, - actions.ui.get('navLinks', 'kibana:foo'), - actions.ui.get('navLinks', 'app-1'), - actions.ui.get('navLinks', 'app-2'), - ]); + expect(actual).toHaveProperty('reserved.foo', [actions.version]); }); test(`actions only specified at the privilege are alright too`, () => { diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts index 797d7fd1bdcc4..c9ea1b44e723d 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts @@ -23,7 +23,7 @@ const features = ([ id: 'feature_2', name: 'Feature 2', navLinkId: 'feature2', - app: [], + app: ['feature2'], catalogue: ['feature2Entry'], management: { kibana: ['somethingElse'], diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts index 00e2419136f48..e8d964b22010c 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts @@ -83,8 +83,7 @@ function toggleDisabledFeatures( for (const feature of disabledFeatures) { // Disable associated navLink, if one exists - const featureNavLinks = feature.navLinkId ? [feature.navLinkId, ...feature.app] : feature.app; - featureNavLinks.forEach((app) => { + feature.app.forEach((app) => { if (navLinks.hasOwnProperty(app) && !enabledAppEntries.has(app)) { navLinks[app] = false; } diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 1ad25a11be879..07233f1685385 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -43,7 +43,7 @@ export default function ({ getService }: FtrProviderContext) { }, global: ['all', 'read'], space: ['all', 'read'], - reserved: ['ml_user', 'ml_admin', 'monitoring'], + reserved: ['ml_user', 'ml_admin', 'ml_apm_user', 'monitoring'], }; await supertest diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts index d5263aed26d0b..74d95fa1e4a76 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -41,7 +41,7 @@ export default function ({ getService }: FtrProviderContext) { }, global: ['all', 'read'], space: ['all', 'read'], - reserved: ['ml_user', 'ml_admin', 'monitoring'], + reserved: ['ml_user', 'ml_admin', 'ml_apm_user', 'monitoring'], }; await supertest diff --git a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts index 971826112a3e2..3c471516e9c66 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts @@ -423,19 +423,19 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(navLinks).to.not.contain(['Metrics']); }); - it(`metrics app is inaccessible and Application Not Found message is rendered`, async () => { - await PageObjects.common.navigateToApp('infraOps'); - await testSubjects.existOrFail('~appNotFoundPageContent'); - await PageObjects.common.navigateToUrlWithBrowserHistory( - 'infraOps', - '/inventory', - undefined, - { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - } + it(`metrics app is inaccessible and returns a 404`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + const messageText = await PageObjects.common.getBodyText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) ); - await testSubjects.existOrFail('~appNotFoundPageContent'); }); }); }); diff --git a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts index 211a9ce718b56..1bf8ded69016b 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts @@ -79,21 +79,19 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`metrics app is inaccessible and Application Not Found message is rendered`, async () => { - await PageObjects.common.navigateToApp('infraOps', { + await PageObjects.common.navigateToActualUrl('infraOps', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, basePath: '/s/custom_space', }); - await testSubjects.existOrFail('~appNotFoundPageContent'); - await PageObjects.common.navigateToUrlWithBrowserHistory( - 'infraOps', - '/inventory', - undefined, - { - basePath: '/s/custom_space', - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - } + const messageText = await PageObjects.common.getBodyText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) ); - await testSubjects.existOrFail('~appNotFoundPageContent'); }); }); diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts index c7d94f86ea420..64154ff6cf3f7 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts @@ -187,19 +187,19 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(navLinks).to.not.contain('Logs'); }); - it(`logs app is inaccessible and Application Not Found message is rendered`, async () => { - await PageObjects.common.navigateToApp('infraLogs'); - await testSubjects.existOrFail('~appNotFoundPageContent'); - await PageObjects.common.navigateToUrlWithBrowserHistory( - 'infraLogs', - '/stream', - undefined, - { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - } + it(`logs app is inaccessible and returns a 404`, async () => { + await PageObjects.common.navigateToActualUrl('infraLogs', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + const messageText = await PageObjects.common.getBodyText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) ); - await testSubjects.existOrFail('~appNotFoundPageContent'); }); }); }); diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts index 4d54539a4d09e..ea08307ccedd3 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts @@ -80,21 +80,19 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`logs app is inaccessible and Application Not Found message is rendered`, async () => { - await PageObjects.common.navigateToApp('infraLogs', { + await PageObjects.common.navigateToActualUrl('infraLogs', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, basePath: '/s/custom_space', }); - await testSubjects.existOrFail('~appNotFoundPageContent'); - await PageObjects.common.navigateToUrlWithBrowserHistory( - 'infraLogs', - '/stream', - undefined, - { - basePath: '/s/custom_space', - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - } + const messageText = await PageObjects.common.getBodyText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) ); - await testSubjects.existOrFail('~appNotFoundPageContent'); }); }); }); diff --git a/x-pack/test/ui_capabilities/common/features.ts b/x-pack/test/ui_capabilities/common/features.ts index 3c015bc21e937..e3febc945c299 100644 --- a/x-pack/test/ui_capabilities/common/features.ts +++ b/x-pack/test/ui_capabilities/common/features.ts @@ -5,7 +5,7 @@ */ interface Feature { - navLinkId: string; + app: string[]; } export interface Features { diff --git a/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/server/index.ts b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/server/index.ts index bff794801119a..5c80b4283a69b 100644 --- a/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/server/index.ts +++ b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/server/index.ts @@ -19,11 +19,11 @@ class FooPlugin implements Plugin { name: 'Foo', icon: 'upArrow', navLinkId: 'foo_plugin', - app: ['kibana'], + app: ['foo_plugin', 'kibana'], catalogue: ['foo'], privileges: { all: { - app: ['kibana'], + app: ['foo_plugin', 'kibana'], catalogue: ['foo'], savedObject: { all: ['foo'], @@ -32,7 +32,7 @@ class FooPlugin implements Plugin { ui: ['create', 'edit', 'delete', 'show'], }, read: { - app: ['kibana'], + app: ['foo_plugin', 'kibana'], catalogue: ['foo'], savedObject: { all: [], diff --git a/x-pack/test/ui_capabilities/common/nav_links_builder.ts b/x-pack/test/ui_capabilities/common/nav_links_builder.ts index b20a499ba7e20..04ab08e08a2ba 100644 --- a/x-pack/test/ui_capabilities/common/nav_links_builder.ts +++ b/x-pack/test/ui_capabilities/common/nav_links_builder.ts @@ -13,11 +13,14 @@ export class NavLinksBuilder { ...features, // management isn't a first-class "feature", but it makes our life easier here to pretend like it is management: { - navLinkId: 'kibana:stack_management', + app: ['kibana:stack_management'], }, // TODO: Temp until navLinkIds fix is merged in appSearch: { - navLinkId: 'appSearch', + app: ['appSearch', 'workplaceSearch'], + }, + kibana: { + app: ['kibana'], }, }; } @@ -38,9 +41,9 @@ export class NavLinksBuilder { private build(callback: buildCallback): Record { const navLinks = {} as Record; for (const [featureId, feature] of Object.entries(this.features)) { - if (feature.navLinkId) { - navLinks[feature.navLinkId] = callback(featureId); - } + feature.app.forEach((app) => { + navLinks[app] = callback(featureId); + }); } return navLinks; diff --git a/x-pack/test/ui_capabilities/common/services/features.ts b/x-pack/test/ui_capabilities/common/services/features.ts index 0f796c1d0a0cc..5f6ec0ad050c7 100644 --- a/x-pack/test/ui_capabilities/common/services/features.ts +++ b/x-pack/test/ui_capabilities/common/services/features.ts @@ -40,7 +40,7 @@ export class FeaturesService { (acc: Features, feature: any) => ({ ...acc, [feature.id]: { - navLinkId: feature.navLinkId, + app: feature.app, }, }), {} diff --git a/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts b/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts index bb1f3b6eefe4a..7f831973aea5c 100644 --- a/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts +++ b/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts @@ -52,7 +52,7 @@ export class UICapabilitiesService { }): Promise { const features = await this.featureService.get(); const applications = Object.values(features) - .map((feature) => feature.navLinkId) + .flatMap((feature) => feature.app) .filter((link) => !!link); const spaceUrlPrefix = spaceId ? `/s/${spaceId}` : ''; diff --git a/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts index 18838e536cf96..d7a0dfa1cf80a 100644 --- a/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts +++ b/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts @@ -57,7 +57,7 @@ export default function navLinksTests({ getService }: FtrProviderContext) { expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('navLinks'); expect(uiCapabilities.value!.navLinks).to.eql( - navLinksBuilder.only('management', 'foo') + navLinksBuilder.only('management', 'foo', 'kibana') ); break; case 'legacy_all': From b5a920d8c9cf94a1468d9f9cb022f716e17bdfa3 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 28 Jul 2020 13:50:02 +0200 Subject: [PATCH 78/96] [Uptime] Convert kuery bar to ts (#70310) Co-authored-by: Elastic Machine --- .../__tests__/alert_monitor_status.test.tsx | 10 - .../overview/alerts/alert_monitor_status.tsx | 3 - .../alert_monitor_status.tsx | 4 - .../overview/kuery_bar/kuery_bar.tsx | 22 +- .../kuery_bar/typeahead/click_outside.js | 40 --- .../overview/kuery_bar/typeahead/index.d.ts | 46 --- .../overview/kuery_bar/typeahead/index.js | 245 -------------- .../overview/kuery_bar/typeahead/index.ts | 7 + .../kuery_bar/typeahead/suggestion.js | 140 -------- .../kuery_bar/typeahead/suggestion.tsx | 89 +++++ .../kuery_bar/typeahead/suggestions.js | 111 ------ .../kuery_bar/typeahead/suggestions.tsx | 116 +++++++ .../overview/kuery_bar/typeahead/typehead.tsx | 318 ++++++++++++++++++ .../plugins/uptime/public/pages/overview.tsx | 8 - 14 files changed, 543 insertions(+), 616 deletions(-) delete mode 100644 x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/click_outside.js delete mode 100644 x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.d.ts delete mode 100644 x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.js create mode 100644 x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.ts delete mode 100644 x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.js create mode 100644 x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.tsx delete mode 100644 x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.js create mode 100644 x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.tsx create mode 100644 x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/typehead.tsx diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/__tests__/alert_monitor_status.test.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/__tests__/alert_monitor_status.test.tsx index f3f3d583fd938..f26da59238b20 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/__tests__/alert_monitor_status.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/__tests__/alert_monitor_status.test.tsx @@ -17,10 +17,6 @@ describe('alert monitor status component', () => { timerangeUnit: 'h', timerangeCount: 21, }, - autocomplete: { - addQuerySuggestionProvider: jest.fn(), - getQuerySuggestions: jest.fn(), - }, enabled: true, hasFilters: false, isOldAlert: true, @@ -45,12 +41,6 @@ describe('alert monitor status component', () => { /> = (p setAlertParams('search', value)} diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx index 4ac0355f5edc8..50b6fe2aa0ef1 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx @@ -7,7 +7,6 @@ import React, { useMemo, useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import { useSelector, useDispatch } from 'react-redux'; -import { DataPublicPluginSetup } from 'src/plugins/data/public'; import { isRight } from 'fp-ts/lib/Either'; import { selectMonitorStatusAlert, @@ -32,7 +31,6 @@ import { useUpdateKueryString } from '../../../../hooks'; interface Props { alertParams: { [key: string]: any }; - autocomplete: DataPublicPluginSetup['autocomplete']; enabled: boolean; numTimes: number; setAlertParams: (key: string, value: any) => void; @@ -43,7 +41,6 @@ interface Props { } export const AlertMonitorStatus: React.FC = ({ - autocomplete, enabled, numTimes, setAlertParams, @@ -122,7 +119,6 @@ export const AlertMonitorStatus: React.FC = ({ return ( ({ suggestions: [], isLoadingIndexPattern: true, @@ -80,7 +85,7 @@ export function KueryBar({ const indexPatternMissing = loading && !indexPattern; - async function onChange(inputValue: string, selectionStart: number) { + async function onChange(inputValue: string, selectionStart: number | null) { if (!indexPattern) { return; } @@ -94,7 +99,7 @@ export function KueryBar({ try { const suggestions = ( - (await autocompleteService.getQuerySuggestions({ + (await autocomplete.getQuerySuggestions({ language: 'kuery', indexPatterns: [indexPattern], query: inputValue, @@ -111,8 +116,7 @@ export function KueryBar({ }, ], })) || [] - ).filter((suggestion) => !startsWith(suggestion.text, 'span.')); - + ).filter((suggestion: QuerySuggestion) => !startsWith(suggestion.text, 'span.')); if (currentRequest !== currentRequestCheck) { return; } @@ -155,8 +159,8 @@ export function KueryBar({ return ( { - this.nodeRef = node; - }; - - onClick = (event) => { - if (this.nodeRef && !this.nodeRef.contains(event.target)) { - this.props.onClickOutside(); - } - }; - - render() { - return ( -
      - {this.props.children} -
      - ); - } -} - -ClickOutside.propTypes = { - onClickOutside: PropTypes.func.isRequired, -}; diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.d.ts b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.d.ts deleted file mode 100644 index 751170f3b1cf7..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.d.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; - -interface TypeaheadProps { - onChange: (inputValue: string, selectionStart: number) => void; - onSubmit: (inputValue: string) => void; - loadMore: () => void; - suggestions: unknown[]; - queryExample: string; - initialValue?: string; - isLoading?: boolean; - disabled?: boolean; -} - -export class Typeahead extends React.Component { - incrementIndex(currentIndex: any): void; - - decrementIndex(currentIndex: any): void; - - onKeyUp(event: any): void; - - onKeyDown(event: any): void; - - selectSuggestion(suggestion: any): void; - - onClickOutside(): void; - - onChangeInputValue(event: any): void; - - onClickInput(event: any): void; - - onClickSuggestion(suggestion: any): void; - - onMouseEnterSuggestion(index: any): void; - - onSubmit(): void; - - render(): any; - - loadMore(): void; -} diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.js b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.js deleted file mode 100644 index 17141235d8bf2..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.js +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import Suggestions from './suggestions'; -import ClickOutside from './click_outside'; -import { EuiFieldSearch, EuiProgress } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -const KEY_CODES = { - LEFT: 37, - UP: 38, - RIGHT: 39, - DOWN: 40, - ENTER: 13, - ESC: 27, - TAB: 9, -}; - -export class Typeahead extends Component { - state = { - isSuggestionsVisible: false, - index: null, - value: '', - inputIsPristine: true, - lastSubmitted: '', - selected: null, - }; - - static getDerivedStateFromProps(props, state) { - if (state.inputIsPristine && props.initialValue) { - return { - value: props.initialValue, - }; - } - - return null; - } - - incrementIndex = (currentIndex) => { - let nextIndex = currentIndex + 1; - if (currentIndex === null || nextIndex >= this.props.suggestions.length) { - nextIndex = 0; - } - this.setState({ index: nextIndex }); - }; - - decrementIndex = (currentIndex) => { - let previousIndex = currentIndex - 1; - if (previousIndex < 0) { - previousIndex = null; - } - this.setState({ index: previousIndex }); - }; - - onKeyUp = (event) => { - const { selectionStart } = event.target; - const { value } = this.state; - switch (event.keyCode) { - case KEY_CODES.LEFT: - this.setState({ isSuggestionsVisible: true }); - this.props.onChange(value, selectionStart); - break; - case KEY_CODES.RIGHT: - this.setState({ isSuggestionsVisible: true }); - this.props.onChange(value, selectionStart); - break; - } - }; - - onKeyDown = (event) => { - const { isSuggestionsVisible, index, value } = this.state; - switch (event.keyCode) { - case KEY_CODES.DOWN: - event.preventDefault(); - if (isSuggestionsVisible) { - this.incrementIndex(index); - } else { - this.setState({ isSuggestionsVisible: true, index: 0 }); - } - break; - case KEY_CODES.UP: - event.preventDefault(); - if (isSuggestionsVisible) { - this.decrementIndex(index); - } - break; - case KEY_CODES.ENTER: - event.preventDefault(); - if (isSuggestionsVisible && this.props.suggestions[index]) { - this.selectSuggestion(this.props.suggestions[index]); - } else { - this.setState({ isSuggestionsVisible: false }); - this.props.onSubmit(value); - } - break; - case KEY_CODES.ESC: - event.preventDefault(); - this.setState({ isSuggestionsVisible: false }); - break; - case KEY_CODES.TAB: - this.setState({ isSuggestionsVisible: false }); - break; - } - }; - - selectSuggestion = (suggestion) => { - const nextInputValue = - this.state.value.substr(0, suggestion.start) + - suggestion.text + - this.state.value.substr(suggestion.end); - - this.setState({ value: nextInputValue, index: null, selected: suggestion }); - this.props.onChange(nextInputValue, nextInputValue.length); - }; - - onClickOutside = () => { - if (this.state.isSuggestionsVisible) { - this.setState({ isSuggestionsVisible: false }); - this.onSubmit(); - } - }; - - onChangeInputValue = (event) => { - const { value, selectionStart } = event.target; - const hasValue = Boolean(value.trim()); - this.setState({ - value, - inputIsPristine: false, - isSuggestionsVisible: hasValue, - index: null, - }); - - if (!hasValue) { - this.props.onSubmit(value); - } - this.props.onChange(value, selectionStart); - }; - - onClickInput = (event) => { - const { selectionStart } = event.target; - this.props.onChange(this.state.value, selectionStart); - }; - - onClickSuggestion = (suggestion) => { - this.selectSuggestion(suggestion); - this.inputRef.focus(); - }; - - onMouseEnterSuggestion = (index) => { - this.setState({ index }); - }; - - onSubmit = () => { - const { value, lastSubmitted, selected } = this.state; - - if ( - lastSubmitted !== value && - selected && - (selected.type === 'value' || selected.text.trim() === ': *') - ) { - this.props.onSubmit(value); - this.setState({ lastSubmitted: value, selected: null }); - } - }; - - onFocus = () => { - this.setState({ isSuggestionsVisible: true }); - }; - - render() { - return ( - -
      - { - if (node) { - this.inputRef = node; - } - }} - disabled={this.props.disabled} - value={this.state.value} - onKeyDown={this.onKeyDown} - onKeyUp={this.onKeyUp} - onFocus={this.onFocus} - onChange={this.onChangeInputValue} - onClick={this.onClickInput} - autoComplete="off" - spellCheck={false} - /> - - {this.props.isLoading && ( - - )} -
      - - -
      - ); - } -} - -Typeahead.propTypes = { - initialValue: PropTypes.string, - isLoading: PropTypes.bool, - disabled: PropTypes.bool, - onChange: PropTypes.func.isRequired, - onSubmit: PropTypes.func.isRequired, - loadMore: PropTypes.func.isRequired, - suggestions: PropTypes.array.isRequired, - queryExample: PropTypes.string.isRequired, -}; - -Typeahead.defaultProps = { - isLoading: false, - disabled: false, - suggestions: [], -}; diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.ts b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.ts new file mode 100644 index 0000000000000..6bf1226131e29 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { Typeahead } from './typehead'; diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.js b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.js deleted file mode 100644 index 615a444d23e73..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.js +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import styled from 'styled-components'; -import { EuiIcon } from '@elastic/eui'; -import { - fontFamilyCode, - px, - units, - fontSizes, - unit, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../apm/public/style/variables'; -import { tint } from 'polished'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; - -function getIconColor(type) { - switch (type) { - case 'field': - return theme.euiColorVis7; - case 'value': - return theme.euiColorVis0; - case 'operator': - return theme.euiColorVis1; - case 'conjunction': - return theme.euiColorVis3; - case 'recentSearch': - return theme.euiColorMediumShade; - } -} - -const Description = styled.div` - color: ${theme.euiColorDarkShade}; - - p { - display: inline; - - span { - font-family: ${fontFamilyCode}; - color: ${theme.euiColorFullShade}; - padding: 0 ${px(units.quarter)}; - display: inline-block; - } - } -`; - -const ListItem = styled.button` - width: inherit; - font-size: ${fontSizes.small}; - height: ${px(units.double)}; - align-items: center; - display: flex; - background: ${(props) => (props.selected ? theme.euiColorLightestShade : 'initial')}; - cursor: pointer; - border-radius: ${px(units.quarter)}; - - ${Description} { - p span { - background: ${(props) => - props.selected ? theme.euiColorEmptyShade : theme.euiColorLightestShade}; - } - @media only screen and (max-width: ${theme.euiBreakpoints.s}) { - margin-left: auto; - text-align: end; - } - } -`; - -const Icon = styled.div` - flex: 0 0 ${px(units.double)}; - background: ${(props) => tint(0.1, getIconColor(props.type))}; - color: ${(props) => getIconColor(props.type)}; - width: 100%; - height: 100%; - text-align: center; - line-height: ${px(units.double)}; -`; - -const TextValue = styled.div` - text-align: left; - flex: 0 0 ${px(unit * 12)}; - color: ${theme.euiColorDarkestShade}; - padding: 0 ${px(units.half)}; - - @media only screen and (max-width: ${theme.euiBreakpoints.s}) { - flex: 0 0 ${px(unit * 8)}; - } - @media only screen and (min-width: 1300px) { - flex: 0 0 ${px(unit * 16)}; - } -`; - -function getEuiIconType(type) { - switch (type) { - case 'field': - return 'kqlField'; - case 'value': - return 'kqlValue'; - case 'recentSearch': - return 'search'; - case 'conjunction': - return 'kqlSelector'; - case 'operator': - return 'kqlOperand'; - default: - throw new Error('Unknown type', type); - } -} - -function Suggestion(props) { - return ( - props.onClick(props.suggestion)} - onMouseEnter={props.onMouseEnter} - > - - - - {props.suggestion.text} - {props.suggestion.description} - - ); -} - -Suggestion.propTypes = { - onClick: PropTypes.func.isRequired, - onMouseEnter: PropTypes.func.isRequired, - selected: PropTypes.bool, - suggestion: PropTypes.object.isRequired, - innerRef: PropTypes.func.isRequired, -}; - -export default Suggestion; diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.tsx new file mode 100644 index 0000000000000..1dc89d2795309 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.tsx @@ -0,0 +1,89 @@ +/* + * 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, { useRef, useEffect, RefObject } from 'react'; +import styled from 'styled-components'; +import { EuiSuggestItem } from '@elastic/eui'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; + +import { QuerySuggestion } from '../../../../../../../../src/plugins/data/public'; + +const SuggestionItem = styled.div<{ selected: boolean }>` + background: ${(props) => (props.selected ? theme.euiColorLightestShade : 'initial')}; +`; + +function getIconColor(type: string) { + switch (type) { + case 'field': + return 'tint5'; + case 'value': + return 'tint0'; + case 'operator': + return 'tint1'; + case 'conjunction': + return 'tint3'; + case 'recentSearch': + return 'tint10'; + default: + return 'tint5'; + } +} + +function getEuiIconType(type: string) { + switch (type) { + case 'field': + return 'kqlField'; + case 'value': + return 'kqlValue'; + case 'recentSearch': + return 'search'; + case 'conjunction': + return 'kqlSelector'; + case 'operator': + return 'kqlOperand'; + default: + throw new Error(`Unknown type ${type}`); + } +} + +interface SuggestionProps { + onClick: (sug: QuerySuggestion) => void; + onMouseEnter: () => void; + selected: boolean; + suggestion: QuerySuggestion; + innerRef: (node: any) => void; +} + +export const Suggestion: React.FC = ({ + innerRef, + selected, + suggestion, + onClick, + onMouseEnter, +}) => { + const childNode: RefObject = useRef(null); + + useEffect(() => { + if (childNode.current) { + innerRef(childNode.current); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [childNode]); + + return ( + + onClick(suggestion)} + onMouseEnter={onMouseEnter} + // @ts-ignore + description={suggestion.description} + /> + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.js b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.js deleted file mode 100644 index 8d614d7ea1aec..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.js +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import styled from 'styled-components'; -import { isEmpty } from 'lodash'; -import Suggestion from './suggestion'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { units, px, unit } from '../../../../../../apm/public/style/variables'; -import { tint } from 'polished'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; - -const List = styled.ul` - width: 100%; - border: 1px solid ${theme.euiColorLightShade}; - border-radius: ${px(units.quarter)}; - box-shadow: 0px ${px(units.quarter)} ${px(units.double)} ${tint(0.1, theme.euiColorFullShade)}; - position: absolute; - background: #fff; - z-index: 10; - left: 0; - max-height: ${px(unit * 20)}; - overflow: scroll; -`; - -class Suggestions extends Component { - childNodes = []; - - scrollIntoView = () => { - const parent = this.parentNode; - const child = this.childNodes[this.props.index]; - - if (this.props.index == null || !parent || !child) { - return; - } - - const scrollTop = Math.max( - Math.min(parent.scrollTop, child.offsetTop), - child.offsetTop + child.offsetHeight - parent.offsetHeight - ); - - parent.scrollTop = scrollTop; - }; - - handleScroll = () => { - const parent = this.parentNode; - - if (!this.props.loadMore || !parent) { - return; - } - - const position = parent.scrollTop + parent.offsetHeight; - const height = parent.scrollHeight; - const remaining = height - position; - const margin = 50; - - if (!height || !position) { - return; - } - if (remaining <= margin) { - this.props.loadMore(); - } - }; - - componentDidUpdate(prevProps) { - if (prevProps.index !== this.props.index) { - this.scrollIntoView(); - } - } - - render() { - if (!this.props.show || isEmpty(this.props.suggestions)) { - return null; - } - - const suggestions = this.props.suggestions.map((suggestion, index) => { - const key = suggestion + '_' + index; - return ( - (this.childNodes[index] = node)} - selected={index === this.props.index} - suggestion={suggestion} - onClick={this.props.onClick} - onMouseEnter={() => this.props.onMouseEnter(index)} - key={key} - /> - ); - }); - - return ( - (this.parentNode = node)} onScroll={this.handleScroll}> - {suggestions} - - ); - } -} - -Suggestions.propTypes = { - index: PropTypes.number, - onClick: PropTypes.func.isRequired, - onMouseEnter: PropTypes.func.isRequired, - show: PropTypes.bool, - suggestions: PropTypes.array.isRequired, - loadMore: PropTypes.func.isRequired, -}; - -export default Suggestions; diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.tsx new file mode 100644 index 0000000000000..dcd8df1ba18ef --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.tsx @@ -0,0 +1,116 @@ +/* + * 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, { useRef, useState, useEffect } from 'react'; +import styled from 'styled-components'; +import { isEmpty } from 'lodash'; +import { tint } from 'polished'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { Suggestion } from './suggestion'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { units, px, unit } from '../../../../../../apm/public/style/variables'; +import { QuerySuggestion } from '../../../../../../../../src/plugins/data/public'; + +const List = styled.ul` + width: 100%; + border: 1px solid ${theme.euiColorLightShade}; + border-radius: ${px(units.quarter)}; + box-shadow: 0px ${px(units.quarter)} ${px(units.double)} ${tint(0.1, theme.euiColorFullShade)}; + background: #fff; + z-index: 10; + max-height: ${px(unit * 20)}; + overflow: scroll; + position: absolute; +`; + +interface SuggestionsProps { + index: number; + onClick: (sug: QuerySuggestion) => void; + onMouseEnter: (index: number) => void; + show?: boolean; + suggestions: QuerySuggestion[]; + loadMore: () => void; +} + +export const Suggestions: React.FC = ({ + show, + index, + onClick, + suggestions, + onMouseEnter, + loadMore, +}) => { + const [childNodes, setChildNodes] = useState([]); + + const parentNode = useRef(null); + + useEffect(() => { + const scrollIntoView = () => { + const parent = parentNode.current; + const child = childNodes[index]; + + if (index == null || !parent || !child) { + return; + } + + const scrollTop = Math.max( + Math.min(parent.scrollTop, child.offsetTop), + child.offsetTop + child.offsetHeight - parent.offsetHeight + ); + + parent.scrollTop = scrollTop; + }; + scrollIntoView(); + }, [index, childNodes]); + + if (!show || isEmpty(suggestions)) { + return null; + } + + const handleScroll = () => { + const parent = parentNode.current; + + if (!loadMore || !parent) { + return; + } + + const position = parent.scrollTop + parent.offsetHeight; + const height = parent.scrollHeight; + const remaining = height - position; + const margin = 50; + + if (!height || !position) { + return; + } + if (remaining <= margin) { + loadMore(); + } + }; + + const suggestionsNodes = suggestions.map((suggestion, currIndex) => { + const key = suggestion + '_' + currIndex; + return ( + { + const nodes = childNodes; + nodes[currIndex] = node; + setChildNodes([...nodes]); + }} + selected={currIndex === index} + suggestion={suggestion} + onClick={onClick} + onMouseEnter={() => onMouseEnter(currIndex)} + key={key} + /> + ); + }); + + return ( + + {suggestionsNodes} + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/typehead.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/typehead.tsx new file mode 100644 index 0000000000000..5582818b6f09b --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/typehead.tsx @@ -0,0 +1,318 @@ +/* + * 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, { KeyboardEvent, ChangeEvent, MouseEvent, useState, useRef, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFieldSearch, EuiProgress, EuiOutsideClickDetector } from '@elastic/eui'; +import { Suggestions } from './suggestions'; +import { QuerySuggestion } from '../../../../../../../../src/plugins/data/public'; + +const KEY_CODES = { + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + ENTER: 13, + ESC: 27, + TAB: 9, +}; + +interface TypeaheadState { + isSuggestionsVisible: boolean; + index: number | null; + value: string; + inputIsPristine: boolean; + lastSubmitted: string; + selected: QuerySuggestion | null; +} + +interface TypeaheadProps { + onChange: (inputValue: string, selectionStart: number | null) => void; + onSubmit: (inputValue: string) => void; + suggestions: QuerySuggestion[]; + queryExample: string; + initialValue?: string; + isLoading?: boolean; + disabled?: boolean; + dataTestSubj: string; + ariaLabel: string; + loadMore: () => void; +} + +export const Typeahead: React.FC = ({ + initialValue, + suggestions, + onChange, + onSubmit, + dataTestSubj, + ariaLabel, + disabled, + isLoading, + loadMore, +}) => { + const [state, setState] = useState({ + isSuggestionsVisible: false, + index: null, + value: '', + inputIsPristine: true, + lastSubmitted: '', + selected: null, + }); + + const inputRef = useRef(); + + useEffect(() => { + if (state.inputIsPristine && initialValue) { + setState((prevState) => ({ + ...prevState, + value: initialValue, + })); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [initialValue]); + + const incrementIndex = (currentIndex: number) => { + let nextIndex = currentIndex + 1; + if (currentIndex === null || nextIndex >= suggestions.length) { + nextIndex = 0; + } + + setState((prevState) => ({ + ...prevState, + index: nextIndex, + })); + }; + + const decrementIndex = (currentIndex: number) => { + let previousIndex: number | null = currentIndex - 1; + if (previousIndex < 0) { + previousIndex = null; + } + + setState((prevState) => ({ + ...prevState, + index: previousIndex, + })); + }; + + const onKeyUp = (event: KeyboardEvent & ChangeEvent) => { + const { selectionStart } = event.target; + const { value } = state; + switch (event.keyCode) { + case KEY_CODES.LEFT: + setState((prevState) => ({ + ...prevState, + isSuggestionsVisible: true, + })); + onChange(value, selectionStart); + break; + case KEY_CODES.RIGHT: + setState((prevState) => ({ + ...prevState, + isSuggestionsVisible: true, + })); + onChange(value, selectionStart); + break; + } + }; + + const onKeyDown = (event: KeyboardEvent) => { + const { isSuggestionsVisible, index, value } = state; + switch (event.keyCode) { + case KEY_CODES.DOWN: + event.preventDefault(); + if (isSuggestionsVisible) { + incrementIndex(index!); + } else { + setState((prevState) => ({ + ...prevState, + isSuggestionsVisible: true, + index: 0, + })); + } + break; + case KEY_CODES.UP: + event.preventDefault(); + if (isSuggestionsVisible) { + decrementIndex(index!); + } + break; + case KEY_CODES.ENTER: + event.preventDefault(); + if (isSuggestionsVisible && suggestions[index!]) { + selectSuggestion(suggestions[index!]); + } else { + setState((prevState) => ({ + ...prevState, + isSuggestionsVisible: false, + })); + + onSubmit(value); + } + break; + case KEY_CODES.ESC: + event.preventDefault(); + + setState((prevState) => ({ + ...prevState, + isSuggestionsVisible: false, + })); + + break; + case KEY_CODES.TAB: + setState((prevState) => ({ + ...prevState, + isSuggestionsVisible: false, + })); + break; + } + }; + + const selectSuggestion = (suggestion: QuerySuggestion) => { + const nextInputValue = + state.value.substr(0, suggestion.start) + + suggestion.text + + state.value.substr(suggestion.end); + + setState((prevState) => ({ + ...prevState, + value: nextInputValue, + index: null, + selected: suggestion, + })); + + onChange(nextInputValue, nextInputValue.length); + }; + + const onClickOutside = () => { + if (state.isSuggestionsVisible) { + setState((prevState) => ({ + ...prevState, + isSuggestionsVisible: false, + })); + + onSuggestionSubmit(); + } + }; + + const onChangeInputValue = (event: ChangeEvent) => { + const { value, selectionStart } = event.target; + const hasValue = Boolean(value.trim()); + + setState((prevState) => ({ + ...prevState, + value, + inputIsPristine: false, + isSuggestionsVisible: hasValue, + index: null, + })); + + if (!hasValue) { + onSubmit(value); + } + onChange(value, selectionStart!); + }; + + const onClickInput = (event: MouseEvent & ChangeEvent) => { + event.stopPropagation(); + const { selectionStart } = event.target; + onChange(state.value, selectionStart!); + }; + + const onFocus = () => { + setState((prevState) => ({ + ...prevState, + isSuggestionsVisible: true, + })); + }; + + const onClickSuggestion = (suggestion: QuerySuggestion) => { + selectSuggestion(suggestion); + if (inputRef.current) inputRef.current.focus(); + }; + + const onMouseEnterSuggestion = (index: number) => { + setState({ ...state, index }); + + setState((prevState) => ({ + ...prevState, + index, + })); + }; + + const onSuggestionSubmit = () => { + const { value, lastSubmitted, selected } = state; + + if ( + lastSubmitted !== value && + selected && + (selected.type === 'value' || selected.text.trim() === ': *') + ) { + onSubmit(value); + + setState((prevState) => ({ + ...prevState, + lastSubmitted: value, + selected: null, + })); + } + }; + + return ( + + +
      + { + if (node) { + inputRef.current = node; + } + }} + disabled={disabled} + value={state.value} + onKeyDown={onKeyDown} + onKeyUp={onKeyUp} + onFocus={onFocus} + onChange={onChangeInputValue} + onClick={onClickInput} + autoComplete="off" + spellCheck={false} + /> + + {isLoading && ( + + )} +
      + + +
      +
      + ); +}; diff --git a/x-pack/plugins/uptime/public/pages/overview.tsx b/x-pack/plugins/uptime/public/pages/overview.tsx index 32c86435913f7..3b58ea1e5cf84 100644 --- a/x-pack/plugins/uptime/public/pages/overview.tsx +++ b/x-pack/plugins/uptime/public/pages/overview.tsx @@ -18,7 +18,6 @@ import { useTrackPageview } from '../../../observability/public'; import { MonitorList } from '../components/overview/monitor_list/monitor_list_container'; import { EmptyState, FilterGroup, KueryBar, ParsingErrorCallout } from '../components/overview'; import { StatusPanel } from '../components/overview/status_panel'; -import { useKibana } from '../../../../../src/plugins/kibana_react/public'; interface Props { loading: boolean; @@ -43,12 +42,6 @@ export const OverviewPageComponent = React.memo( const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = useGetUrlParams(); const { search, filters: urlFilters } = params; - const { - services: { - data: { autocomplete }, - }, - } = useKibana(); - useTrackPageview({ app: 'uptime', path: 'overview' }); useTrackPageview({ app: 'uptime', path: 'overview', delay: 15000 }); @@ -77,7 +70,6 @@ export const OverviewPageComponent = React.memo( aria-label={i18n.translate('xpack.uptime.filterBar.ariaLabel', { defaultMessage: 'Input filter criteria for the overview page', })} - autocomplete={autocomplete} data-test-subj="xpack.uptime.filterBar" />
      From 7a10077776a729a1f7dc674c04e73b757a1dd2f4 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Tue, 28 Jul 2020 12:53:36 +0100 Subject: [PATCH 79/96] [Security Solution] Template unit tests (#72399) * add unit test for failure cases * add unit tests * update wording * fix error when update template without ttid or ttversion * fix unit test * add comment * review Co-authored-by: Elastic Machine --- .../rules/pre_packaged_rules/translations.ts | 2 +- .../update_callout.test.tsx | 92 +++ .../pre_packaged_rules/update_callout.tsx | 9 +- .../rules/use_pre_packaged_rules.tsx | 4 +- .../detection_engine/rules/helpers.test.tsx | 136 +++++ ...get_prepackaged_rules_status_route.test.ts | 51 ++ .../routes/import_timelines_route.test.ts | 22 + .../routes/utils/compare_timelines_status.ts | 40 +- .../routes/utils/failure_cases.test.ts | 542 ++++++++++++++++++ .../timeline/routes/utils/failure_cases.ts | 17 +- 10 files changed, 888 insertions(+), 27 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.test.ts diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts index 37c1715c05d71..49da7dbf6d514 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts @@ -24,7 +24,7 @@ export const PRE_BUILT_MSG = i18n.translate( export const PRE_BUILT_ACTION = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.prePackagedRules.loadPreBuiltButton', { - defaultMessage: 'Load prebuilt detection rules', + defaultMessage: 'Load prebuilt detection rules and timeline templates', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.test.tsx index 5033fcd11dc7c..283bba462792c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.test.tsx @@ -9,6 +9,7 @@ import { shallow } from 'enzyme'; import { UpdatePrePackagedRulesCallOut } from './update_callout'; import { useKibana } from '../../../../common/lib/kibana'; + jest.mock('../../../../common/lib/kibana'); describe('UpdatePrePackagedRulesCallOut', () => { @@ -22,6 +23,7 @@ describe('UpdatePrePackagedRulesCallOut', () => { }, }); }); + it('renders correctly', () => { const wrapper = shallow( { expect(wrapper.find('EuiCallOut')).toHaveLength(1); }); + + it('renders callOutMessage correctly: numberOfUpdatedRules > 0 and numberOfUpdatedTimelines = 0', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="update-callout"]').find('p').text()).toEqual( + 'You can update 1 Elastic prebuilt ruleRelease notes' + ); + }); + + it('renders buttonTitle correctly: numberOfUpdatedRules > 0 and numberOfUpdatedTimelines = 0', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="update-callout-button"]').prop('children')).toEqual( + 'Update 1 Elastic prebuilt rule' + ); + }); + + it('renders callOutMessage correctly: numberOfUpdatedRules = 0 and numberOfUpdatedTimelines > 0', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="update-callout"]').find('p').text()).toEqual( + 'You can update 1 Elastic prebuilt timelineRelease notes' + ); + }); + + it('renders buttonTitle correctly: numberOfUpdatedRules = 0 and numberOfUpdatedTimelines > 0', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="update-callout-button"]').prop('children')).toEqual( + 'Update 1 Elastic prebuilt timeline' + ); + }); + + it('renders callOutMessage correctly: numberOfUpdatedRules > 0 and numberOfUpdatedTimelines > 0', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="update-callout"]').find('p').text()).toEqual( + 'You can update 1 Elastic prebuilt rule and 1 Elastic prebuilt timeline. Note that this will reload deleted Elastic prebuilt rules.Release notes' + ); + }); + + it('renders buttonTitle correctly: numberOfUpdatedRules > 0 and numberOfUpdatedTimelines > 0', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="update-callout-button"]').prop('children')).toEqual( + 'Update 1 Elastic prebuilt rule and 1 Elastic prebuilt timeline' + ); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.tsx index 4b454a9ed4d4a..30f8cfa7fb3a5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.tsx @@ -51,7 +51,7 @@ const UpdatePrePackagedRulesCallOutComponent: React.FC +

      {prepackagedRulesOrTimelines?.callOutMessage}
      @@ -62,7 +62,12 @@ const UpdatePrePackagedRulesCallOutComponent: React.FC

      - + {prepackagedRulesOrTimelines?.buttonTitle}
      diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx index 08c85695e9313..d82d97883a3d0 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx @@ -169,7 +169,9 @@ export const usePrePackagedRules = ({ if ( isSubscribed && ((prePackagedRuleStatusResponse.rules_not_installed === 0 && - prePackagedRuleStatusResponse.rules_not_updated === 0) || + prePackagedRuleStatusResponse.rules_not_updated === 0 && + prePackagedRuleStatusResponse.timelines_not_installed === 0 && + prePackagedRuleStatusResponse.timelines_not_updated === 0) || iterationTryOfFetchingPrePackagedCount > 100) ) { setLoadingCreatePrePackagedRules(false); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx index c01317e4f48c5..b40243efcfb46 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx @@ -13,6 +13,8 @@ import { getActionsStepsData, getHumanizedDuration, getModifiedAboutDetailsData, + getPrePackagedRuleStatus, + getPrePackagedTimelineStatus, determineDetailsValue, userHasNoPermissions, } from './helpers'; @@ -394,4 +396,138 @@ describe('rule helpers', () => { expect(result).toEqual(userHasNoPermissionsExpectedResult); }); }); + + describe('getPrePackagedRuleStatus', () => { + test('ruleNotInstalled', () => { + const rulesInstalled = 0; + const rulesNotInstalled = 1; + const rulesNotUpdated = 0; + const result: string = getPrePackagedRuleStatus( + rulesInstalled, + rulesNotInstalled, + rulesNotUpdated + ); + + expect(result).toEqual('ruleNotInstalled'); + }); + + test('ruleInstalled', () => { + const rulesInstalled = 1; + const rulesNotInstalled = 0; + const rulesNotUpdated = 0; + const result: string = getPrePackagedRuleStatus( + rulesInstalled, + rulesNotInstalled, + rulesNotUpdated + ); + + expect(result).toEqual('ruleInstalled'); + }); + + test('someRuleUninstall', () => { + const rulesInstalled = 1; + const rulesNotInstalled = 1; + const rulesNotUpdated = 0; + const result: string = getPrePackagedRuleStatus( + rulesInstalled, + rulesNotInstalled, + rulesNotUpdated + ); + + expect(result).toEqual('someRuleUninstall'); + }); + + test('ruleNeedUpdate', () => { + const rulesInstalled = 1; + const rulesNotInstalled = 0; + const rulesNotUpdated = 1; + const result: string = getPrePackagedRuleStatus( + rulesInstalled, + rulesNotInstalled, + rulesNotUpdated + ); + + expect(result).toEqual('ruleNeedUpdate'); + }); + + test('unknown', () => { + const rulesInstalled = null; + const rulesNotInstalled = null; + const rulesNotUpdated = null; + const result: string = getPrePackagedRuleStatus( + rulesInstalled, + rulesNotInstalled, + rulesNotUpdated + ); + + expect(result).toEqual('unknown'); + }); + }); + + describe('getPrePackagedTimelineStatus', () => { + test('timelinesNotInstalled', () => { + const timelinesInstalled = 0; + const timelinesNotInstalled = 1; + const timelinesNotUpdated = 0; + const result: string = getPrePackagedTimelineStatus( + timelinesInstalled, + timelinesNotInstalled, + timelinesNotUpdated + ); + + expect(result).toEqual('timelinesNotInstalled'); + }); + + test('timelinesInstalled', () => { + const timelinesInstalled = 1; + const timelinesNotInstalled = 0; + const timelinesNotUpdated = 0; + const result: string = getPrePackagedTimelineStatus( + timelinesInstalled, + timelinesNotInstalled, + timelinesNotUpdated + ); + + expect(result).toEqual('timelinesInstalled'); + }); + + test('someTimelineUninstall', () => { + const timelinesInstalled = 1; + const timelinesNotInstalled = 1; + const timelinesNotUpdated = 0; + const result: string = getPrePackagedTimelineStatus( + timelinesInstalled, + timelinesNotInstalled, + timelinesNotUpdated + ); + + expect(result).toEqual('someTimelineUninstall'); + }); + + test('timelineNeedUpdate', () => { + const timelinesInstalled = 1; + const timelinesNotInstalled = 0; + const timelinesNotUpdated = 1; + const result: string = getPrePackagedTimelineStatus( + timelinesInstalled, + timelinesNotInstalled, + timelinesNotUpdated + ); + + expect(result).toEqual('timelineNeedUpdate'); + }); + + test('unknown', () => { + const timelinesInstalled = null; + const timelinesNotInstalled = null; + const timelinesNotUpdated = null; + const result: string = getPrePackagedTimelineStatus( + timelinesInstalled, + timelinesNotInstalled, + timelinesNotUpdated + ); + + expect(result).toEqual('unknown'); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts index f8b6f7e3ddcba..fa2a575d3f69f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts @@ -14,6 +14,11 @@ import { } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, createMockConfig } from '../__mocks__'; import { SecurityPluginSetup } from '../../../../../../security/server'; +import { checkTimelinesStatus } from '../../../timeline/routes/utils/check_timelines_status'; +import { + mockCheckTimelinesStatusBeforeInstallResult, + mockCheckTimelinesStatusAfterInstallResult, +} from '../../../timeline/routes/__mocks__/import_timelines'; jest.mock('../../rules/get_prepackaged_rules', () => { return { @@ -38,6 +43,12 @@ jest.mock('../../rules/get_prepackaged_rules', () => { }; }); +jest.mock('../../../timeline/routes/utils/check_timelines_status', () => { + return { + checkTimelinesStatus: jest.fn(), + }; +}); + describe('get_prepackaged_rule_status_route', () => { const mockGetCurrentUser = { user: { @@ -126,5 +137,45 @@ describe('get_prepackaged_rule_status_route', () => { timelines_not_updated: 0, }); }); + + test('0 timelines installed, 3 timelines not installed, 0 timelines not updated', async () => { + clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); + (checkTimelinesStatus as jest.Mock).mockResolvedValue( + mockCheckTimelinesStatusBeforeInstallResult + ); + const request = getPrepackagedRulesStatusRequest(); + const response = await server.inject(request, context); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + rules_custom_installed: 0, + rules_installed: 0, + rules_not_installed: 1, + rules_not_updated: 0, + timelines_installed: 0, + timelines_not_installed: 3, + timelines_not_updated: 0, + }); + }); + + test('3 timelines installed, 0 timelines not installed, 0 timelines not updated', async () => { + clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); + (checkTimelinesStatus as jest.Mock).mockResolvedValue( + mockCheckTimelinesStatusAfterInstallResult + ); + const request = getPrepackagedRulesStatusRequest(); + const response = await server.inject(request, context); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + rules_custom_installed: 0, + rules_installed: 0, + rules_not_installed: 1, + rules_not_updated: 0, + timelines_installed: 3, + timelines_not_installed: 0, + timelines_not_updated: 0, + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts index fe5993cb0161d..b817896e901c1 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts @@ -598,6 +598,28 @@ describe('import timeline templates', () => { mockNewTemplateTimelineId ); }); + + test('should return 200 if create via import without a templateTimelineId or templateTimelineVersion', async () => { + mockGetTupleDuplicateErrorsAndUniqueTimeline.mockReturnValue([ + mockDuplicateIdErrors, + [ + { + ...mockUniqueParsedTemplateTimelineObjects[0], + templateTimelineId: null, + templateTimelineVersion: null, + }, + ], + ]); + const mockRequest = getImportTimelinesRequest(); + const result = await server.inject(mockRequest, context); + expect(result.body).toEqual({ + errors: [], + success: true, + success_count: 1, + timelines_installed: 1, + timelines_updated: 0, + }); + }); }); describe('Import a timeline template already exist', () => { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.ts index d61d217a4cf49..f9515741d1250 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { isEmpty } from 'lodash/fp'; +import { isEmpty, isInteger } from 'lodash/fp'; import { TimelineTypeLiteralWithNull, TimelineType, @@ -71,13 +71,28 @@ export class CompareTimelinesStatus { } public get isCreatable() { + const noExistingTimeline = this.timelineObject.isCreatable && !this.isHandlingTemplateTimeline; + + const templateCreatable = + this.isHandlingTemplateTimeline && this.templateTimelineObject.isCreatable; + + const noExistingTimelineOrTemplate = templateCreatable && this.timelineObject.isCreatable; + + // From Line 87-91 is the condition for creating a template via import without given a templateTimelineId or templateTimelineVersion, + // but keep the existing savedObjectId and version there. + // Therefore even the timeline exists, we still allow it to create a new timeline template by assigning a templateTimelineId and templateTimelineVersion. + // https://github.com/elastic/kibana/pull/67496#discussion_r454337222 + // Line 90-91 means that we want to make sure the existing timeline retrieved by savedObjectId is atemplate. + // If it is not a template, we show an error this timeline is already exist instead. + const retriveTemplateViaSavedObjectId = + templateCreatable && + !this.timelineObject.isCreatable && + this.timelineObject.getData?.timelineType === this.timelineType; + return ( this.isTitleValid && !this.isSavedObjectVersionConflict && - ((this.timelineObject.isCreatable && !this.isHandlingTemplateTimeline) || - (this.templateTimelineObject.isCreatable && - this.timelineObject.isCreatable && - this.isHandlingTemplateTimeline)) + (noExistingTimeline || noExistingTimelineOrTemplate || retriveTemplateViaSavedObjectId) ); } @@ -195,24 +210,27 @@ export class CompareTimelinesStatus { } private get isTemplateVersionConflict() { - const version = this.templateTimelineObject?.getVersion; + const templateTimelineVersion = this.templateTimelineObject?.getVersion; const existingTemplateTimelineVersion = this.templateTimelineObject?.data ?.templateTimelineVersion; if ( - version != null && + templateTimelineVersion != null && this.templateTimelineObject.isExists && existingTemplateTimelineVersion != null ) { - return version <= existingTemplateTimelineVersion; - } else if (this.templateTimelineObject.isExists && version == null) { + return templateTimelineVersion <= existingTemplateTimelineVersion; + } else if (this.templateTimelineObject.isExists && templateTimelineVersion == null) { return true; } return false; } private get isTemplateVersionValid() { - const version = this.templateTimelineObject?.getVersion; - return typeof version === 'number' && !this.isTemplateVersionConflict; + const templateTimelineVersion = this.templateTimelineObject?.getVersion; + return ( + templateTimelineVersion == null || + (isInteger(templateTimelineVersion) && !this.isTemplateVersionConflict) + ); } private get isUpdatedTimelineStatusValid() { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.test.ts new file mode 100644 index 0000000000000..3c3ad1cf2d7f8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.test.ts @@ -0,0 +1,542 @@ +/* + * 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 { + commonFailureChecker, + checkIsCreateFailureCases, + checkIsUpdateFailureCases, + checkIsCreateViaImportFailureCases, + EMPTY_TITLE_ERROR_MESSAGE, + UPDATE_STATUS_ERROR_MESSAGE, + CREATE_TIMELINE_ERROR_MESSAGE, + CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, + CREATE_TEMPLATE_TIMELINE_WITHOUT_VERSION_ERROR_MESSAGE, + NO_MATCH_ID_ERROR_MESSAGE, + NO_MATCH_VERSION_ERROR_MESSAGE, + NOT_ALLOW_UPDATE_TIMELINE_TYPE_ERROR_MESSAGE, + UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, + CREATE_WITH_INVALID_STATUS_ERROR_MESSAGE, + getImportExistingTimelineError, + checkIsUpdateViaImportFailureCases, + NOT_ALLOW_UPDATE_STATUS_ERROR_MESSAGE, + TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE, +} from './failure_cases'; +import { + TimelineStatus, + TimelineType, + TimelineSavedObject, +} from '../../../../../common/types/timeline'; +import { mockGetTimelineValue, mockGetTemplateTimelineValue } from '../__mocks__/import_timelines'; + +describe('failure cases', () => { + describe('commonFailureChecker', () => { + test('If timeline type is draft, it should not return error if title is not given', () => { + const result = commonFailureChecker(TimelineStatus.draft, null); + + expect(result).toBeNull(); + }); + + test('If timeline type is active, it should return error if title is not given', () => { + const result = commonFailureChecker(TimelineStatus.active, null); + + expect(result).toEqual({ + body: EMPTY_TITLE_ERROR_MESSAGE, + statusCode: 405, + }); + }); + + test('If timeline type is immutable, it should return error if title is not given', () => { + const result = commonFailureChecker(TimelineStatus.immutable, null); + + expect(result).toEqual({ + body: EMPTY_TITLE_ERROR_MESSAGE, + statusCode: 405, + }); + }); + + test('If timeline type is not a draft, it should return no error if title is given', () => { + const result = commonFailureChecker(TimelineStatus.active, 'title'); + + expect(result).toBeNull(); + }); + }); + + describe('checkIsCreateFailureCases', () => { + test('Should return error if trying to create a timeline that is already exist', () => { + const isHandlingTemplateTimeline = false; + const version = null; + const templateTimelineVersion = null; + const templateTimelineId = null; + const existTimeline = mockGetTimelineValue as TimelineSavedObject; + const existTemplateTimeline = null; + const result = checkIsCreateFailureCases( + isHandlingTemplateTimeline, + TimelineStatus.active, + TimelineType.default, + version, + templateTimelineVersion, + templateTimelineId, + existTimeline, + existTemplateTimeline + ); + + expect(result).toEqual({ + body: CREATE_TIMELINE_ERROR_MESSAGE, + statusCode: 405, + }); + }); + + test('Should return error if trying to create a timeline template that is already exist', () => { + const isHandlingTemplateTimeline = true; + const version = null; + const templateTimelineVersion = 1; + const templateTimelineId = 'template-timeline-id-one'; + const existTimeline = null; + const existTemplateTimeline = mockGetTemplateTimelineValue as TimelineSavedObject; + const result = checkIsCreateFailureCases( + isHandlingTemplateTimeline, + TimelineStatus.active, + TimelineType.template, + version, + templateTimelineVersion, + templateTimelineId, + existTimeline, + existTemplateTimeline + ); + + expect(result).toEqual({ + body: CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, + statusCode: 405, + }); + }); + + test('Should return error if trying to create a timeline template without providing templateTimelineVersion', () => { + const isHandlingTemplateTimeline = true; + const version = null; + const templateTimelineVersion = null; + const templateTimelineId = 'template-timeline-id-one'; + const existTimeline = null; + const existTemplateTimeline = null; + const result = checkIsCreateFailureCases( + isHandlingTemplateTimeline, + TimelineStatus.active, + TimelineType.template, + version, + templateTimelineVersion, + templateTimelineId, + existTimeline, + existTemplateTimeline + ); + + expect(result).toEqual({ + body: CREATE_TEMPLATE_TIMELINE_WITHOUT_VERSION_ERROR_MESSAGE, + statusCode: 403, + }); + }); + }); + + describe('checkIsUpdateFailureCases', () => { + test('Should return error if trying to update status field of an existing immutable timeline', () => { + const isHandlingTemplateTimeline = false; + const version = mockGetTimelineValue.version; + const templateTimelineVersion = null; + const templateTimelineId = null; + const existTimeline = { + ...(mockGetTimelineValue as TimelineSavedObject), + status: TimelineStatus.immutable, + }; + const existTemplateTimeline = null; + const result = checkIsUpdateFailureCases( + isHandlingTemplateTimeline, + TimelineStatus.active, + TimelineType.default, + version, + templateTimelineVersion, + templateTimelineId, + existTimeline, + existTemplateTimeline + ); + + expect(result).toEqual({ + body: UPDATE_STATUS_ERROR_MESSAGE, + statusCode: 403, + }); + }); + + test('Should return error if trying to update status field of an existing immutable timeline template', () => { + const isHandlingTemplateTimeline = true; + const version = mockGetTemplateTimelineValue.version; + const templateTimelineVersion = mockGetTemplateTimelineValue.templateTimelineVersion; + const templateTimelineId = mockGetTemplateTimelineValue.templateTimelineId; + const existTimeline = null; + const existTemplateTimeline = { + ...(mockGetTemplateTimelineValue as TimelineSavedObject), + status: TimelineStatus.immutable, + }; + const result = checkIsUpdateFailureCases( + isHandlingTemplateTimeline, + TimelineStatus.active, + TimelineType.template, + version, + templateTimelineVersion, + templateTimelineId, + existTimeline, + existTemplateTimeline + ); + + expect(result).toEqual({ + body: UPDATE_STATUS_ERROR_MESSAGE, + statusCode: 403, + }); + }); + + test('should return error if trying to update timelineType field of an existing timeline template', () => { + const isHandlingTemplateTimeline = true; + const version = mockGetTemplateTimelineValue.version; + const templateTimelineVersion = mockGetTemplateTimelineValue.templateTimelineVersion; + const templateTimelineId = mockGetTemplateTimelineValue.templateTimelineId; + const existTimeline = null; + const existTemplateTimeline = mockGetTemplateTimelineValue as TimelineSavedObject; + const result = checkIsUpdateFailureCases( + isHandlingTemplateTimeline, + TimelineStatus.active, + TimelineType.default, + version, + templateTimelineVersion, + templateTimelineId, + existTimeline, + existTemplateTimeline + ); + + expect(result).toEqual({ + body: NOT_ALLOW_UPDATE_TIMELINE_TYPE_ERROR_MESSAGE, + statusCode: 403, + }); + }); + + test('should return error if trying to update a timeline template that does not exist', () => { + const isHandlingTemplateTimeline = true; + const version = mockGetTemplateTimelineValue.version; + const templateTimelineVersion = mockGetTemplateTimelineValue.templateTimelineVersion; + const templateTimelineId = mockGetTemplateTimelineValue.templateTimelineId; + const existTimeline = null; + const existTemplateTimeline = null; + const result = checkIsUpdateFailureCases( + isHandlingTemplateTimeline, + TimelineStatus.active, + TimelineType.default, + version, + templateTimelineVersion, + templateTimelineId, + existTimeline, + existTemplateTimeline + ); + + expect(result).toEqual({ + body: UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, + statusCode: 405, + }); + }); + + test('should return error if there is no matched timeline found by given templateTimelineId', () => { + const isHandlingTemplateTimeline = true; + const version = mockGetTemplateTimelineValue.version; + const templateTimelineVersion = mockGetTemplateTimelineValue.templateTimelineVersion; + const templateTimelineId = mockGetTemplateTimelineValue.templateTimelineId; + const existTimeline = { + ...(mockGetTemplateTimelineValue as TimelineSavedObject), + savedObjectId: 'someOtherId', + }; + const existTemplateTimeline = mockGetTemplateTimelineValue as TimelineSavedObject; + const result = checkIsUpdateFailureCases( + isHandlingTemplateTimeline, + TimelineStatus.active, + TimelineType.template, + version, + templateTimelineVersion, + templateTimelineId, + existTimeline, + existTemplateTimeline + ); + + expect(result).toEqual({ + body: NO_MATCH_ID_ERROR_MESSAGE, + statusCode: 409, + }); + }); + + test('should return error if given version field is defferent from existing version of timelin template', () => { + const isHandlingTemplateTimeline = true; + const version = 'xxx'; + const templateTimelineVersion = mockGetTemplateTimelineValue.templateTimelineVersion; + const templateTimelineId = mockGetTemplateTimelineValue.templateTimelineId; + const existTimeline = null; + const existTemplateTimeline = mockGetTemplateTimelineValue as TimelineSavedObject; + const result = checkIsUpdateFailureCases( + isHandlingTemplateTimeline, + TimelineStatus.active, + TimelineType.template, + version, + templateTimelineVersion, + templateTimelineId, + existTimeline, + existTemplateTimeline + ); + + expect(result).toEqual({ + body: NO_MATCH_VERSION_ERROR_MESSAGE, + statusCode: 409, + }); + }); + }); + + describe('checkIsCreateViaImportFailureCases', () => { + test('should return error if trying to create a draft timeline', () => { + const isHandlingTemplateTimeline = true; + const version = mockGetTemplateTimelineValue.version; + const templateTimelineVersion = mockGetTemplateTimelineValue.templateTimelineVersion; + const templateTimelineId = mockGetTemplateTimelineValue.templateTimelineId; + const existTimeline = null; + const existTemplateTimeline = mockGetTemplateTimelineValue as TimelineSavedObject; + const result = checkIsCreateViaImportFailureCases( + isHandlingTemplateTimeline, + TimelineStatus.draft, + TimelineType.template, + version, + templateTimelineVersion, + templateTimelineId, + existTimeline, + existTemplateTimeline + ); + + expect(result).toEqual({ + body: CREATE_WITH_INVALID_STATUS_ERROR_MESSAGE, + statusCode: 405, + }); + }); + + test('should return error if trying to create a timeline template which is already exist', () => { + const isHandlingTemplateTimeline = true; + const version = mockGetTemplateTimelineValue.version; + const templateTimelineVersion = mockGetTemplateTimelineValue.templateTimelineVersion; + const templateTimelineId = mockGetTemplateTimelineValue.templateTimelineId; + const existTimeline = null; + const existTemplateTimeline = mockGetTemplateTimelineValue as TimelineSavedObject; + const result = checkIsCreateViaImportFailureCases( + isHandlingTemplateTimeline, + TimelineStatus.active, + TimelineType.template, + version, + templateTimelineVersion, + templateTimelineId, + existTimeline, + existTemplateTimeline + ); + + expect(result).toEqual({ + body: getImportExistingTimelineError(mockGetTimelineValue.savedObjectId), + statusCode: 405, + }); + }); + + test('should return error if importe a timeline which is already exists', () => { + const isHandlingTemplateTimeline = false; + const version = mockGetTimelineValue.version; + const templateTimelineVersion = null; + const templateTimelineId = null; + const existTimeline = mockGetTimelineValue as TimelineSavedObject; + const existTemplateTimeline = null; + const result = checkIsCreateViaImportFailureCases( + isHandlingTemplateTimeline, + TimelineStatus.active, + TimelineType.default, + version, + templateTimelineVersion, + templateTimelineId, + existTimeline, + existTemplateTimeline + ); + + expect(result).toEqual({ + body: getImportExistingTimelineError(mockGetTimelineValue.savedObjectId), + statusCode: 405, + }); + }); + }); + + describe('checkIsUpdateViaImportFailureCases', () => { + test('should return error if trying to update a timeline which does not exist', () => { + const isHandlingTemplateTimeline = false; + const version = mockGetTimelineValue.version; + const templateTimelineVersion = null; + const templateTimelineId = null; + const existTimeline = mockGetTimelineValue as TimelineSavedObject; + const existTemplateTimeline = null; + const result = checkIsUpdateViaImportFailureCases( + isHandlingTemplateTimeline, + TimelineStatus.active, + TimelineType.default, + version, + templateTimelineVersion, + templateTimelineId, + existTimeline, + existTemplateTimeline + ); + + expect(result).toEqual({ + body: getImportExistingTimelineError(mockGetTimelineValue.savedObjectId), + statusCode: 405, + }); + }); + + test('should return error if trying to update timelineType field of an existing timeline template', () => { + const isHandlingTemplateTimeline = true; + const version = mockGetTemplateTimelineValue.version; + const templateTimelineVersion = mockGetTemplateTimelineValue.templateTimelineVersion; + const templateTimelineId = mockGetTemplateTimelineValue.templateTimelineId; + const existTimeline = null; + const existTemplateTimeline = mockGetTemplateTimelineValue as TimelineSavedObject; + const result = checkIsUpdateViaImportFailureCases( + isHandlingTemplateTimeline, + TimelineStatus.active, + TimelineType.default, + version, + templateTimelineVersion, + templateTimelineId, + existTimeline, + existTemplateTimeline + ); + + expect(result).toEqual({ + body: NOT_ALLOW_UPDATE_TIMELINE_TYPE_ERROR_MESSAGE, + statusCode: 403, + }); + }); + + test('should return error if trying to update status field of an existing timeline template', () => { + const isHandlingTemplateTimeline = true; + const version = mockGetTemplateTimelineValue.version; + const templateTimelineVersion = mockGetTemplateTimelineValue.templateTimelineVersion; + const templateTimelineId = mockGetTemplateTimelineValue.templateTimelineId; + const existTimeline = null; + const existTemplateTimeline = mockGetTemplateTimelineValue as TimelineSavedObject; + const result = checkIsUpdateViaImportFailureCases( + isHandlingTemplateTimeline, + TimelineStatus.immutable, + TimelineType.template, + version, + templateTimelineVersion, + templateTimelineId, + existTimeline, + existTemplateTimeline + ); + + expect(result).toEqual({ + body: NOT_ALLOW_UPDATE_STATUS_ERROR_MESSAGE, + statusCode: 405, + }); + }); + + test('should return error if trying to update a timeline template that does not exist', () => { + const isHandlingTemplateTimeline = true; + const version = mockGetTemplateTimelineValue.version; + const templateTimelineVersion = mockGetTemplateTimelineValue.templateTimelineVersion; + const templateTimelineId = mockGetTemplateTimelineValue.templateTimelineId; + const existTimeline = null; + const existTemplateTimeline = null; + const result = checkIsUpdateViaImportFailureCases( + isHandlingTemplateTimeline, + TimelineStatus.active, + TimelineType.default, + version, + templateTimelineVersion, + templateTimelineId, + existTimeline, + existTemplateTimeline + ); + + expect(result).toEqual({ + body: UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, + statusCode: 405, + }); + }); + + test('should return error if there is no matched timeline found by given templateTimelineId', () => { + const isHandlingTemplateTimeline = true; + const version = mockGetTemplateTimelineValue.version; + const templateTimelineVersion = mockGetTemplateTimelineValue.templateTimelineVersion; + const templateTimelineId = mockGetTemplateTimelineValue.templateTimelineId; + const existTimeline = { + ...(mockGetTemplateTimelineValue as TimelineSavedObject), + savedObjectId: 'someOtherId', + }; + const existTemplateTimeline = mockGetTemplateTimelineValue as TimelineSavedObject; + const result = checkIsUpdateViaImportFailureCases( + isHandlingTemplateTimeline, + TimelineStatus.active, + TimelineType.template, + version, + templateTimelineVersion, + templateTimelineId, + existTimeline, + existTemplateTimeline + ); + + expect(result).toEqual({ + body: NO_MATCH_ID_ERROR_MESSAGE, + statusCode: 409, + }); + }); + + test('should return error if given version field is defferent from existing version of timelin template', () => { + const isHandlingTemplateTimeline = true; + const version = 'xxx'; + const templateTimelineVersion = mockGetTemplateTimelineValue.templateTimelineVersion; + const templateTimelineId = mockGetTemplateTimelineValue.templateTimelineId; + const existTimeline = null; + const existTemplateTimeline = mockGetTemplateTimelineValue as TimelineSavedObject; + const result = checkIsUpdateViaImportFailureCases( + isHandlingTemplateTimeline, + TimelineStatus.active, + TimelineType.template, + version, + templateTimelineVersion, + templateTimelineId, + existTimeline, + existTemplateTimeline + ); + + expect(result).toEqual({ + body: NO_MATCH_VERSION_ERROR_MESSAGE, + statusCode: 409, + }); + }); + + test('should return error if given templateTimelineVersion field is less or equal to existing templateTimelineVersion of timelin template', () => { + const isHandlingTemplateTimeline = true; + const version = mockGetTemplateTimelineValue.version; + const templateTimelineVersion = mockGetTemplateTimelineValue.templateTimelineVersion; + const templateTimelineId = mockGetTemplateTimelineValue.templateTimelineId; + const existTimeline = null; + const existTemplateTimeline = mockGetTemplateTimelineValue as TimelineSavedObject; + const result = checkIsUpdateViaImportFailureCases( + isHandlingTemplateTimeline, + TimelineStatus.active, + TimelineType.template, + version, + templateTimelineVersion, + templateTimelineId, + existTimeline, + existTemplateTimeline + ); + + expect(result).toEqual({ + body: TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE, + statusCode: 409, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts index d41e8fc190983..b926819d66c92 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts @@ -78,7 +78,10 @@ const commonUpdateTemplateTimelineCheck = ( existTemplateTimeline: TimelineSavedObject | null ) => { if (isHandlingTemplateTimeline) { - if (existTimeline != null && timelineType !== existTimeline.timelineType) { + if ( + (existTimeline != null && timelineType !== existTimeline.timelineType) || + (existTemplateTimeline != null && timelineType !== existTemplateTimeline.timelineType) + ) { return { body: NOT_ALLOW_UPDATE_TIMELINE_TYPE_ERROR_MESSAGE, statusCode: 403, @@ -106,11 +109,7 @@ const commonUpdateTemplateTimelineCheck = ( }; } - if ( - existTemplateTimeline != null && - existTemplateTimeline.templateTimelineVersion == null && - existTemplateTimeline.version !== version - ) { + if (existTemplateTimeline != null && existTemplateTimeline.version !== version) { // throw error 409 conflict timeline return { body: NO_MATCH_VERSION_ERROR_MESSAGE, @@ -231,12 +230,6 @@ export const checkIsUpdateViaImportFailureCases = ( }; } } else { - if (existTemplateTimeline != null && timelineType !== existTemplateTimeline?.timelineType) { - return { - body: NOT_ALLOW_UPDATE_TIMELINE_TYPE_ERROR_MESSAGE, - statusCode: 403, - }; - } const isStatusValid = ((existTemplateTimeline?.status == null || existTemplateTimeline?.status === TimelineStatus.active) && From 8c710aae3a7702ecd16e7dab997ed331103ff165 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 28 Jul 2020 14:21:24 +0200 Subject: [PATCH 80/96] [ Functional test ] Increase the waiting time for the filter bar request (#73424) --- .../apps/visualize/input_control_vis/chained_controls.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/functional/apps/visualize/input_control_vis/chained_controls.js b/test/functional/apps/visualize/input_control_vis/chained_controls.js index 179ffa5125a9a..89cca7dc7827e 100644 --- a/test/functional/apps/visualize/input_control_vis/chained_controls.js +++ b/test/functional/apps/visualize/input_control_vis/chained_controls.js @@ -34,6 +34,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualize.loadSavedVisualization('chained input control', { navigateToVisualize: false, }); + await testSubjects.waitForEnabled('addFilter', 10000); }); it('should disable child control when parent control is not set', async () => { From 49846834ebae9d2e1d0ac67353649f0c13ed9dd8 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 28 Jul 2020 15:23:05 +0200 Subject: [PATCH 81/96] [SIEM] Unskips and fixes Cypress tests (#73322) * removes not needed configuration * fixes events columnts tests * unksips persisten timeline test * fixes failing test * skips events test since need more time for investigation Co-authored-by: Elastic Machine --- .../cypress/integration/timeline_local_storage.spec.ts | 3 +-- x-pack/plugins/security_solution/cypress/tasks/common.ts | 8 ++++---- .../security_solution/cypress/tasks/hosts/events.ts | 2 +- x-pack/test/security_solution_cypress/config.ts | 2 -- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts index 7c047459c56cc..383ebe2220585 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts @@ -13,8 +13,7 @@ import { TABLE_COLUMN_EVENTS_MESSAGE } from '../screens/hosts/external_events'; import { waitsForEventsToBeLoaded, openEventsViewerFieldsBrowser } from '../tasks/hosts/events'; import { removeColumn, resetFields } from '../tasks/timeline'; -// Failing: See https://github.com/elastic/kibana/issues/72339 -describe.skip('persistent timeline', () => { +describe('persistent timeline', () => { before(() => { loginAndWaitForPage(HOSTS_URL); openEvents(); diff --git a/x-pack/plugins/security_solution/cypress/tasks/common.ts b/x-pack/plugins/security_solution/cypress/tasks/common.ts index a385ad78f63b7..e16db54599981 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/common.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/common.ts @@ -23,14 +23,14 @@ export const drag = (subject: JQuery) => { clientY: subjectLocation.top, force: true, }) - .wait(1000) + .wait(3000) .trigger('mousemove', { button: primaryButton, clientX: subjectLocation.left + dndSloppyClickDetectionThreshold, clientY: subjectLocation.top, force: true, }) - .wait(1000); + .wait(3000); }; /** Drags the subject being dragged on the specified drop target, but does not drop it */ @@ -44,9 +44,9 @@ export const dragWithoutDrop = (dropTarget: JQuery) => { export const drop = (dropTarget: JQuery) => { cy.wrap(dropTarget) .trigger('mousemove', { button: primaryButton, force: true }) - .wait(1000) + .wait(3000) .trigger('mouseup', { force: true }) - .wait(1000); + .wait(3000); }; export const reload = (afterReload: () => void) => { diff --git a/x-pack/plugins/security_solution/cypress/tasks/hosts/events.ts b/x-pack/plugins/security_solution/cypress/tasks/hosts/events.ts index 57c819d967883..1d2c4aa8d0834 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/hosts/events.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/hosts/events.ts @@ -68,7 +68,7 @@ export const dragAndDropColumn = ({ .eq(column) .then((header) => drag(header)); - cy.wait(3000); // wait for DOM updates before moving + cy.wait(5000); // wait for DOM updates before moving cy.get(DRAGGABLE_HEADER) .eq(newPosition) diff --git a/x-pack/test/security_solution_cypress/config.ts b/x-pack/test/security_solution_cypress/config.ts index 1ad3a36cc57ae..83290a60a17a6 100644 --- a/x-pack/test/security_solution_cypress/config.ts +++ b/x-pack/test/security_solution_cypress/config.ts @@ -46,8 +46,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { '--csp.strict=false', // define custom kibana server args here `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, - '--xpack.ingestManager.enabled=true', - '--xpack.ingestManager.fleet.enabled=true', ], }, }; From 19532fc43911d887bebc3ecabae57706509e25ff Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 28 Jul 2020 15:53:23 +0200 Subject: [PATCH 82/96] [APM] Optimize traces overview (#70200) Co-authored-by: Elastic Machine --- .../app/TraceOverview/TraceList.tsx | 12 +- .../app/TransactionOverview/List/index.tsx | 12 +- .../apm/public/hooks/useTransactionList.ts | 40 +- .../aggregate-latency-metrics/index.ts | 6 +- .../apm/scripts/shared/read-kibana-config.ts | 4 +- ...egister_transaction_duration_alert_type.ts | 2 +- .../metrics/fetch_and_transform_metrics.ts | 37 +- .../lib/metrics/transform_metrics_chart.ts | 37 +- .../get_services/get_services_items_stats.ts | 3 +- .../__snapshots__/fetcher.test.ts.snap | 228 - .../__snapshots__/queries.test.ts.snap | 557 ++- .../__snapshots__/transform.test.ts.snap | 2822 ------------ .../lib/transaction_groups/fetcher.test.ts | 64 - .../server/lib/transaction_groups/fetcher.ts | 185 +- .../get_transaction_group_stats.ts | 144 + .../server/lib/transaction_groups/index.ts | 10 +- .../lib/transaction_groups/queries.test.ts | 8 +- .../lib/transaction_groups/transform.test.ts | 135 - .../lib/transaction_groups/transform.ts | 89 - .../get_local_filter_query.ts | 2 +- .../lib/ui_filters/local_ui_filters/index.ts | 1 + .../apm/typings/elasticsearch/aggregations.ts | 20 +- .../expectation/top_traces.expectation.json | 3970 ++++++++++++----- .../basic/tests/traces/top_traces.ts | 25 +- .../expectation/top_transaction_groups.json | 2639 ++++++++--- .../top_transaction_groups.ts | 25 +- 26 files changed, 5606 insertions(+), 5471 deletions(-) delete mode 100644 x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap delete mode 100644 x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/transform.test.ts.snap delete mode 100644 x-pack/plugins/apm/server/lib/transaction_groups/fetcher.test.ts create mode 100644 x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts delete mode 100644 x-pack/plugins/apm/server/lib/transaction_groups/transform.test.ts delete mode 100644 x-pack/plugins/apm/server/lib/transaction_groups/transform.ts diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx index 898e32f5c2c09..f54255ec0cd18 100644 --- a/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import styled from 'styled-components'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ITransactionGroup } from '../../../../server/lib/transaction_groups/transform'; +import { TransactionGroup } from '../../../../server/lib/transaction_groups/fetcher'; import { fontSizes, truncate } from '../../../style/variables'; import { asMillisecondDuration } from '../../../utils/formatters'; import { EmptyMessage } from '../../shared/EmptyMessage'; @@ -24,11 +24,11 @@ const StyledTransactionLink = styled(TransactionDetailLink)` `; interface Props { - items: ITransactionGroup[]; + items: TransactionGroup[]; isLoading: boolean; } -const traceListColumns: Array> = [ +const traceListColumns: Array> = [ { field: 'name', name: i18n.translate('xpack.apm.tracesTable.nameColumnLabel', { @@ -36,8 +36,8 @@ const traceListColumns: Array> = [ }), width: '40%', sortable: true, - render: (name: string, { sample }: ITransactionGroup) => ( - + render: (_: string, { sample }: TransactionGroup) => ( + > = [ transactionName={sample.transaction.name} transactionType={sample.transaction.type} > - {name} + {sample.transaction.name} ), diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.tsx index ae1b07bde0c87..2b1c1b8e8c11c 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.tsx @@ -10,7 +10,7 @@ import React, { useMemo } from 'react'; import styled from 'styled-components'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ITransactionGroup } from '../../../../../server/lib/transaction_groups/transform'; +import { TransactionGroup } from '../../../../../server/lib/transaction_groups/fetcher'; import { fontFamilyCode, truncate } from '../../../../style/variables'; import { asDecimal, asMillisecondDuration } from '../../../../utils/formatters'; import { ImpactBar } from '../../../shared/ImpactBar'; @@ -25,12 +25,12 @@ const TransactionNameLink = styled(TransactionDetailLink)` `; interface Props { - items: ITransactionGroup[]; + items: TransactionGroup[]; isLoading: boolean; } export function TransactionList({ items, isLoading }: Props) { - const columns: Array> = useMemo( + const columns: Array> = useMemo( () => [ { field: 'name', @@ -39,11 +39,11 @@ export function TransactionList({ items, isLoading }: Props) { }), width: '50%', sortable: true, - render: (transactionName: string, { sample }: ITransactionGroup) => { + render: (_, { sample }: TransactionGroup) => { return ( - {transactionName || NOT_AVAILABLE_LABEL} + {sample.transaction.name || NOT_AVAILABLE_LABEL} ); diff --git a/x-pack/plugins/apm/public/hooks/useTransactionList.ts b/x-pack/plugins/apm/public/hooks/useTransactionList.ts index ed6bb9309a557..0ad221b95b4ff 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionList.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionList.ts @@ -4,45 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useMemo } from 'react'; import { IUrlParams } from '../context/UrlParamsContext/types'; import { useUiFilters } from '../context/UrlParamsContext'; import { useFetcher } from './useFetcher'; import { APIReturnType } from '../services/rest/createCallApmApi'; -const getRelativeImpact = ( - impact: number, - impactMin: number, - impactMax: number -) => - Math.max( - ((impact - impactMin) / Math.max(impactMax - impactMin, 1)) * 100, - 1 - ); - type TransactionsAPIResponse = APIReturnType< '/api/apm/services/{serviceName}/transaction_groups' >; -function getWithRelativeImpact(items: TransactionsAPIResponse['items']) { - const impacts = items - .map(({ impact }) => impact) - .filter((impact) => impact !== null) as number[]; - - const impactMin = Math.min(...impacts); - const impactMax = Math.max(...impacts); - - return items.map((item) => { - return { - ...item, - impactRelative: - item.impact !== null - ? getRelativeImpact(item.impact, impactMin, impactMax) - : null, - }; - }); -} - const DEFAULT_RESPONSE: TransactionsAPIResponse = { items: [], isAggregationAccurate: true, @@ -72,16 +42,8 @@ export function useTransactionList(urlParams: IUrlParams) { [serviceName, start, end, transactionType, uiFilters] ); - const memoizedData = useMemo( - () => ({ - items: getWithRelativeImpact(data.items), - isAggregationAccurate: data.isAggregationAccurate, - bucketSize: data.bucketSize, - }), - [data] - ); return { - data: memoizedData, + data, status, error, }; diff --git a/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts b/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts index 28b095335e93d..c3cf363cbec05 100644 --- a/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts +++ b/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts @@ -10,7 +10,7 @@ import pLimit from 'p-limit'; import pRetry from 'p-retry'; import { parse, format } from 'url'; import { set } from '@elastic/safer-lodash-set'; -import { unique, without, merge, flatten } from 'lodash'; +import { uniq, without, merge, flatten } from 'lodash'; import * as histogram from 'hdr-histogram-js'; import { ESSearchResponse } from '../../typings/elasticsearch'; import { @@ -114,8 +114,8 @@ export async function aggregateLatencyMetrics() { .filter(Boolean) as string[]; const fields = only.length - ? unique(only) - : without(unique([...include, ...defaultFields]), ...exclude); + ? uniq(only) + : without(uniq([...include, ...defaultFields]), ...exclude); const globalFilter = argv.filter ? JSON.parse(String(argv.filter)) : {}; diff --git a/x-pack/plugins/apm/scripts/shared/read-kibana-config.ts b/x-pack/plugins/apm/scripts/shared/read-kibana-config.ts index bc5f1afc63cac..fe226c8ab27d2 100644 --- a/x-pack/plugins/apm/scripts/shared/read-kibana-config.ts +++ b/x-pack/plugins/apm/scripts/shared/read-kibana-config.ts @@ -6,7 +6,7 @@ import path from 'path'; import fs from 'fs'; import yaml from 'js-yaml'; -import { identity, pick } from 'lodash'; +import { identity, pickBy } from 'lodash'; export type KibanaConfig = ReturnType; @@ -22,7 +22,7 @@ export const readKibanaConfig = () => { ) ) || {}) as {}; - const cliEsCredentials = pick( + const cliEsCredentials = pickBy( { 'elasticsearch.username': process.env.ELASTICSEARCH_USERNAME, 'elasticsearch.password': process.env.ELASTICSEARCH_PASSWORD, diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts index 1d14c509274a8..a922457b14cea 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts @@ -157,7 +157,7 @@ export function registerTransactionDurationAlertType({ const { agg } = response.aggregations; - const value = 'values' in agg ? agg.values[0] : agg?.value; + const value = 'values' in agg ? Object.values(agg.values)[0] : agg?.value; if (value && value > alertParams.threshold * 1000) { const alertInstance = services.alertInstanceFactory( diff --git a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts index 6de2728ee4366..895920a9b6c7d 100644 --- a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Unionize } from 'utility-types'; +import { Unionize, Overwrite } from 'utility-types'; +import { ESSearchRequest } from '../../../typings/elasticsearch'; import { Setup, SetupTimeRange, @@ -17,14 +18,28 @@ import { getMetricsProjection } from '../../../common/projections/metrics'; import { mergeProjection } from '../../../common/projections/util/merge_projection'; import { AggregationOptionsByType } from '../../../typings/elasticsearch/aggregations'; -interface Aggs { - [key: string]: Unionize<{ - min: AggregationOptionsByType['min']; - max: AggregationOptionsByType['max']; - sum: AggregationOptionsByType['sum']; - avg: AggregationOptionsByType['avg']; - }>; -} +type MetricsAggregationMap = Unionize<{ + min: AggregationOptionsByType['min']; + max: AggregationOptionsByType['max']; + sum: AggregationOptionsByType['sum']; + avg: AggregationOptionsByType['avg']; +}>; + +type MetricAggs = Record; + +export type GenericMetricsRequest = Overwrite< + ESSearchRequest, + { + body: { + aggs: { + timeseriesData: { + date_histogram: AggregationOptionsByType['date_histogram']; + aggs: MetricAggs; + }; + } & MetricAggs; + }; + } +>; interface Filter { exists?: { @@ -35,7 +50,7 @@ interface Filter { }; } -export async function fetchAndTransformMetrics({ +export async function fetchAndTransformMetrics({ setup, serviceName, serviceNodeName, @@ -58,7 +73,7 @@ export async function fetchAndTransformMetrics({ serviceNodeName, }); - const params = mergeProjection(projection, { + const params: GenericMetricsRequest = mergeProjection(projection, { body: { size: 0, query: { diff --git a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts index affb7c2a12075..a191d5400e36c 100644 --- a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts +++ b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts @@ -4,40 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { Unionize, Overwrite } from 'utility-types'; import { ChartBase } from './types'; -import { - ESSearchResponse, - ESSearchRequest, -} from '../../../typings/elasticsearch'; -import { AggregationOptionsByType } from '../../../typings/elasticsearch/aggregations'; +import { ESSearchResponse } from '../../../typings/elasticsearch'; import { getVizColorForIndex } from '../../../common/viz_colors'; +import { GenericMetricsRequest } from './fetch_and_transform_metrics'; export type GenericMetricsChart = ReturnType< typeof transformDataToMetricsChart >; -interface MetricsAggregationMap { - min: AggregationOptionsByType['min']; - max: AggregationOptionsByType['max']; - sum: AggregationOptionsByType['sum']; - avg: AggregationOptionsByType['avg']; -} - -type GenericMetricsRequest = Overwrite< - ESSearchRequest, - { - body: { - aggs: { - timeseriesData: { - date_histogram: AggregationOptionsByType['date_histogram']; - aggs: Record>; - }; - } & Record>; - }; - } ->; - export function transformDataToMetricsChart( result: ESSearchResponse, chartBase: ChartBase @@ -51,11 +26,7 @@ export function transformDataToMetricsChart( yUnit: chartBase.yUnit, noHits: hits.total.value === 0, series: Object.keys(chartBase.series).map((seriesKey, i) => { - const overallValue = (aggregations?.[seriesKey] as - | { - value: number | null; - } - | undefined)?.value; + const overallValue = aggregations?.[seriesKey]?.value; return { title: chartBase.series[seriesKey].title, @@ -66,7 +37,7 @@ export function transformDataToMetricsChart( overallValue, data: timeseriesData?.buckets.map((bucket) => { - const { value } = bucket[seriesKey] as { value: number | null }; + const { value } = bucket[seriesKey]; const y = value === null || isNaN(value) ? null : value; return { x: bucket.key, diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts index c28bcad841ffd..de699028f5675 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { arrayUnionToCallable } from '../../../../common/utils/array_union_to_callable'; import { PROCESSOR_EVENT, TRANSACTION_DURATION, @@ -187,7 +186,7 @@ export const getTransactionRates = async ({ const deltaAsMinutes = getDeltaAsMinutes(setup); - return arrayUnionToCallable(aggregations.services.buckets).map((bucket) => { + return aggregations.services.buckets.map((bucket) => { const transactionsPerMinute = bucket.doc_count / deltaAsMinutes; return { serviceName: bucket.key as string, diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap deleted file mode 100644 index b354d3ed1f88d..0000000000000 --- a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap +++ /dev/null @@ -1,228 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`transactionGroupsFetcher type: top_traces should call client.search with correct query 1`] = ` -Array [ - Array [ - Object { - "body": Object { - "aggs": Object { - "transaction_groups": Object { - "aggs": Object { - "avg": Object { - "avg": Object { - "field": "transaction.duration.us", - }, - }, - "p95": Object { - "percentiles": Object { - "field": "transaction.duration.us", - "hdr": Object { - "number_of_significant_value_digits": 2, - }, - "percents": Array [ - 95, - ], - }, - }, - "sample": Object { - "top_hits": Object { - "size": 1, - "sort": Array [ - Object { - "_score": "desc", - }, - Object { - "@timestamp": Object { - "order": "desc", - }, - }, - ], - }, - }, - "sum": Object { - "sum": Object { - "field": "transaction.duration.us", - }, - }, - }, - "composite": Object { - "size": 10000, - "sources": Array [ - Object { - "service": Object { - "terms": Object { - "field": "service.name", - }, - }, - }, - Object { - "transaction": Object { - "terms": Object { - "field": "transaction.name", - }, - }, - }, - ], - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, - }, - }, - }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, - Object { - "term": Object { - "service.environment": "test", - }, - }, - ], - "must_not": Array [ - Object { - "exists": Object { - "field": "parent.id", - }, - }, - ], - "should": Array [ - Object { - "term": Object { - "transaction.sampled": true, - }, - }, - ], - }, - }, - "size": 0, - }, - "index": "myIndex", - }, - ], -] -`; - -exports[`transactionGroupsFetcher type: top_transactions should call client.search with correct query 1`] = ` -Array [ - Array [ - Object { - "body": Object { - "aggs": Object { - "transaction_groups": Object { - "aggs": Object { - "avg": Object { - "avg": Object { - "field": "transaction.duration.us", - }, - }, - "p95": Object { - "percentiles": Object { - "field": "transaction.duration.us", - "hdr": Object { - "number_of_significant_value_digits": 2, - }, - "percents": Array [ - 95, - ], - }, - }, - "sample": Object { - "top_hits": Object { - "size": 1, - "sort": Array [ - Object { - "_score": "desc", - }, - Object { - "@timestamp": Object { - "order": "desc", - }, - }, - ], - }, - }, - "sum": Object { - "sum": Object { - "field": "transaction.duration.us", - }, - }, - }, - "composite": Object { - "size": 101, - "sources": Array [ - Object { - "transaction": Object { - "terms": Object { - "field": "transaction.name", - }, - }, - }, - ], - }, - }, - "transactions": Object { - "terms": Object { - "field": "transaction.name", - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, - }, - }, - }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, - Object { - "term": Object { - "transaction.type": "request", - }, - }, - Object { - "term": Object { - "service.name": "opbeans-node", - }, - }, - Object { - "term": Object { - "service.environment": "test", - }, - }, - ], - "should": Array [ - Object { - "term": Object { - "transaction.sampled": true, - }, - }, - ], - }, - }, - "size": 0, - }, - "index": "myIndex", - }, - ], -] -`; diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap index 884a7d18cc4d4..deca46f4ebd0c 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap @@ -1,220 +1,479 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`transaction group queries fetches top traces 1`] = ` -Object { - "body": Object { - "aggs": Object { - "transaction_groups": Object { - "aggs": Object { - "avg": Object { - "avg": Object { - "field": "transaction.duration.us", - }, - }, - "p95": Object { - "percentiles": Object { - "field": "transaction.duration.us", - "hdr": Object { - "number_of_significant_value_digits": 2, +Array [ + Object { + "body": Object { + "aggs": Object { + "transaction_groups": Object { + "aggs": Object { + "sample": Object { + "top_hits": Object { + "size": 1, }, - "percents": Array [ - 95, - ], }, }, - "sample": Object { - "top_hits": Object { - "size": 1, - "sort": Array [ - Object { - "_score": "desc", + "composite": Object { + "size": 10000, + "sources": Array [ + Object { + "service.name": Object { + "terms": Object { + "field": "service.name", + }, }, - Object { - "@timestamp": Object { - "order": "desc", + }, + Object { + "transaction.name": Object { + "terms": Object { + "field": "transaction.name", }, }, - ], + }, + ], + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "my.custom.ui.filter": "foo-bar", + }, + }, + ], + "must_not": Array [ + Object { + "exists": Object { + "field": "parent.id", + }, + }, + ], + "should": Array [ + Object { + "term": Object { + "transaction.sampled": true, + }, }, + ], + }, + }, + "sort": Array [ + Object { + "_score": "desc", + }, + Object { + "@timestamp": Object { + "order": "desc", }, - "sum": Object { - "sum": Object { - "field": "transaction.duration.us", + }, + ], + }, + "index": "myIndex", + "size": 0, + }, + Object { + "body": Object { + "aggs": Object { + "transaction_groups": Object { + "aggs": Object { + "avg": Object { + "avg": Object { + "field": "transaction.duration.us", + }, }, }, + "composite": Object { + "size": 10000, + "sources": Array [ + Object { + "service.name": Object { + "terms": Object { + "field": "service.name", + }, + }, + }, + Object { + "transaction.name": Object { + "terms": Object { + "field": "transaction.name", + }, + }, + }, + ], + }, }, - "composite": Object { - "size": 10000, - "sources": Array [ + }, + "query": Object { + "bool": Object { + "filter": Array [ Object { - "service": Object { - "terms": Object { - "field": "service.name", + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, }, }, }, Object { - "transaction": Object { - "terms": Object { - "field": "transaction.name", - }, + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "my.custom.ui.filter": "foo-bar", + }, + }, + ], + "must_not": Array [ + Object { + "exists": Object { + "field": "parent.id", }, }, ], }, }, }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, + "index": "myIndex", + "size": 0, + }, + Object { + "body": Object { + "aggs": Object { + "transaction_groups": Object { + "aggs": Object { + "sum": Object { + "sum": Object { + "field": "transaction.duration.us", }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, + "composite": Object { + "size": 10000, + "sources": Array [ + Object { + "service.name": Object { + "terms": Object { + "field": "service.name", + }, + }, + }, + Object { + "transaction.name": Object { + "terms": Object { + "field": "transaction.name", + }, + }, + }, + ], }, - Object { - "term": Object { - "my.custom.ui.filter": "foo-bar", + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, }, - }, - ], - "must_not": Array [ - Object { - "exists": Object { - "field": "parent.id", + Object { + "term": Object { + "processor.event": "transaction", + }, }, - }, - ], - "should": Array [ - Object { - "term": Object { - "transaction.sampled": true, + Object { + "term": Object { + "my.custom.ui.filter": "foo-bar", + }, }, - }, - ], + ], + "must_not": Array [ + Object { + "exists": Object { + "field": "parent.id", + }, + }, + ], + }, }, }, + "index": "myIndex", "size": 0, }, - "index": "myIndex", -} +] `; exports[`transaction group queries fetches top transactions 1`] = ` -Object { - "body": Object { - "aggs": Object { - "transaction_groups": Object { - "aggs": Object { - "avg": Object { - "avg": Object { - "field": "transaction.duration.us", - }, - }, - "p95": Object { - "percentiles": Object { - "field": "transaction.duration.us", - "hdr": Object { - "number_of_significant_value_digits": 2, +Array [ + Object { + "body": Object { + "aggs": Object { + "transaction_groups": Object { + "aggs": Object { + "sample": Object { + "top_hits": Object { + "size": 1, }, - "percents": Array [ - 95, - ], }, }, - "sample": Object { - "top_hits": Object { - "size": 1, - "sort": Array [ - Object { - "_score": "desc", - }, - Object { - "@timestamp": Object { - "order": "desc", - }, + "terms": Object { + "field": "transaction.name", + "size": 101, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, }, - ], + }, }, + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "transaction.type": "bar", + }, + }, + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "term": Object { + "my.custom.ui.filter": "foo-bar", + }, + }, + ], + "should": Array [ + Object { + "term": Object { + "transaction.sampled": true, + }, + }, + ], + }, + }, + "sort": Array [ + Object { + "_score": "desc", + }, + Object { + "@timestamp": Object { + "order": "desc", }, - "sum": Object { - "sum": Object { - "field": "transaction.duration.us", + }, + ], + }, + "index": "myIndex", + "size": 0, + }, + Object { + "body": Object { + "aggs": Object { + "transaction_groups": Object { + "aggs": Object { + "avg": Object { + "avg": Object { + "field": "transaction.duration.us", + }, }, }, + "terms": Object { + "field": "transaction.name", + "size": 101, + }, }, - "composite": Object { - "size": 101, - "sources": Array [ + }, + "query": Object { + "bool": Object { + "filter": Array [ Object { - "transaction": Object { - "terms": Object { - "field": "transaction.name", + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, }, }, }, + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "transaction.type": "bar", + }, + }, + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "term": Object { + "my.custom.ui.filter": "foo-bar", + }, + }, ], }, }, - "transactions": Object { - "terms": Object { - "field": "transaction.name", + }, + "index": "myIndex", + "size": 0, + }, + Object { + "body": Object { + "aggs": Object { + "transaction_groups": Object { + "aggs": Object { + "sum": Object { + "sum": Object { + "field": "transaction.duration.us", + }, + }, + }, + "terms": Object { + "field": "transaction.name", + "size": 101, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "transaction.type": "bar", + }, + }, + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "term": Object { + "my.custom.ui.filter": "foo-bar", + }, + }, + ], }, }, }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, + "index": "myIndex", + "size": 0, + }, + Object { + "body": Object { + "aggs": Object { + "transaction_groups": Object { + "aggs": Object { + "p95": Object { + "percentiles": Object { + "field": "transaction.duration.us", + "hdr": Object { + "number_of_significant_value_digits": 2, + }, + "percents": Array [ + 95, + ], }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, + "terms": Object { + "field": "transaction.name", + "size": 101, }, - Object { - "term": Object { - "transaction.type": "bar", + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, }, - }, - Object { - "term": Object { - "service.name": "foo", + Object { + "term": Object { + "processor.event": "transaction", + }, }, - }, - Object { - "term": Object { - "my.custom.ui.filter": "foo-bar", + Object { + "term": Object { + "transaction.type": "bar", + }, }, - }, - ], - "should": Array [ - Object { - "term": Object { - "transaction.sampled": true, + Object { + "term": Object { + "service.name": "foo", + }, }, - }, - ], + Object { + "term": Object { + "my.custom.ui.filter": "foo-bar", + }, + }, + ], + }, }, }, + "index": "myIndex", "size": 0, }, - "index": "myIndex", -} +] `; diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/transform.test.ts.snap b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/transform.test.ts.snap deleted file mode 100644 index 66b805ab2efc1..0000000000000 --- a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/transform.test.ts.snap +++ /dev/null @@ -1,2822 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`transactionGroupsTransformer should match snapshot 1`] = ` -Array [ - Object { - "averageResponseTime": 48021.972616494, - "impact": 100, - "name": "GET /api", - "p95": 67138.18364917398, - "sample": Object { - "@timestamp": "2018-11-18T20:53:44.070Z", - "agent": Object { - "hostname": "b359e3afece8", - "type": "apm-server", - "version": "7.0.0-alpha1", - }, - "context": Object { - "custom": Object { - "containerId": 5176, - }, - "process": Object { - "argv": Array [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js", - ], - "pid": 3756, - "ppid": 1, - "title": "node /app/server.js", - }, - "request": Object { - "headers": Object { - "accept": "*/*", - "accept-encoding": "gzip, deflate", - "connection": "keep-alive", - "elastic-apm-traceparent": "00-86c68779d8a65b06fb78e770ffc436a5-4aaea53dc1791183-01", - "host": "opbeans-node:3000", - "user-agent": "python-requests/2.20.0", - }, - "http_version": "1.1", - "method": "GET", - "socket": Object { - "encrypted": false, - "remote_address": "::ffff:172.18.0.6", - }, - "url": Object { - "full": "http://opbeans-node:3000/api/types/3", - "hostname": "opbeans-node", - "pathname": "/api/types/3", - "port": "3000", - "protocol": "http:", - "raw": "/api/types/3", - }, - }, - "response": Object { - "headers": Object { - "connection": "close", - "content-type": "application/json;charset=UTF-8", - "date": "Sun, 18 Nov 2018 20:53:43 GMT", - "transfer-encoding": "chunked", - "x-powered-by": "Express", - }, - "status_code": 404, - }, - "service": Object { - "agent": Object { - "name": "nodejs", - "version": "1.14.2", - }, - "language": Object { - "name": "javascript", - }, - "name": "opbeans-node", - "runtime": Object { - "name": "node", - "version": "8.12.0", - }, - "version": "1.0.0", - }, - "system": Object { - "architecture": "x64", - "hostname": "98195610c255", - "ip": "172.18.0.10", - "platform": "linux", - }, - "tags": Object { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo -bar -baz", - "this-is-a-very-long-tag-name-without-any-spaces": "test", - }, - "user": Object { - "email": "kimchy@elastic.co", - "id": "42", - "username": "kimchy", - }, - }, - "host": Object { - "name": "b359e3afece8", - }, - "parent": Object { - "id": "4aaea53dc1791183", - }, - "processor": Object { - "event": "transaction", - "name": "transaction", - }, - "timestamp": Object { - "us": 1542574424070007, - }, - "trace": Object { - "id": "86c68779d8a65b06fb78e770ffc436a5", - }, - "transaction": Object { - "duration": Object { - "us": 8684, - }, - "id": "a78bca581dcd8ff8", - "name": "GET /api", - "result": "HTTP 4xx", - "sampled": true, - "span_count": Object { - "started": 1, - }, - "type": "request", - }, - }, - "transactionsPerMinute": 691926.3157894736, - }, - Object { - "averageResponseTime": 2651.8784461553205, - "impact": 15.770246496477105, - "name": "GET static file", - "p95": 6140.579335038363, - "sample": Object { - "@timestamp": "2018-11-18T20:53:43.304Z", - "agent": Object { - "hostname": "b359e3afece8", - "type": "apm-server", - "version": "7.0.0-alpha1", - }, - "context": Object { - "process": Object { - "argv": Array [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js", - ], - "pid": 3756, - "ppid": 1, - "title": "node /app/server.js", - }, - "request": Object { - "headers": Object { - "accept": "*/*", - "host": "opbeans-node:3000", - "user-agent": "curl/7.38.0", - }, - "http_version": "1.1", - "method": "GET", - "socket": Object { - "encrypted": false, - "remote_address": "::ffff:172.18.0.10", - }, - "url": Object { - "full": "http://opbeans-node:3000/", - "hostname": "opbeans-node", - "pathname": "/", - "port": "3000", - "protocol": "http:", - "raw": "/", - }, - }, - "response": Object { - "headers": Object { - "accept-ranges": "bytes", - "cache-control": "public, max-age=0", - "connection": "keep-alive", - "content-length": "640", - "content-type": "text/html; charset=UTF-8", - "date": "Sun, 18 Nov 2018 20:53:43 GMT", - "etag": "W/\\"280-1670775e878\\"", - "last-modified": "Mon, 12 Nov 2018 10:27:07 GMT", - "x-powered-by": "Express", - }, - "status_code": 200, - }, - "service": Object { - "agent": Object { - "name": "nodejs", - "version": "1.14.2", - }, - "language": Object { - "name": "javascript", - }, - "name": "opbeans-node", - "runtime": Object { - "name": "node", - "version": "8.12.0", - }, - "version": "1.0.0", - }, - "system": Object { - "architecture": "x64", - "hostname": "98195610c255", - "ip": "172.18.0.10", - "platform": "linux", - }, - }, - "host": Object { - "name": "b359e3afece8", - }, - "processor": Object { - "event": "transaction", - "name": "transaction", - }, - "timestamp": Object { - "us": 1542574423304006, - }, - "trace": Object { - "id": "b303d2a4a007946b63b9db7fafe639a0", - }, - "transaction": Object { - "duration": Object { - "us": 1801, - }, - "id": "2869c13633534be5", - "name": "GET static file", - "result": "HTTP 2xx", - "sampled": true, - "span_count": Object { - "started": 0, - }, - "type": "request", - }, - }, - "transactionsPerMinute": 1977031.5789473683, - }, - Object { - "averageResponseTime": 32554.36257814184, - "impact": 14.344171563678346, - "name": "GET /api/stats", - "p95": 59356.73611111111, - "sample": Object { - "@timestamp": "2018-11-18T20:53:42.560Z", - "agent": Object { - "hostname": "b359e3afece8", - "type": "apm-server", - "version": "7.0.0-alpha1", - }, - "context": Object { - "custom": Object { - "containerId": 207, - }, - "process": Object { - "argv": Array [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js", - ], - "pid": 3756, - "ppid": 1, - "title": "node /app/server.js", - }, - "request": Object { - "headers": Object { - "accept": "*/*", - "accept-encoding": "gzip, deflate", - "connection": "keep-alive", - "elastic-apm-traceparent": "00-63ccc3b0929dafb7f2fbcabdc7f7af25-821a787e73ab1563-01", - "host": "opbeans-node:3000", - "if-none-match": "W/\\"77-uxKJrX5GSMJJWTKh3orUFAEVxSs\\"", - "referer": "http://opbeans-node:3000/dashboard", - "user-agent": "Chromeless 1.4.0", - }, - "http_version": "1.1", - "method": "GET", - "socket": Object { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7", - }, - "url": Object { - "full": "http://opbeans-node:3000/api/stats", - "hostname": "opbeans-node", - "pathname": "/api/stats", - "port": "3000", - "protocol": "http:", - "raw": "/api/stats", - }, - }, - "response": Object { - "headers": Object { - "connection": "keep-alive", - "date": "Sun, 18 Nov 2018 20:53:42 GMT", - "etag": "W/\\"77-uxKJrX5GSMJJWTKh3orUFAEVxSs\\"", - "x-powered-by": "Express", - }, - "status_code": 304, - }, - "service": Object { - "agent": Object { - "name": "nodejs", - "version": "1.14.2", - }, - "language": Object { - "name": "javascript", - }, - "name": "opbeans-node", - "runtime": Object { - "name": "node", - "version": "8.12.0", - }, - "version": "1.0.0", - }, - "system": Object { - "architecture": "x64", - "hostname": "98195610c255", - "ip": "172.18.0.10", - "platform": "linux", - }, - "tags": Object { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo -bar -baz", - "this-is-a-very-long-tag-name-without-any-spaces": "test", - }, - "user": Object { - "email": "kimchy@elastic.co", - "id": "42", - "username": "kimchy", - }, - }, - "host": Object { - "name": "b359e3afece8", - }, - "parent": Object { - "id": "821a787e73ab1563", - }, - "processor": Object { - "event": "transaction", - "name": "transaction", - }, - "timestamp": Object { - "us": 1542574422560002, - }, - "trace": Object { - "id": "63ccc3b0929dafb7f2fbcabdc7f7af25", - }, - "transaction": Object { - "duration": Object { - "us": 28753, - }, - "id": "fb754e7628da2fb5", - "name": "GET /api/stats", - "result": "HTTP 3xx", - "sampled": true, - "span_count": Object { - "started": 7, - }, - "type": "request", - }, - }, - "transactionsPerMinute": 146494.73684210525, - }, - Object { - "averageResponseTime": 32159.926322043968, - "impact": 10.27904952170656, - "name": "GET /api/customers", - "p95": 59845.85714285714, - "sample": Object { - "@timestamp": "2018-11-18T20:53:21.180Z", - "agent": Object { - "hostname": "b359e3afece8", - "type": "apm-server", - "version": "7.0.0-alpha1", - }, - "context": Object { - "custom": Object { - "containerId": 2531, - }, - "process": Object { - "argv": Array [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js", - ], - "pid": 3710, - "ppid": 1, - "title": "node /app/server.js", - }, - "request": Object { - "headers": Object { - "accept": "*/*", - "accept-encoding": "gzip, deflate", - "connection": "keep-alive", - "elastic-apm-traceparent": "00-541025da8ecc2f51f21c1a4ad6992b77-ca18d9d4c3879519-01", - "host": "opbeans-node:3000", - "user-agent": "python-requests/2.20.0", - }, - "http_version": "1.1", - "method": "GET", - "socket": Object { - "encrypted": false, - "remote_address": "::ffff:172.18.0.6", - }, - "url": Object { - "full": "http://opbeans-node:3000/api/customers", - "hostname": "opbeans-node", - "pathname": "/api/customers", - "port": "3000", - "protocol": "http:", - "raw": "/api/customers", - }, - }, - "response": Object { - "headers": Object { - "connection": "keep-alive", - "content-length": "186769", - "content-type": "application/json; charset=utf-8", - "date": "Sun, 18 Nov 2018 20:53:21 GMT", - "etag": "W/\\"2d991-yG3J8W/roH7fSxXTudZrO27Ax9s\\"", - "x-powered-by": "Express", - }, - "status_code": 200, - }, - "service": Object { - "agent": Object { - "name": "nodejs", - "version": "1.14.2", - }, - "language": Object { - "name": "javascript", - }, - "name": "opbeans-node", - "runtime": Object { - "name": "node", - "version": "8.12.0", - }, - "version": "1.0.0", - }, - "system": Object { - "architecture": "x64", - "hostname": "98195610c255", - "ip": "172.18.0.10", - "platform": "linux", - }, - "tags": Object { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo -bar -baz", - "this-is-a-very-long-tag-name-without-any-spaces": "test", - }, - "user": Object { - "email": "kimchy@elastic.co", - "id": "42", - "username": "kimchy", - }, - }, - "host": Object { - "name": "b359e3afece8", - }, - "parent": Object { - "id": "ca18d9d4c3879519", - }, - "processor": Object { - "event": "transaction", - "name": "transaction", - }, - "timestamp": Object { - "us": 1542574401180002, - }, - "trace": Object { - "id": "541025da8ecc2f51f21c1a4ad6992b77", - }, - "transaction": Object { - "duration": Object { - "us": 18077, - }, - "id": "94852b9dd1075982", - "name": "GET /api/customers", - "result": "HTTP 2xx", - "sampled": true, - "span_count": Object { - "started": 2, - }, - "type": "request", - }, - }, - "transactionsPerMinute": 106294.73684210525, - }, - Object { - "averageResponseTime": 33265.03326147213, - "impact": 10.256357027376065, - "name": "GET /api/orders", - "p95": 58827.489999999976, - "sample": Object { - "@timestamp": "2018-11-18T20:53:40.973Z", - "agent": Object { - "hostname": "b359e3afece8", - "type": "apm-server", - "version": "7.0.0-alpha1", - }, - "context": Object { - "custom": Object { - "containerId": 408, - }, - "process": Object { - "argv": Array [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js", - ], - "pid": 3756, - "ppid": 1, - "title": "node /app/server.js", - }, - "request": Object { - "headers": Object { - "connection": "close", - "host": "opbeans-node:3000", - "user-agent": "workload/2.4.3", - }, - "http_version": "1.1", - "method": "GET", - "socket": Object { - "encrypted": false, - "remote_address": "::ffff:172.18.0.10", - }, - "url": Object { - "full": "http://opbeans-node:3000/api/orders", - "hostname": "opbeans-node", - "pathname": "/api/orders", - "port": "3000", - "protocol": "http:", - "raw": "/api/orders", - }, - }, - "response": Object { - "headers": Object { - "connection": "close", - "content-length": "103612", - "content-type": "application/json; charset=utf-8", - "date": "Sun, 18 Nov 2018 20:53:40 GMT", - "etag": "W/\\"194bc-cOw6+iRf7XCeqMXHrle3IOig7tY\\"", - "x-powered-by": "Express", - }, - "status_code": 200, - }, - "service": Object { - "agent": Object { - "name": "nodejs", - "version": "1.14.2", - }, - "language": Object { - "name": "javascript", - }, - "name": "opbeans-node", - "runtime": Object { - "name": "node", - "version": "8.12.0", - }, - "version": "1.0.0", - }, - "system": Object { - "architecture": "x64", - "hostname": "98195610c255", - "ip": "172.18.0.10", - "platform": "linux", - }, - "tags": Object { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo -bar -baz", - "this-is-a-very-long-tag-name-without-any-spaces": "test", - }, - "user": Object { - "email": "kimchy@elastic.co", - "id": "42", - "username": "kimchy", - }, - }, - "host": Object { - "name": "b359e3afece8", - }, - "processor": Object { - "event": "transaction", - "name": "transaction", - }, - "timestamp": Object { - "us": 1542574420973006, - }, - "trace": Object { - "id": "0afce85f593cbbdd09949936fe964f0f", - }, - "transaction": Object { - "duration": Object { - "us": 23040, - }, - "id": "89f200353eb50539", - "name": "GET /api/orders", - "result": "HTTP 2xx", - "sampled": true, - "span_count": Object { - "started": 2, - }, - "type": "request", - }, - }, - "transactionsPerMinute": 102536.84210526315, - }, - Object { - "averageResponseTime": 27516.89144558744, - "impact": 9.651458992731666, - "name": "GET /api/products/top", - "p95": 56064.679999999986, - "sample": Object { - "@timestamp": "2018-11-18T20:52:57.316Z", - "agent": Object { - "hostname": "b359e3afece8", - "type": "apm-server", - "version": "7.0.0-alpha1", - }, - "context": Object { - "custom": Object { - "containerId": 5113, - }, - "process": Object { - "argv": Array [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js", - ], - "pid": 3686, - "ppid": 1, - "title": "node /app/server.js", - }, - "request": Object { - "headers": Object { - "accept": "*/*", - "accept-encoding": "gzip, deflate", - "connection": "keep-alive", - "elastic-apm-traceparent": "00-74f12e705936d66350f4741ebeb55189-fcebe94cd2136215-01", - "host": "opbeans-node:3000", - "referer": "http://opbeans-node:3000/dashboard", - "user-agent": "Chromeless 1.4.0", - }, - "http_version": "1.1", - "method": "GET", - "socket": Object { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7", - }, - "url": Object { - "full": "http://opbeans-node:3000/api/products/top", - "hostname": "opbeans-node", - "pathname": "/api/products/top", - "port": "3000", - "protocol": "http:", - "raw": "/api/products/top", - }, - }, - "response": Object { - "headers": Object { - "connection": "keep-alive", - "content-length": "282", - "content-type": "application/json; charset=utf-8", - "date": "Sun, 18 Nov 2018 20:52:57 GMT", - "etag": "W/\\"11a-lcI9zuMZYYsDRpEZgYqDYr96cKM\\"", - "x-powered-by": "Express", - }, - "status_code": 200, - }, - "service": Object { - "agent": Object { - "name": "nodejs", - "version": "1.14.2", - }, - "language": Object { - "name": "javascript", - }, - "name": "opbeans-node", - "runtime": Object { - "name": "node", - "version": "8.12.0", - }, - "version": "1.0.0", - }, - "system": Object { - "architecture": "x64", - "hostname": "98195610c255", - "ip": "172.18.0.10", - "platform": "linux", - }, - "tags": Object { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo -bar -baz", - "this-is-a-very-long-tag-name-without-any-spaces": "test", - }, - "user": Object { - "email": "kimchy@elastic.co", - "id": "42", - "username": "kimchy", - }, - }, - "host": Object { - "name": "b359e3afece8", - }, - "parent": Object { - "id": "fcebe94cd2136215", - }, - "processor": Object { - "event": "transaction", - "name": "transaction", - }, - "timestamp": Object { - "us": 1542574377316005, - }, - "trace": Object { - "id": "74f12e705936d66350f4741ebeb55189", - }, - "transaction": Object { - "duration": Object { - "us": 48781, - }, - "id": "be4bd5475d5d9e6f", - "name": "GET /api/products/top", - "result": "HTTP 2xx", - "sampled": true, - "span_count": Object { - "started": 4, - }, - "type": "request", - }, - }, - "transactionsPerMinute": 116652.63157894736, - }, - Object { - "averageResponseTime": 12683.190864600327, - "impact": 4.4239778504968, - "name": "GET /api/products", - "p95": 35009.67999999999, - "sample": Object { - "@timestamp": "2018-11-18T20:53:43.477Z", - "agent": Object { - "hostname": "b359e3afece8", - "type": "apm-server", - "version": "7.0.0-alpha1", - }, - "context": Object { - "custom": Object { - "containerId": 2857, - }, - "process": Object { - "argv": Array [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js", - ], - "pid": 3756, - "ppid": 1, - "title": "node /app/server.js", - }, - "request": Object { - "headers": Object { - "connection": "close", - "host": "opbeans-node:3000", - "user-agent": "workload/2.4.3", - }, - "http_version": "1.1", - "method": "GET", - "socket": Object { - "encrypted": false, - "remote_address": "::ffff:172.18.0.10", - }, - "url": Object { - "full": "http://opbeans-node:3000/api/products", - "hostname": "opbeans-node", - "pathname": "/api/products", - "port": "3000", - "protocol": "http:", - "raw": "/api/products", - }, - }, - "response": Object { - "headers": Object { - "connection": "close", - "content-length": "1023", - "content-type": "application/json; charset=utf-8", - "date": "Sun, 18 Nov 2018 20:53:43 GMT", - "etag": "W/\\"3ff-VyOxcDApb+a/lnjkm9FeTOGSDrs\\"", - "x-powered-by": "Express", - }, - "status_code": 200, - }, - "service": Object { - "agent": Object { - "name": "nodejs", - "version": "1.14.2", - }, - "language": Object { - "name": "javascript", - }, - "name": "opbeans-node", - "runtime": Object { - "name": "node", - "version": "8.12.0", - }, - "version": "1.0.0", - }, - "system": Object { - "architecture": "x64", - "hostname": "98195610c255", - "ip": "172.18.0.10", - "platform": "linux", - }, - "tags": Object { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo -bar -baz", - "this-is-a-very-long-tag-name-without-any-spaces": "test", - }, - "user": Object { - "email": "kimchy@elastic.co", - "id": "42", - "username": "kimchy", - }, - }, - "host": Object { - "name": "b359e3afece8", - }, - "processor": Object { - "event": "transaction", - "name": "transaction", - }, - "timestamp": Object { - "us": 1542574423477006, - }, - "trace": Object { - "id": "bee00a8efb523ca4b72adad57f7caba3", - }, - "transaction": Object { - "duration": Object { - "us": 6915, - }, - "id": "d8fc6d3b8707b64c", - "name": "GET /api/products", - "result": "HTTP 2xx", - "sampled": true, - "span_count": Object { - "started": 2, - }, - "type": "request", - }, - }, - "transactionsPerMinute": 116147.36842105263, - }, - Object { - "averageResponseTime": 255966.30555555556, - "impact": 4.3693406535517445, - "name": "POST /api/orders", - "p95": 320238.5, - "sample": Object { - "@timestamp": "2018-11-18T20:43:32.010Z", - "agent": Object { - "hostname": "b359e3afece8", - "type": "apm-server", - "version": "7.0.0-alpha1", - }, - "context": Object { - "custom": Object { - "containerId": 4669, - }, - "process": Object { - "argv": Array [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js", - ], - "pid": 2413, - "ppid": 1, - "title": "node /app/server.js", - }, - "request": Object { - "body": "[REDACTED]", - "headers": Object { - "accept": "application/json", - "connection": "close", - "content-length": "129", - "content-type": "application/json", - "host": "opbeans-node:3000", - "user-agent": "workload/2.4.3", - }, - "http_version": "1.1", - "method": "POST", - "socket": Object { - "encrypted": false, - "remote_address": "::ffff:172.18.0.10", - }, - "url": Object { - "full": "http://opbeans-node:3000/api/orders", - "hostname": "opbeans-node", - "pathname": "/api/orders", - "port": "3000", - "protocol": "http:", - "raw": "/api/orders", - }, - }, - "response": Object { - "headers": Object { - "connection": "close", - "content-length": "13", - "content-type": "application/json; charset=utf-8", - "date": "Sun, 18 Nov 2018 20:43:32 GMT", - "etag": "W/\\"d-g9K2iK4ordyN88lGL4LmPlYNfhc\\"", - "x-powered-by": "Express", - }, - "status_code": 200, - }, - "service": Object { - "agent": Object { - "name": "nodejs", - "version": "1.14.2", - }, - "language": Object { - "name": "javascript", - }, - "name": "opbeans-node", - "runtime": Object { - "name": "node", - "version": "8.12.0", - }, - "version": "1.0.0", - }, - "system": Object { - "architecture": "x64", - "hostname": "98195610c255", - "ip": "172.18.0.10", - "platform": "linux", - }, - "tags": Object { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo -bar -baz", - "this-is-a-very-long-tag-name-without-any-spaces": "test", - }, - "user": Object { - "email": "kimchy@elastic.co", - "id": "42", - "username": "kimchy", - }, - }, - "host": Object { - "name": "b359e3afece8", - }, - "processor": Object { - "event": "transaction", - "name": "transaction", - }, - "timestamp": Object { - "us": 1542573812010006, - }, - "trace": Object { - "id": "2b1252a338249daeecf6afb0c236e31b", - }, - "transaction": Object { - "duration": Object { - "us": 291572, - }, - "id": "2c9f39e9ec4a0111", - "name": "POST /api/orders", - "result": "HTTP 2xx", - "sampled": true, - "span_count": Object { - "started": 16, - }, - "type": "request", - }, - }, - "transactionsPerMinute": 5684.210526315789, - }, - Object { - "averageResponseTime": 17189.329210275926, - "impact": 3.424381787142002, - "name": "GET /api/products/:id/customers", - "p95": 39284.79999999999, - "sample": Object { - "@timestamp": "2018-11-18T20:48:24.769Z", - "agent": Object { - "hostname": "b359e3afece8", - "type": "apm-server", - "version": "7.0.0-alpha1", - }, - "context": Object { - "custom": Object { - "containerId": 1735, - }, - "process": Object { - "argv": Array [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js", - ], - "pid": 3100, - "ppid": 1, - "title": "node /app/server.js", - }, - "request": Object { - "headers": Object { - "accept": "*/*", - "accept-encoding": "gzip, deflate", - "connection": "keep-alive", - "elastic-apm-traceparent": "00-28f178c354d17f400dea04bc4a7b3c57-68f5d1607cac7779-01", - "host": "opbeans-node:3000", - "user-agent": "python-requests/2.20.0", - }, - "http_version": "1.1", - "method": "GET", - "socket": Object { - "encrypted": false, - "remote_address": "::ffff:172.18.0.6", - }, - "url": Object { - "full": "http://opbeans-node:3000/api/products/2/customers", - "hostname": "opbeans-node", - "pathname": "/api/products/2/customers", - "port": "3000", - "protocol": "http:", - "raw": "/api/products/2/customers", - }, - }, - "response": Object { - "headers": Object { - "connection": "keep-alive", - "content-length": "186570", - "content-type": "application/json; charset=utf-8", - "date": "Sun, 18 Nov 2018 20:48:24 GMT", - "etag": "W/\\"2d8ca-Z9NzuHyGyxwtzpOkcIxBvzm24iw\\"", - "x-powered-by": "Express", - }, - "status_code": 200, - }, - "service": Object { - "agent": Object { - "name": "nodejs", - "version": "1.14.2", - }, - "language": Object { - "name": "javascript", - }, - "name": "opbeans-node", - "runtime": Object { - "name": "node", - "version": "8.12.0", - }, - "version": "1.0.0", - }, - "system": Object { - "architecture": "x64", - "hostname": "98195610c255", - "ip": "172.18.0.10", - "platform": "linux", - }, - "tags": Object { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo -bar -baz", - "this-is-a-very-long-tag-name-without-any-spaces": "test", - }, - "user": Object { - "email": "kimchy@elastic.co", - "id": "42", - "username": "kimchy", - }, - }, - "host": Object { - "name": "b359e3afece8", - }, - "parent": Object { - "id": "68f5d1607cac7779", - }, - "processor": Object { - "event": "transaction", - "name": "transaction", - }, - "timestamp": Object { - "us": 1542574104769029, - }, - "trace": Object { - "id": "28f178c354d17f400dea04bc4a7b3c57", - }, - "transaction": Object { - "duration": Object { - "us": 49338, - }, - "id": "2a87ae20ad04ee0c", - "name": "GET /api/products/:id/customers", - "result": "HTTP 2xx", - "sampled": true, - "span_count": Object { - "started": 1, - }, - "type": "request", - }, - }, - "transactionsPerMinute": 66378.94736842105, - }, - Object { - "averageResponseTime": 11257.757916666667, - "impact": 2.558180605569336, - "name": "GET /api/types", - "p95": 35222.944444444445, - "sample": Object { - "@timestamp": "2018-11-18T20:53:44.978Z", - "agent": Object { - "hostname": "b359e3afece8", - "type": "apm-server", - "version": "7.0.0-alpha1", - }, - "context": Object { - "custom": Object { - "containerId": 2193, - }, - "process": Object { - "argv": Array [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js", - ], - "pid": 3756, - "ppid": 1, - "title": "node /app/server.js", - }, - "request": Object { - "headers": Object { - "connection": "close", - "host": "opbeans-node:3000", - "user-agent": "workload/2.4.3", - }, - "http_version": "1.1", - "method": "GET", - "socket": Object { - "encrypted": false, - "remote_address": "::ffff:172.18.0.10", - }, - "url": Object { - "full": "http://opbeans-node:3000/api/types", - "hostname": "opbeans-node", - "pathname": "/api/types", - "port": "3000", - "protocol": "http:", - "raw": "/api/types", - }, - }, - "response": Object { - "headers": Object { - "connection": "close", - "content-length": "112", - "content-type": "application/json; charset=utf-8", - "date": "Sun, 18 Nov 2018 20:53:44 GMT", - "etag": "W/\\"70-1z6hT7P1WHgBgS/BeUEVeHhOCQU\\"", - "x-powered-by": "Express", - }, - "status_code": 200, - }, - "service": Object { - "agent": Object { - "name": "nodejs", - "version": "1.14.2", - }, - "language": Object { - "name": "javascript", - }, - "name": "opbeans-node", - "runtime": Object { - "name": "node", - "version": "8.12.0", - }, - "version": "1.0.0", - }, - "system": Object { - "architecture": "x64", - "hostname": "98195610c255", - "ip": "172.18.0.10", - "platform": "linux", - }, - "tags": Object { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo -bar -baz", - "this-is-a-very-long-tag-name-without-any-spaces": "test", - }, - "user": Object { - "email": "kimchy@elastic.co", - "id": "42", - "username": "kimchy", - }, - }, - "host": Object { - "name": "b359e3afece8", - }, - "processor": Object { - "event": "transaction", - "name": "transaction", - }, - "timestamp": Object { - "us": 1542574424978005, - }, - "trace": Object { - "id": "0d84126973411c19b470f2d9eea958d3", - }, - "transaction": Object { - "duration": Object { - "us": 7891, - }, - "id": "0f10668e4fb3adc7", - "name": "GET /api/types", - "result": "HTTP 2xx", - "sampled": true, - "span_count": Object { - "started": 2, - }, - "type": "request", - }, - }, - "transactionsPerMinute": 75789.47368421052, - }, - Object { - "averageResponseTime": 3504.5108924806746, - "impact": 2.3600993453143766, - "name": "GET *", - "p95": 11431.738095238095, - "sample": Object { - "@timestamp": "2018-11-18T20:53:42.493Z", - "agent": Object { - "hostname": "b359e3afece8", - "type": "apm-server", - "version": "7.0.0-alpha1", - }, - "context": Object { - "custom": Object { - "containerId": 6446, - }, - "process": Object { - "argv": Array [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js", - ], - "pid": 3756, - "ppid": 1, - "title": "node /app/server.js", - }, - "request": Object { - "headers": Object { - "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", - "accept-encoding": "gzip, deflate", - "connection": "keep-alive", - "host": "opbeans-node:3000", - "if-modified-since": "Mon, 12 Nov 2018 10:27:07 GMT", - "if-none-match": "W/\\"280-1670775e878\\"", - "upgrade-insecure-requests": "1", - "user-agent": "Chromeless 1.4.0", - }, - "http_version": "1.1", - "method": "GET", - "socket": Object { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7", - }, - "url": Object { - "full": "http://opbeans-node:3000/dashboard", - "hostname": "opbeans-node", - "pathname": "/dashboard", - "port": "3000", - "protocol": "http:", - "raw": "/dashboard", - }, - }, - "response": Object { - "headers": Object { - "accept-ranges": "bytes", - "cache-control": "public, max-age=0", - "connection": "keep-alive", - "date": "Sun, 18 Nov 2018 20:53:42 GMT", - "etag": "W/\\"280-1670775e878\\"", - "last-modified": "Mon, 12 Nov 2018 10:27:07 GMT", - "x-powered-by": "Express", - }, - "status_code": 304, - }, - "service": Object { - "agent": Object { - "name": "nodejs", - "version": "1.14.2", - }, - "language": Object { - "name": "javascript", - }, - "name": "opbeans-node", - "runtime": Object { - "name": "node", - "version": "8.12.0", - }, - "version": "1.0.0", - }, - "system": Object { - "architecture": "x64", - "hostname": "98195610c255", - "ip": "172.18.0.10", - "platform": "linux", - }, - "tags": Object { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo -bar -baz", - "this-is-a-very-long-tag-name-without-any-spaces": "test", - }, - "user": Object { - "email": "kimchy@elastic.co", - "id": "42", - "username": "kimchy", - }, - }, - "host": Object { - "name": "b359e3afece8", - }, - "processor": Object { - "event": "transaction", - "name": "transaction", - }, - "timestamp": Object { - "us": 1542574422493006, - }, - "trace": Object { - "id": "7efb6ade88cdea20cd96ca482681cde7", - }, - "transaction": Object { - "duration": Object { - "us": 1901, - }, - "id": "f5fc4621949b63fb", - "name": "GET *", - "result": "HTTP 3xx", - "sampled": true, - "span_count": Object { - "started": 0, - }, - "type": "request", - }, - }, - "transactionsPerMinute": 224684.21052631576, - }, - Object { - "averageResponseTime": 32387.73641304348, - "impact": 2.2558112380477584, - "name": "GET /log-error", - "p95": 40061.1, - "sample": Object { - "@timestamp": "2018-11-18T20:52:51.462Z", - "agent": Object { - "hostname": "b359e3afece8", - "type": "apm-server", - "version": "7.0.0-alpha1", - }, - "context": Object { - "custom": Object { - "containerId": 4877, - }, - "process": Object { - "argv": Array [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js", - ], - "pid": 3659, - "ppid": 1, - "title": "node /app/server.js", - }, - "request": Object { - "headers": Object { - "connection": "close", - "host": "opbeans-node:3000", - "user-agent": "workload/2.4.3", - }, - "http_version": "1.1", - "method": "GET", - "socket": Object { - "encrypted": false, - "remote_address": "::ffff:172.18.0.10", - }, - "url": Object { - "full": "http://opbeans-node:3000/log-error", - "hostname": "opbeans-node", - "pathname": "/log-error", - "port": "3000", - "protocol": "http:", - "raw": "/log-error", - }, - }, - "response": Object { - "headers": Object { - "connection": "close", - "content-length": "24", - "content-type": "text/html; charset=utf-8", - "date": "Sun, 18 Nov 2018 20:52:51 GMT", - "etag": "W/\\"18-MS3VbhH7auHMzO0fUuNF6v14N/M\\"", - "x-powered-by": "Express", - }, - "status_code": 500, - }, - "service": Object { - "agent": Object { - "name": "nodejs", - "version": "1.14.2", - }, - "language": Object { - "name": "javascript", - }, - "name": "opbeans-node", - "runtime": Object { - "name": "node", - "version": "8.12.0", - }, - "version": "1.0.0", - }, - "system": Object { - "architecture": "x64", - "hostname": "98195610c255", - "ip": "172.18.0.10", - "platform": "linux", - }, - "tags": Object { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo -bar -baz", - "this-is-a-very-long-tag-name-without-any-spaces": "test", - }, - "user": Object { - "email": "kimchy@elastic.co", - "id": "42", - "username": "kimchy", - }, - }, - "host": Object { - "name": "b359e3afece8", - }, - "processor": Object { - "event": "transaction", - "name": "transaction", - }, - "timestamp": Object { - "us": 1542574371462005, - }, - "trace": Object { - "id": "15366d65659b5fc8f67ff127391b3aff", - }, - "transaction": Object { - "duration": Object { - "us": 33367, - }, - "id": "ec9c465c5042ded8", - "name": "GET /log-error", - "result": "HTTP 5xx", - "sampled": true, - "span_count": Object { - "started": 0, - }, - "type": "request", - }, - }, - "transactionsPerMinute": 23242.105263157893, - }, - Object { - "averageResponseTime": 32900.72714285714, - "impact": 2.1791207411745854, - "name": "GET /log-message", - "p95": 40444, - "sample": Object { - "@timestamp": "2018-11-18T20:49:09.225Z", - "agent": Object { - "hostname": "b359e3afece8", - "type": "apm-server", - "version": "7.0.0-alpha1", - }, - "context": Object { - "custom": Object { - "containerId": 321, - }, - "process": Object { - "argv": Array [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js", - ], - "pid": 3142, - "ppid": 1, - "title": "node /app/server.js", - }, - "request": Object { - "headers": Object { - "connection": "close", - "host": "opbeans-node:3000", - "user-agent": "workload/2.4.3", - }, - "http_version": "1.1", - "method": "GET", - "socket": Object { - "encrypted": false, - "remote_address": "::ffff:172.18.0.10", - }, - "url": Object { - "full": "http://opbeans-node:3000/log-message", - "hostname": "opbeans-node", - "pathname": "/log-message", - "port": "3000", - "protocol": "http:", - "raw": "/log-message", - }, - }, - "response": Object { - "headers": Object { - "connection": "close", - "content-length": "24", - "content-type": "text/html; charset=utf-8", - "date": "Sun, 18 Nov 2018 20:49:09 GMT", - "etag": "W/\\"18-MS3VbhH7auHMzO0fUuNF6v14N/M\\"", - "x-powered-by": "Express", - }, - "status_code": 500, - }, - "service": Object { - "agent": Object { - "name": "nodejs", - "version": "1.14.2", - }, - "language": Object { - "name": "javascript", - }, - "name": "opbeans-node", - "runtime": Object { - "name": "node", - "version": "8.12.0", - }, - "version": "1.0.0", - }, - "system": Object { - "architecture": "x64", - "hostname": "98195610c255", - "ip": "172.18.0.10", - "platform": "linux", - }, - "tags": Object { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo -bar -baz", - "this-is-a-very-long-tag-name-without-any-spaces": "test", - }, - "user": Object { - "email": "kimchy@elastic.co", - "id": "42", - "username": "kimchy", - }, - }, - "host": Object { - "name": "b359e3afece8", - }, - "processor": Object { - "event": "transaction", - "name": "transaction", - }, - "timestamp": Object { - "us": 1542574149225004, - }, - "trace": Object { - "id": "ba18b741cdd3ac83eca89a5fede47577", - }, - "transaction": Object { - "duration": Object { - "us": 32381, - }, - "id": "b9a8f96d7554d09f", - "name": "GET /log-message", - "result": "HTTP 5xx", - "sampled": true, - "span_count": Object { - "started": 0, - }, - "type": "request", - }, - }, - "transactionsPerMinute": 22105.263157894737, - }, - Object { - "averageResponseTime": 10548.218597063622, - "impact": 1.8338763992340905, - "name": "GET /api/products/:id", - "p95": 28413.383333333328, - "sample": Object { - "@timestamp": "2018-11-18T20:52:57.963Z", - "agent": Object { - "hostname": "b359e3afece8", - "type": "apm-server", - "version": "7.0.0-alpha1", - }, - "context": Object { - "custom": Object { - "containerId": 7184, - }, - "process": Object { - "argv": Array [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js", - ], - "pid": 3686, - "ppid": 1, - "title": "node /app/server.js", - }, - "request": Object { - "headers": Object { - "connection": "close", - "host": "opbeans-node:3000", - "user-agent": "workload/2.4.3", - }, - "http_version": "1.1", - "method": "GET", - "socket": Object { - "encrypted": false, - "remote_address": "::ffff:172.18.0.10", - }, - "url": Object { - "full": "http://opbeans-node:3000/api/products/3", - "hostname": "opbeans-node", - "pathname": "/api/products/3", - "port": "3000", - "protocol": "http:", - "raw": "/api/products/3", - }, - }, - "response": Object { - "headers": Object { - "connection": "close", - "content-length": "231", - "content-type": "application/json; charset=utf-8", - "date": "Sun, 18 Nov 2018 20:52:57 GMT", - "etag": "W/\\"e7-kkuzj37GZDzXDh0CWqh5Gan0VO4\\"", - "x-powered-by": "Express", - }, - "status_code": 200, - }, - "service": Object { - "agent": Object { - "name": "nodejs", - "version": "1.14.2", - }, - "language": Object { - "name": "javascript", - }, - "name": "opbeans-node", - "runtime": Object { - "name": "node", - "version": "8.12.0", - }, - "version": "1.0.0", - }, - "system": Object { - "architecture": "x64", - "hostname": "98195610c255", - "ip": "172.18.0.10", - "platform": "linux", - }, - "tags": Object { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo -bar -baz", - "this-is-a-very-long-tag-name-without-any-spaces": "test", - }, - "user": Object { - "email": "kimchy@elastic.co", - "id": "42", - "username": "kimchy", - }, - }, - "host": Object { - "name": "b359e3afece8", - }, - "processor": Object { - "event": "transaction", - "name": "transaction", - }, - "timestamp": Object { - "us": 1542574377963005, - }, - "trace": Object { - "id": "ca86ec845e412e4b4506a715d51548ec", - }, - "transaction": Object { - "duration": Object { - "us": 6959, - }, - "id": "d324897ffb7ebcdc", - "name": "GET /api/products/:id", - "result": "HTTP 2xx", - "sampled": true, - "span_count": Object { - "started": 1, - }, - "type": "request", - }, - }, - "transactionsPerMinute": 58073.68421052631, - }, - Object { - "averageResponseTime": 9868.217894736843, - "impact": 1.7722323960215767, - "name": "GET /api/customers/:id", - "p95": 27486.5, - "sample": Object { - "@timestamp": "2018-11-18T20:52:56.797Z", - "agent": Object { - "hostname": "b359e3afece8", - "type": "apm-server", - "version": "7.0.0-alpha1", - }, - "context": Object { - "custom": Object { - "containerId": 8225, - }, - "process": Object { - "argv": Array [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js", - ], - "pid": 3686, - "ppid": 1, - "title": "node /app/server.js", - }, - "request": Object { - "headers": Object { - "accept": "*/*", - "accept-encoding": "gzip, deflate", - "connection": "keep-alive", - "elastic-apm-traceparent": "00-e6140d30363f18b585f5d3b753f4d025-aa82e2c847265626-01", - "host": "opbeans-node:3000", - "user-agent": "python-requests/2.20.0", - }, - "http_version": "1.1", - "method": "GET", - "socket": Object { - "encrypted": false, - "remote_address": "::ffff:172.18.0.6", - }, - "url": Object { - "full": "http://opbeans-node:3000/api/customers/700", - "hostname": "opbeans-node", - "pathname": "/api/customers/700", - "port": "3000", - "protocol": "http:", - "raw": "/api/customers/700", - }, - }, - "response": Object { - "headers": Object { - "connection": "keep-alive", - "content-length": "193", - "content-type": "application/json; charset=utf-8", - "date": "Sun, 18 Nov 2018 20:52:56 GMT", - "etag": "W/\\"c1-LbuhkuLzFyZ0H+7+JQGA5b0kvNs\\"", - "x-powered-by": "Express", - }, - "status_code": 200, - }, - "service": Object { - "agent": Object { - "name": "nodejs", - "version": "1.14.2", - }, - "language": Object { - "name": "javascript", - }, - "name": "opbeans-node", - "runtime": Object { - "name": "node", - "version": "8.12.0", - }, - "version": "1.0.0", - }, - "system": Object { - "architecture": "x64", - "hostname": "98195610c255", - "ip": "172.18.0.10", - "platform": "linux", - }, - "tags": Object { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo -bar -baz", - "this-is-a-very-long-tag-name-without-any-spaces": "test", - }, - "user": Object { - "email": "kimchy@elastic.co", - "id": "42", - "username": "kimchy", - }, - }, - "host": Object { - "name": "b359e3afece8", - }, - "parent": Object { - "id": "aa82e2c847265626", - }, - "processor": Object { - "event": "transaction", - "name": "transaction", - }, - "timestamp": Object { - "us": 1542574376797031, - }, - "trace": Object { - "id": "e6140d30363f18b585f5d3b753f4d025", - }, - "transaction": Object { - "duration": Object { - "us": 9735, - }, - "id": "60e230d12f3f0960", - "name": "GET /api/customers/:id", - "result": "HTTP 2xx", - "sampled": true, - "span_count": Object { - "started": 1, - }, - "type": "request", - }, - }, - "transactionsPerMinute": 59999.99999999999, - }, - Object { - "averageResponseTime": 12763.68806073154, - "impact": 1.7479924334286208, - "name": "GET /api/types/:id", - "p95": 30576.749999999996, - "sample": Object { - "@timestamp": "2018-11-18T20:53:35.967Z", - "agent": Object { - "hostname": "b359e3afece8", - "type": "apm-server", - "version": "7.0.0-alpha1", - }, - "context": Object { - "custom": Object { - "containerId": 5345, - }, - "process": Object { - "argv": Array [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js", - ], - "pid": 3756, - "ppid": 1, - "title": "node /app/server.js", - }, - "request": Object { - "headers": Object { - "connection": "close", - "host": "opbeans-node:3000", - "user-agent": "workload/2.4.3", - }, - "http_version": "1.1", - "method": "GET", - "socket": Object { - "encrypted": false, - "remote_address": "::ffff:172.18.0.10", - }, - "url": Object { - "full": "http://opbeans-node:3000/api/types/1", - "hostname": "opbeans-node", - "pathname": "/api/types/1", - "port": "3000", - "protocol": "http:", - "raw": "/api/types/1", - }, - }, - "response": Object { - "headers": Object { - "connection": "close", - "content-length": "217", - "content-type": "application/json; charset=utf-8", - "date": "Sun, 18 Nov 2018 20:53:35 GMT", - "etag": "W/\\"d9-cebOOHODBQMZd1wt+ZZBaSPgQLQ\\"", - "x-powered-by": "Express", - }, - "status_code": 200, - }, - "service": Object { - "agent": Object { - "name": "nodejs", - "version": "1.14.2", - }, - "language": Object { - "name": "javascript", - }, - "name": "opbeans-node", - "runtime": Object { - "name": "node", - "version": "8.12.0", - }, - "version": "1.0.0", - }, - "system": Object { - "architecture": "x64", - "hostname": "98195610c255", - "ip": "172.18.0.10", - "platform": "linux", - }, - "tags": Object { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo -bar -baz", - "this-is-a-very-long-tag-name-without-any-spaces": "test", - }, - "user": Object { - "email": "kimchy@elastic.co", - "id": "42", - "username": "kimchy", - }, - }, - "host": Object { - "name": "b359e3afece8", - }, - "processor": Object { - "event": "transaction", - "name": "transaction", - }, - "timestamp": Object { - "us": 1542574415967005, - }, - "trace": Object { - "id": "2223b30b5cbaf2e221fcf70ac6d9abbe", - }, - "transaction": Object { - "duration": Object { - "us": 13064, - }, - "id": "053436abacdec0a4", - "name": "GET /api/types/:id", - "result": "HTTP 2xx", - "sampled": true, - "span_count": Object { - "started": 2, - }, - "type": "request", - }, - }, - "transactionsPerMinute": 45757.8947368421, - }, - Object { - "averageResponseTime": 10584.05144193297, - "impact": 1.280810614916383, - "name": "GET /api/orders/:id", - "p95": 26555.399999999998, - "sample": Object { - "@timestamp": "2018-11-18T20:51:36.949Z", - "agent": Object { - "hostname": "b359e3afece8", - "type": "apm-server", - "version": "7.0.0-alpha1", - }, - "context": Object { - "custom": Object { - "containerId": 5999, - }, - "process": Object { - "argv": Array [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js", - ], - "pid": 3475, - "ppid": 1, - "title": "node /app/server.js", - }, - "request": Object { - "headers": Object { - "connection": "close", - "host": "opbeans-node:3000", - "user-agent": "workload/2.4.3", - }, - "http_version": "1.1", - "method": "GET", - "socket": Object { - "encrypted": false, - "remote_address": "::ffff:172.18.0.10", - }, - "url": Object { - "full": "http://opbeans-node:3000/api/orders/183", - "hostname": "opbeans-node", - "pathname": "/api/orders/183", - "port": "3000", - "protocol": "http:", - "raw": "/api/orders/183", - }, - }, - "response": Object { - "headers": Object { - "connection": "close", - "content-length": "0", - "date": "Sun, 18 Nov 2018 20:51:36 GMT", - "x-powered-by": "Express", - }, - "status_code": 404, - }, - "service": Object { - "agent": Object { - "name": "nodejs", - "version": "1.14.2", - }, - "language": Object { - "name": "javascript", - }, - "name": "opbeans-node", - "runtime": Object { - "name": "node", - "version": "8.12.0", - }, - "version": "1.0.0", - }, - "system": Object { - "architecture": "x64", - "hostname": "98195610c255", - "ip": "172.18.0.10", - "platform": "linux", - }, - "tags": Object { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo -bar -baz", - "this-is-a-very-long-tag-name-without-any-spaces": "test", - }, - "user": Object { - "email": "kimchy@elastic.co", - "id": "42", - "username": "kimchy", - }, - }, - "host": Object { - "name": "b359e3afece8", - }, - "processor": Object { - "event": "transaction", - "name": "transaction", - }, - "timestamp": Object { - "us": 1542574296949004, - }, - "trace": Object { - "id": "dab6421fa44a6869887e0edf32e1ad6f", - }, - "transaction": Object { - "duration": Object { - "us": 5906, - }, - "id": "937ef5588454f74a", - "name": "GET /api/orders/:id", - "result": "HTTP 4xx", - "sampled": true, - "span_count": Object { - "started": 1, - }, - "type": "request", - }, - }, - "transactionsPerMinute": 40515.789473684206, - }, - Object { - "averageResponseTime": 1422.926672899693, - "impact": 1.0027124806135428, - "name": "GET unknown route", - "p95": 2311.885238095238, - "sample": Object { - "@timestamp": "2018-11-18T20:53:42.504Z", - "agent": Object { - "hostname": "b359e3afece8", - "type": "apm-server", - "version": "7.0.0-alpha1", - }, - "context": Object { - "process": Object { - "argv": Array [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js", - ], - "pid": 3756, - "ppid": 1, - "title": "node /app/server.js", - }, - "request": Object { - "headers": Object { - "accept": "*/*", - "accept-encoding": "gzip, deflate", - "connection": "keep-alive", - "host": "opbeans-node:3000", - "referer": "http://opbeans-node:3000/dashboard", - "user-agent": "Chromeless 1.4.0", - }, - "http_version": "1.1", - "method": "GET", - "socket": Object { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7", - }, - "url": Object { - "full": "http://opbeans-node:3000/rum-config.js", - "hostname": "opbeans-node", - "pathname": "/rum-config.js", - "port": "3000", - "protocol": "http:", - "raw": "/rum-config.js", - }, - }, - "response": Object { - "headers": Object { - "connection": "keep-alive", - "content-length": "172", - "content-type": "text/javascript", - "date": "Sun, 18 Nov 2018 20:53:42 GMT", - "x-powered-by": "Express", - }, - "status_code": 200, - }, - "service": Object { - "agent": Object { - "name": "nodejs", - "version": "1.14.2", - }, - "language": Object { - "name": "javascript", - }, - "name": "opbeans-node", - "runtime": Object { - "name": "node", - "version": "8.12.0", - }, - "version": "1.0.0", - }, - "system": Object { - "architecture": "x64", - "hostname": "98195610c255", - "ip": "172.18.0.10", - "platform": "linux", - }, - }, - "host": Object { - "name": "b359e3afece8", - }, - "processor": Object { - "event": "transaction", - "name": "transaction", - }, - "timestamp": Object { - "us": 1542574422504004, - }, - "trace": Object { - "id": "4399e7233e6e7b77e70c2fff111b8f28", - }, - "transaction": Object { - "duration": Object { - "us": 911, - }, - "id": "107881ae2be1b56d", - "name": "GET unknown route", - "result": "HTTP 2xx", - "sampled": true, - "span_count": Object { - "started": 0, - }, - "type": "request", - }, - }, - "transactionsPerMinute": 236431.5789473684, - }, - Object { - "averageResponseTime": 21331.714285714286, - "impact": 0.28817487960409877, - "name": "POST /api", - "p95": 30938, - "sample": Object { - "@timestamp": "2018-11-18T20:29:42.751Z", - "agent": Object { - "hostname": "b359e3afece8", - "type": "apm-server", - "version": "7.0.0-alpha1", - }, - "context": Object { - "custom": Object { - "containerId": 2927, - }, - "process": Object { - "argv": Array [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js", - ], - "pid": 546, - "ppid": 1, - "title": "node /app/server.js", - }, - "request": Object { - "body": "[REDACTED]", - "headers": Object { - "accept": "application/json", - "connection": "close", - "content-length": "129", - "content-type": "application/json", - "host": "opbeans-node:3000", - "user-agent": "workload/2.4.3", - }, - "http_version": "1.1", - "method": "POST", - "socket": Object { - "encrypted": false, - "remote_address": "::ffff:172.18.0.10", - }, - "url": Object { - "full": "http://opbeans-node:3000/api/orders", - "hostname": "opbeans-node", - "pathname": "/api/orders", - "port": "3000", - "protocol": "http:", - "raw": "/api/orders", - }, - }, - "response": Object { - "headers": Object { - "connection": "close", - "content-length": "0", - "date": "Sun, 18 Nov 2018 20:29:42 GMT", - "x-powered-by": "Express", - }, - "status_code": 400, - }, - "service": Object { - "agent": Object { - "name": "nodejs", - "version": "1.14.2", - }, - "language": Object { - "name": "javascript", - }, - "name": "opbeans-node", - "runtime": Object { - "name": "node", - "version": "8.12.0", - }, - "version": "1.0.0", - }, - "system": Object { - "architecture": "x64", - "hostname": "98195610c255", - "ip": "172.18.0.10", - "platform": "linux", - }, - "tags": Object { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo -bar -baz", - "this-is-a-very-long-tag-name-without-any-spaces": "test", - }, - "user": Object { - "email": "kimchy@elastic.co", - "id": "42", - "username": "kimchy", - }, - }, - "host": Object { - "name": "b359e3afece8", - }, - "processor": Object { - "event": "transaction", - "name": "transaction", - }, - "timestamp": Object { - "us": 1542572982751005, - }, - "trace": Object { - "id": "8ed4d94ec8fc11b1ea1b0aa59c2320ff", - }, - "transaction": Object { - "duration": Object { - "us": 21083, - }, - "id": "d67c2f7aa897110c", - "name": "POST /api", - "result": "HTTP 4xx", - "sampled": true, - "span_count": Object { - "started": 1, - }, - "type": "request", - }, - }, - "transactionsPerMinute": 4642.105263157894, - }, - Object { - "averageResponseTime": 4694.005586592179, - "impact": 0.1498515000753004, - "name": "GET /is-it-coffee-time", - "p95": 11022.99999999992, - "sample": Object { - "@timestamp": "2018-11-18T20:46:19.317Z", - "agent": Object { - "hostname": "b359e3afece8", - "type": "apm-server", - "version": "7.0.0-alpha1", - }, - "context": Object { - "custom": Object { - "containerId": 8593, - }, - "process": Object { - "argv": Array [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js", - ], - "pid": 2760, - "ppid": 1, - "title": "node /app/server.js", - }, - "request": Object { - "headers": Object { - "connection": "close", - "host": "opbeans-node:3000", - "user-agent": "workload/2.4.3", - }, - "http_version": "1.1", - "method": "GET", - "socket": Object { - "encrypted": false, - "remote_address": "::ffff:172.18.0.10", - }, - "url": Object { - "full": "http://opbeans-node:3000/is-it-coffee-time", - "hostname": "opbeans-node", - "pathname": "/is-it-coffee-time", - "port": "3000", - "protocol": "http:", - "raw": "/is-it-coffee-time", - }, - }, - "response": Object { - "headers": Object { - "connection": "close", - "content-length": "148", - "content-security-policy": "default-src 'self'", - "content-type": "text/html; charset=utf-8", - "date": "Sun, 18 Nov 2018 20:46:19 GMT", - "x-content-type-options": "nosniff", - "x-powered-by": "Express", - }, - "status_code": 500, - }, - "service": Object { - "agent": Object { - "name": "nodejs", - "version": "1.14.2", - }, - "language": Object { - "name": "javascript", - }, - "name": "opbeans-node", - "runtime": Object { - "name": "node", - "version": "8.12.0", - }, - "version": "1.0.0", - }, - "system": Object { - "architecture": "x64", - "hostname": "98195610c255", - "ip": "172.18.0.10", - "platform": "linux", - }, - "tags": Object { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo -bar -baz", - "this-is-a-very-long-tag-name-without-any-spaces": "test", - }, - "user": Object { - "email": "kimchy@elastic.co", - "id": "42", - "username": "kimchy", - }, - }, - "host": Object { - "name": "b359e3afece8", - }, - "processor": Object { - "event": "transaction", - "name": "transaction", - }, - "timestamp": Object { - "us": 1542573979317007, - }, - "trace": Object { - "id": "821812b416de4c73ced87f8777fa46a6", - }, - "transaction": Object { - "duration": Object { - "us": 4253, - }, - "id": "319a5c555a1ab207", - "name": "GET /is-it-coffee-time", - "result": "HTTP 5xx", - "sampled": true, - "span_count": Object { - "started": 0, - }, - "type": "request", - }, - }, - "transactionsPerMinute": 11305.263157894737, - }, - Object { - "averageResponseTime": 4549.889880952381, - "impact": 0.13543365054509587, - "name": "GET /throw-error", - "p95": 7719.700000000001, - "sample": Object { - "@timestamp": "2018-11-18T20:47:10.714Z", - "agent": Object { - "hostname": "b359e3afece8", - "type": "apm-server", - "version": "7.0.0-alpha1", - }, - "context": Object { - "custom": Object { - "containerId": 7220, - }, - "process": Object { - "argv": Array [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js", - ], - "pid": 2895, - "ppid": 1, - "title": "node /app/server.js", - }, - "request": Object { - "headers": Object { - "connection": "close", - "host": "opbeans-node:3000", - "user-agent": "workload/2.4.3", - }, - "http_version": "1.1", - "method": "GET", - "socket": Object { - "encrypted": false, - "remote_address": "::ffff:172.18.0.10", - }, - "url": Object { - "full": "http://opbeans-node:3000/throw-error", - "hostname": "opbeans-node", - "pathname": "/throw-error", - "port": "3000", - "protocol": "http:", - "raw": "/throw-error", - }, - }, - "response": Object { - "headers": Object { - "connection": "close", - "content-length": "148", - "content-security-policy": "default-src 'self'", - "content-type": "text/html; charset=utf-8", - "date": "Sun, 18 Nov 2018 20:47:10 GMT", - "x-content-type-options": "nosniff", - "x-powered-by": "Express", - }, - "status_code": 500, - }, - "service": Object { - "agent": Object { - "name": "nodejs", - "version": "1.14.2", - }, - "language": Object { - "name": "javascript", - }, - "name": "opbeans-node", - "runtime": Object { - "name": "node", - "version": "8.12.0", - }, - "version": "1.0.0", - }, - "system": Object { - "architecture": "x64", - "hostname": "98195610c255", - "ip": "172.18.0.10", - "platform": "linux", - }, - "tags": Object { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo -bar -baz", - "this-is-a-very-long-tag-name-without-any-spaces": "test", - }, - "user": Object { - "email": "kimchy@elastic.co", - "id": "42", - "username": "kimchy", - }, - }, - "host": Object { - "name": "b359e3afece8", - }, - "processor": Object { - "event": "transaction", - "name": "transaction", - }, - "timestamp": Object { - "us": 1542574030714012, - }, - "trace": Object { - "id": "6c0ef23e1f963f304ce440a909914d35", - }, - "transaction": Object { - "duration": Object { - "us": 4458, - }, - "id": "ecd187dc53f09fbd", - "name": "GET /throw-error", - "result": "HTTP 5xx", - "sampled": true, - "span_count": Object { - "started": 0, - }, - "type": "request", - }, - }, - "transactionsPerMinute": 10610.526315789473, - }, - Object { - "averageResponseTime": 2742.4615384615386, - "impact": 0.08501028923348058, - "name": "OPTIONS unknown route", - "p95": 4370.000000000002, - "sample": Object { - "@timestamp": "2018-11-18T20:49:00.707Z", - "agent": Object { - "hostname": "b359e3afece8", - "type": "apm-server", - "version": "7.0.0-alpha1", - }, - "context": Object { - "custom": Object { - "containerId": 3775, - }, - "process": Object { - "argv": Array [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js", - ], - "pid": 3142, - "ppid": 1, - "title": "node /app/server.js", - }, - "request": Object { - "headers": Object { - "connection": "close", - "content-length": "0", - "host": "opbeans-node:3000", - "user-agent": "workload/2.4.3", - }, - "http_version": "1.1", - "method": "OPTIONS", - "socket": Object { - "encrypted": false, - "remote_address": "::ffff:172.18.0.10", - }, - "url": Object { - "full": "http://opbeans-node:3000/", - "hostname": "opbeans-node", - "pathname": "/", - "port": "3000", - "protocol": "http:", - "raw": "/", - }, - }, - "response": Object { - "headers": Object { - "allow": "GET,HEAD", - "connection": "close", - "content-length": "8", - "content-type": "text/html; charset=utf-8", - "date": "Sun, 18 Nov 2018 20:49:00 GMT", - "etag": "W/\\"8-ZRAf8oNBS3Bjb/SU2GYZCmbtmXg\\"", - "x-powered-by": "Express", - }, - "status_code": 200, - }, - "service": Object { - "agent": Object { - "name": "nodejs", - "version": "1.14.2", - }, - "language": Object { - "name": "javascript", - }, - "name": "opbeans-node", - "runtime": Object { - "name": "node", - "version": "8.12.0", - }, - "version": "1.0.0", - }, - "system": Object { - "architecture": "x64", - "hostname": "98195610c255", - "ip": "172.18.0.10", - "platform": "linux", - }, - "tags": Object { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo -bar -baz", - "this-is-a-very-long-tag-name-without-any-spaces": "test", - }, - "user": Object { - "email": "kimchy@elastic.co", - "id": "42", - "username": "kimchy", - }, - }, - "host": Object { - "name": "b359e3afece8", - }, - "processor": Object { - "event": "transaction", - "name": "transaction", - }, - "timestamp": Object { - "us": 1542574140707006, - }, - "trace": Object { - "id": "469e3e5f91ffe3195a8e58cdd1cdefa8", - }, - "transaction": Object { - "duration": Object { - "us": 2371, - }, - "id": "a8c87ebc7ec68bc0", - "name": "OPTIONS unknown route", - "result": "HTTP 2xx", - "sampled": true, - "span_count": Object { - "started": 0, - }, - "type": "request", - }, - }, - "transactionsPerMinute": 11494.736842105262, - }, - Object { - "averageResponseTime": 5192.9, - "impact": 0, - "name": "POST unknown route", - "p95": 13230.5, - "sample": Object { - "@timestamp": "2018-11-18T18:43:50.994Z", - "agent": Object { - "hostname": "b359e3afece8", - "type": "apm-server", - "version": "7.0.0-alpha1", - }, - "context": Object { - "custom": Object { - "containerId": 6102, - }, - "process": Object { - "argv": Array [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainerFork.js", - ], - "pid": 19196, - "ppid": 1, - "title": "node /app/server.js", - }, - "request": Object { - "body": "[REDACTED]", - "headers": Object { - "accept": "*/*", - "accept-encoding": "gzip, deflate", - "content-length": "380", - "content-type": "multipart/form-data; boundary=2b2e40be188a4cb5a56c05a0c182f6c9", - "elastic-apm-traceparent": "00-19688959ea6cbccda8013c11566ea329-1fc3665eef2dcdfc-01", - "host": "172.18.0.9:3000", - "user-agent": "Python/3.7 aiohttp/3.3.2", - "x-forwarded-for": "172.18.0.11", - }, - "http_version": "1.1", - "method": "POST", - "socket": Object { - "encrypted": false, - "remote_address": "::ffff:172.18.0.9", - }, - "url": Object { - "full": "http://172.18.0.9:3000/api/orders/csv", - "hostname": "172.18.0.9", - "pathname": "/api/orders/csv", - "port": "3000", - "protocol": "http:", - "raw": "/api/orders/csv", - }, - }, - "response": Object { - "headers": Object { - "connection": "keep-alive", - "content-length": "154", - "content-security-policy": "default-src 'self'", - "content-type": "text/html; charset=utf-8", - "date": "Sun, 18 Nov 2018 18:43:50 GMT", - "x-content-type-options": "nosniff", - "x-powered-by": "Express", - }, - "status_code": 404, - }, - "service": Object { - "agent": Object { - "name": "nodejs", - "version": "1.14.2", - }, - "language": Object { - "name": "javascript", - }, - "name": "opbeans-node", - "runtime": Object { - "name": "node", - "version": "8.12.0", - }, - "version": "1.0.0", - }, - "system": Object { - "architecture": "x64", - "hostname": "98195610c255", - "ip": "172.18.0.10", - "platform": "linux", - }, - "tags": Object { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo -bar -baz", - "this-is-a-very-long-tag-name-without-any-spaces": "test", - }, - "user": Object { - "email": "kimchy@elastic.co", - "id": "42", - "username": "kimchy", - }, - }, - "host": Object { - "name": "b359e3afece8", - }, - "parent": Object { - "id": "1fc3665eef2dcdfc", - }, - "processor": Object { - "event": "transaction", - "name": "transaction", - }, - "timestamp": Object { - "us": 1542566630994005, - }, - "trace": Object { - "id": "19688959ea6cbccda8013c11566ea329", - }, - "transaction": Object { - "duration": Object { - "us": 3467, - }, - "id": "92c3ceea57899061", - "name": "POST unknown route", - "result": "HTTP 4xx", - "sampled": true, - "span_count": Object { - "started": 0, - }, - "type": "request", - }, - }, - "transactionsPerMinute": 631.578947368421, - }, -] -`; diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.test.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.test.ts deleted file mode 100644 index a26c3d85a3fc4..0000000000000 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { transactionGroupsFetcher } from './fetcher'; -import { APMConfig } from '../..'; - -function getSetup() { - return { - start: 1528113600000, - end: 1528977600000, - client: { - search: jest.fn(), - } as any, - internalClient: { - search: jest.fn(), - } as any, - config: { - 'xpack.apm.ui.transactionGroupBucketSize': 100, - } as APMConfig, - uiFiltersES: [{ term: { 'service.environment': 'test' } }], - indices: { - 'apm_oss.sourcemapIndices': 'myIndex', - 'apm_oss.errorIndices': 'myIndex', - 'apm_oss.onboardingIndices': 'myIndex', - 'apm_oss.spanIndices': 'myIndex', - 'apm_oss.transactionIndices': 'myIndex', - 'apm_oss.metricsIndices': 'myIndex', - apmAgentConfigurationIndex: 'myIndex', - apmCustomLinkIndex: 'myIndex', - }, - dynamicIndexPattern: null as any, - }; -} - -describe('transactionGroupsFetcher', () => { - describe('type: top_traces', () => { - it('should call client.search with correct query', async () => { - const setup = getSetup(); - const bucketSize = 100; - await transactionGroupsFetcher({ type: 'top_traces' }, setup, bucketSize); - expect(setup.client.search.mock.calls).toMatchSnapshot(); - }); - }); - - describe('type: top_transactions', () => { - it('should call client.search with correct query', async () => { - const setup = getSetup(); - const bucketSize = 100; - await transactionGroupsFetcher( - { - type: 'top_transactions', - serviceName: 'opbeans-node', - transactionType: 'request', - }, - setup, - bucketSize - ); - expect(setup.client.search.mock.calls).toMatchSnapshot(); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index a5cc74b18a7ef..73bf1d01924e7 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -3,23 +3,31 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { take, sortBy } from 'lodash'; +import { Unionize } from 'utility-types'; +import moment from 'moment'; +import { joinByKey } from '../../../common/utils/join_by_key'; +import { ESSearchRequest } from '../../../typings/elasticsearch'; import { SERVICE_NAME, - TRANSACTION_DURATION, - TRANSACTION_SAMPLED, TRANSACTION_NAME, } from '../../../common/elasticsearch_fieldnames'; import { getTransactionGroupsProjection } from '../../../common/projections/transaction_groups'; import { mergeProjection } from '../../../common/projections/util/merge_projection'; import { PromiseReturnType } from '../../../../observability/typings/common'; -import { SortOptions } from '../../../typings/elasticsearch/aggregations'; +import { AggregationOptionsByType } from '../../../typings/elasticsearch/aggregations'; import { Transaction } from '../../../typings/es_schemas/ui/transaction'; import { Setup, SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; +import { + getSamples, + getAverages, + getSums, + getPercentiles, +} from './get_transaction_group_stats'; interface TopTransactionOptions { type: 'top_transactions'; @@ -36,68 +44,149 @@ interface TopTraceOptions { export type Options = TopTransactionOptions | TopTraceOptions; export type ESResponse = PromiseReturnType; + +export type TransactionGroupRequestBase = ESSearchRequest & { + body: { + aggs: { + transaction_groups: Unionize< + Pick + >; + }; + }; +}; + +export type TransactionGroupSetup = Setup & SetupTimeRange & SetupUIFilters; + +function getItemsWithRelativeImpact( + setup: TransactionGroupSetup, + items: Array<{ + sum?: number | null; + key: string | Record; + avg?: number | null; + count?: number | null; + p95?: number; + sample?: Transaction; + }> +) { + const values = items + .map(({ sum }) => sum) + .filter((value) => value !== null) as number[]; + + const max = Math.max(...values); + const min = Math.min(...values); + + const duration = moment.duration(setup.end - setup.start); + const minutes = duration.asMinutes(); + + const itemsWithRelativeImpact: TransactionGroup[] = items + .map((item) => { + return { + key: item.key, + averageResponseTime: item.avg, + transactionsPerMinute: (item.count ?? 0) / minutes, + impact: + item.sum !== null && item.sum !== undefined + ? ((item.sum - min) / (max - min)) * 100 || 0 + : 0, + p95: item.p95, + sample: item.sample!, + }; + }) + .filter((item) => item.sample); + + return itemsWithRelativeImpact; +} + export async function transactionGroupsFetcher( options: Options, - setup: Setup & SetupTimeRange & SetupUIFilters, + setup: TransactionGroupSetup, bucketSize: number ) { - const { client } = setup; - const projection = getTransactionGroupsProjection({ setup, options, }); - const sort: SortOptions = [ - { _score: 'desc' as const }, // sort by _score to ensure that buckets with sampled:true ends up on top - { '@timestamp': { order: 'desc' as const } }, - ]; - const isTopTraces = options.type === 'top_traces'; - if (isTopTraces) { - // Delete the projection aggregation when searching for traces, as it should use the combined aggregation instead - delete projection.body.aggs; - } + delete projection.body.aggs; + + // traces overview is hardcoded to 10000 + // transactions overview: 1 extra bucket is added to check whether the total number of buckets exceed the specified bucket size. + const expectedBucketSize = isTopTraces ? 10000 : bucketSize; + const size = isTopTraces ? 10000 : expectedBucketSize + 1; - const params = mergeProjection(projection, { + const request = mergeProjection(projection, { + size: 0, body: { - size: 0, - query: { - bool: { - // prefer sampled transactions - should: [{ term: { [TRANSACTION_SAMPLED]: true } }], - }, - }, aggs: { transaction_groups: { - composite: { - // traces overview is hardcoded to 10000 - // transactions overview: 1 extra bucket is added to check whether the total number of buckets exceed the specified bucket size. - size: isTopTraces ? 10000 : bucketSize + 1, - sources: [ - ...(isTopTraces - ? [{ service: { terms: { field: SERVICE_NAME } } }] - : []), - { transaction: { terms: { field: TRANSACTION_NAME } } }, - ], - }, - aggs: { - sample: { top_hits: { size: 1, sort } }, - avg: { avg: { field: TRANSACTION_DURATION } }, - p95: { - percentiles: { - field: TRANSACTION_DURATION, - percents: [95], - hdr: { number_of_significant_value_digits: 2 }, - }, - }, - sum: { sum: { field: TRANSACTION_DURATION } }, - }, + ...(isTopTraces + ? { + composite: { + sources: [ + { [SERVICE_NAME]: { terms: { field: SERVICE_NAME } } }, + { + [TRANSACTION_NAME]: { + terms: { field: TRANSACTION_NAME }, + }, + }, + ], + size, + }, + } + : { + terms: { + field: TRANSACTION_NAME, + size, + }, + }), }, }, }, }); - return client.search(params); + const params = { + request, + setup, + }; + + const [samples, averages, sums, percentiles] = await Promise.all([ + getSamples(params), + getAverages(params), + getSums(params), + !isTopTraces ? getPercentiles(params) : Promise.resolve(undefined), + ]); + + const stats = [ + ...samples, + ...averages, + ...sums, + ...(percentiles ? percentiles : []), + ]; + + const items = joinByKey(stats, 'key'); + + const itemsWithRelativeImpact = getItemsWithRelativeImpact(setup, items); + + return { + items: take( + // sort by impact by default so most impactful services are not cut off + sortBy(itemsWithRelativeImpact, 'impact').reverse(), + expectedBucketSize + ), + // The aggregation is considered accurate if the configured bucket size is larger or equal to the number of buckets returned + // the actual number of buckets retrieved are `bucketsize + 1` to detect whether it's above the limit + isAggregationAccurate: expectedBucketSize >= itemsWithRelativeImpact.length, + bucketSize, + }; +} + +export interface TransactionGroup { + key: Record | string; + averageResponseTime: number | null | undefined; + transactionsPerMinute: number; + p95: number | undefined; + impact: number; + sample: Transaction; } diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts new file mode 100644 index 0000000000000..59fb370113ec2 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts @@ -0,0 +1,144 @@ +/* + * 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 { merge } from 'lodash'; +import { arrayUnionToCallable } from '../../../common/utils/array_union_to_callable'; +import { Transaction } from '../../../typings/es_schemas/ui/transaction'; +import { + TRANSACTION_SAMPLED, + TRANSACTION_DURATION, +} from '../../../common/elasticsearch_fieldnames'; +import { + AggregationInputMap, + SortOptions, +} from '../../../typings/elasticsearch/aggregations'; +import { TransactionGroupRequestBase, TransactionGroupSetup } from './fetcher'; + +interface MetricParams { + request: TransactionGroupRequestBase; + setup: TransactionGroupSetup; +} + +type BucketKey = string | Record; + +function mergeRequestWithAggs< + TRequestBase extends TransactionGroupRequestBase, + TInputMap extends AggregationInputMap +>(request: TRequestBase, aggs: TInputMap) { + return merge({}, request, { + body: { + aggs: { + transaction_groups: { + aggs, + }, + }, + }, + }); +} + +export async function getSamples({ request, setup }: MetricParams) { + const params = mergeRequestWithAggs(request, { + sample: { + top_hits: { + size: 1, + }, + }, + }); + + const sort: SortOptions = [ + { _score: 'desc' as const }, // sort by _score to ensure that buckets with sampled:true ends up on top + { '@timestamp': { order: 'desc' as const } }, + ]; + + const response = await setup.client.search({ + ...params, + body: { + ...params.body, + query: { + ...params.body.query, + bool: { + ...params.body.query.bool, + should: [{ term: { [TRANSACTION_SAMPLED]: true } }], + }, + }, + sort, + }, + }); + + return arrayUnionToCallable( + response.aggregations?.transaction_groups.buckets ?? [] + ).map((bucket) => { + return { + key: bucket.key as BucketKey, + count: bucket.doc_count, + sample: bucket.sample.hits.hits[0]._source as Transaction, + }; + }); +} + +export async function getAverages({ request, setup }: MetricParams) { + const params = mergeRequestWithAggs(request, { + avg: { + avg: { + field: TRANSACTION_DURATION, + }, + }, + }); + + const response = await setup.client.search(params); + + return arrayUnionToCallable( + response.aggregations?.transaction_groups.buckets ?? [] + ).map((bucket) => { + return { + key: bucket.key as BucketKey, + avg: bucket.avg.value, + }; + }); +} + +export async function getSums({ request, setup }: MetricParams) { + const params = mergeRequestWithAggs(request, { + sum: { + sum: { + field: TRANSACTION_DURATION, + }, + }, + }); + + const response = await setup.client.search(params); + + return arrayUnionToCallable( + response.aggregations?.transaction_groups.buckets ?? [] + ).map((bucket) => { + return { + key: bucket.key as BucketKey, + sum: bucket.sum.value, + }; + }); +} + +export async function getPercentiles({ request, setup }: MetricParams) { + const params = mergeRequestWithAggs(request, { + p95: { + percentiles: { + field: TRANSACTION_DURATION, + hdr: { number_of_significant_value_digits: 2 }, + percents: [95], + }, + }, + }); + + const response = await setup.client.search(params); + + return arrayUnionToCallable( + response.aggregations?.transaction_groups.buckets ?? [] + ).map((bucket) => { + return { + key: bucket.key as BucketKey, + p95: Object.values(bucket.p95.values)[0], + }; + }); +} diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/index.ts b/x-pack/plugins/apm/server/lib/transaction_groups/index.ts index 893e586b351a8..6e0d619268d44 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/index.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/index.ts @@ -10,19 +10,11 @@ import { SetupUIFilters, } from '../helpers/setup_request'; import { transactionGroupsFetcher, Options } from './fetcher'; -import { transactionGroupsTransformer } from './transform'; export async function getTransactionGroupList( options: Options, setup: Setup & SetupTimeRange & SetupUIFilters ) { - const { start, end } = setup; const bucketSize = setup.config['xpack.apm.ui.transactionGroupBucketSize']; - const response = await transactionGroupsFetcher(options, setup, bucketSize); - return transactionGroupsTransformer({ - response, - start, - end, - bucketSize, - }); + return await transactionGroupsFetcher(options, setup, bucketSize); } diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts b/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts index 2c5aa79bb3483..0b2ff3a72975b 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts @@ -31,7 +31,9 @@ describe('transaction group queries', () => { ) ); - expect(mock.params).toMatchSnapshot(); + const allParams = mock.spy.mock.calls.map((call) => call[0]); + + expect(allParams).toMatchSnapshot(); }); it('fetches top traces', async () => { @@ -46,6 +48,8 @@ describe('transaction group queries', () => { ) ); - expect(mock.params).toMatchSnapshot(); + const allParams = mock.spy.mock.calls.map((call) => call[0]); + + expect(allParams).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/transform.test.ts b/x-pack/plugins/apm/server/lib/transaction_groups/transform.test.ts deleted file mode 100644 index 0bb29e27f0219..0000000000000 --- a/x-pack/plugins/apm/server/lib/transaction_groups/transform.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ESResponse } from './fetcher'; -import { transactionGroupsResponse } from './mock_responses/transaction_groups_response'; -import { transactionGroupsTransformer } from './transform'; - -describe('transactionGroupsTransformer', () => { - it('should match snapshot', () => { - const { - bucketSize, - isAggregationAccurate, - items, - } = transactionGroupsTransformer({ - response: transactionGroupsResponse, - start: 100, - end: 2000, - bucketSize: 100, - }); - - expect(bucketSize).toBe(100); - expect(isAggregationAccurate).toBe(true); - expect(items).toMatchSnapshot(); - }); - - it('should transform response correctly', () => { - const bucket = { - key: { transaction: 'POST /api/orders' }, - doc_count: 180, - avg: { value: 255966.30555555556 }, - p95: { values: { '95.0': 320238.5 } }, - sum: { value: 3000000000 }, - sample: { - hits: { - total: 180, - hits: [{ _source: 'sample source' }], - }, - }, - }; - - const response = ({ - aggregations: { - transaction_groups: { - buckets: [bucket], - }, - }, - } as unknown) as ESResponse; - - expect( - transactionGroupsTransformer({ - response, - start: 100, - end: 20000, - bucketSize: 100, - }) - ).toEqual({ - bucketSize: 100, - isAggregationAccurate: true, - items: [ - { - averageResponseTime: 255966.30555555556, - impact: 0, - name: 'POST /api/orders', - p95: 320238.5, - sample: 'sample source', - transactionsPerMinute: 542.713567839196, - }, - ], - }); - }); - - it('`isAggregationAccurate` should be false if number of bucket is higher than `bucketSize`', () => { - const bucket = { - key: { transaction: 'POST /api/orders' }, - doc_count: 180, - avg: { value: 255966.30555555556 }, - p95: { values: { '95.0': 320238.5 } }, - sum: { value: 3000000000 }, - sample: { - hits: { - total: 180, - hits: [{ _source: 'sample source' }], - }, - }, - }; - - const response = ({ - aggregations: { - transaction_groups: { - buckets: [bucket, bucket, bucket, bucket], // four buckets returned - }, - }, - } as unknown) as ESResponse; - - const { isAggregationAccurate } = transactionGroupsTransformer({ - response, - start: 100, - end: 20000, - bucketSize: 3, // bucket size of three - }); - - expect(isAggregationAccurate).toEqual(false); - }); - - it('should calculate impact from sum', () => { - const getBucket = (sum: number) => ({ - key: { transaction: 'POST /api/orders' }, - doc_count: 180, - avg: { value: 300000 }, - p95: { values: { '95.0': 320000 } }, - sum: { value: sum }, - sample: { hits: { total: 180, hits: [{ _source: 'sample source' }] } }, - }); - - const response = ({ - aggregations: { - transaction_groups: { - buckets: [getBucket(10), getBucket(20), getBucket(50)], - }, - }, - } as unknown) as ESResponse; - - const { items } = transactionGroupsTransformer({ - response, - start: 100, - end: 20000, - bucketSize: 100, - }); - - expect(items.map((bucket) => bucket.impact)).toEqual([100, 25, 0]); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/transform.ts b/x-pack/plugins/apm/server/lib/transaction_groups/transform.ts deleted file mode 100644 index b04ff6764675d..0000000000000 --- a/x-pack/plugins/apm/server/lib/transaction_groups/transform.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import moment from 'moment'; -import { orderBy } from 'lodash'; -import { ESResponse } from './fetcher'; - -function calculateRelativeImpacts(items: ITransactionGroup[]) { - const values = items - .map(({ impact }) => impact) - .filter((value) => value !== null) as number[]; - - const max = Math.max(...values); - const min = Math.min(...values); - - return items.map((bucket) => ({ - ...bucket, - impact: - bucket.impact !== null - ? ((bucket.impact - min) / (max - min)) * 100 || 0 - : 0, - })); -} - -const getBuckets = (response: ESResponse) => { - if (response.aggregations) { - return orderBy( - response.aggregations.transaction_groups.buckets, - ['sum.value'], - ['desc'] - ); - } - return []; -}; - -export type ITransactionGroup = ReturnType; -function getTransactionGroup( - bucket: ReturnType[0], - minutes: number -) { - const averageResponseTime = bucket.avg.value; - const transactionsPerMinute = bucket.doc_count / minutes; - const impact = bucket.sum.value; - const sample = bucket.sample.hits.hits[0]._source; - - return { - name: bucket.key.transaction, - sample, - p95: bucket.p95.values['95.0'], - averageResponseTime, - transactionsPerMinute, - impact, - }; -} - -export function transactionGroupsTransformer({ - response, - start, - end, - bucketSize, -}: { - response: ESResponse; - start: number; - end: number; - bucketSize: number; -}): { - items: ITransactionGroup[]; - isAggregationAccurate: boolean; - bucketSize: number; -} { - const buckets = getBuckets(response); - const duration = moment.duration(end - start); - const minutes = duration.asMinutes(); - const items = buckets.map((bucket) => getTransactionGroup(bucket, minutes)); - - const itemsWithRelativeImpact = calculateRelativeImpacts(items); - - return { - items: itemsWithRelativeImpact, - - // The aggregation is considered accurate if the configured bucket size is larger or equal to the number of buckets returned - // the actual number of buckets retrieved are `bucketsize + 1` to detect whether it's above the limit - isAggregationAccurate: bucketSize >= buckets.length, - bucketSize, - }; -} diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts index 1cecf14f2eeb8..e892284fd87cd 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts @@ -35,7 +35,7 @@ export const getLocalFilterQuery = ({ }, }, } - : {}; + : null; return mergeProjection(projection, { body: { diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts index 588d5c7896db9..3833b93c8d1f7 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts @@ -43,6 +43,7 @@ export async function getLocalUIFilters({ const response = await client.search(query); const filter = localUIFilters[name]; + const buckets = response?.aggregations?.by_terms?.buckets ?? []; return { diff --git a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts index ac7499c23e926..d25ec8709e3be 100644 --- a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts +++ b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Unionize } from 'utility-types'; +import { Unionize, UnionToIntersection } from 'utility-types'; type SortOrder = 'asc' | 'desc'; type SortInstruction = Record; @@ -288,10 +288,13 @@ interface AggregationResponsePart< } | undefined; composite: { - after_key: Record, number>; + after_key: Record< + GetCompositeKeys, + string | number + >; buckets: Array< { - key: Record, number>; + key: Record, string | number>; doc_count: number; } & BucketSubAggregationResponse< TAggregationOptionsMap['aggs'], @@ -337,6 +340,15 @@ interface AggregationResponsePart< // keyof AggregationResponsePart<{}, unknown> // >; +// ensures aggregations work with requests where aggregation options are a union type, +// e.g. { transaction_groups: { composite: any } | { terms: any } }. +// Union keys are not included in keyof. The type will fall back to keyof T if +// UnionToIntersection fails, which happens when there are conflicts between the union +// types, e.g. { foo: string; bar?: undefined } | { foo?: undefined; bar: string }; +export type ValidAggregationKeysOf< + T extends Record +> = keyof (UnionToIntersection extends never ? T : UnionToIntersection); + export type AggregationResponseMap< TAggregationInputMap extends AggregationInputMap | undefined, TDocument @@ -345,6 +357,6 @@ export type AggregationResponseMap< [TName in keyof TAggregationInputMap]: AggregationResponsePart< TAggregationInputMap[TName], TDocument - >[AggregationType & keyof TAggregationInputMap[TName]]; + >[AggregationType & ValidAggregationKeysOf]; } : undefined; diff --git a/x-pack/test/apm_api_integration/basic/tests/traces/expectation/top_traces.expectation.json b/x-pack/test/apm_api_integration/basic/tests/traces/expectation/top_traces.expectation.json index bacb340292f93..4db040e92e7fa 100644 --- a/x-pack/test/apm_api_integration/basic/tests/traces/expectation/top_traces.expectation.json +++ b/x-pack/test/apm_api_integration/basic/tests/traces/expectation/top_traces.expectation.json @@ -1,20 +1,35 @@ [ { - "name": "Process payment", + "key": { + "service.name": "opbeans-node", + "transaction.name": "Process payment" + }, + "averageResponseTime": 1745009, + "transactionsPerMinute": 0.25, + "impact": 100, "sample": { "@timestamp": "2020-06-29T06:48:29.892Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:39.379730Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:39.379730Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "observer": { "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", @@ -34,59 +49,97 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "timestamp": { "us": 1593413309892019 }, - "trace": { "id": "bc393b659bef63291b6fa08e6f1d3f14" }, + "timestamp": { + "us": 1593413309892019 + }, + "trace": { + "id": "bc393b659bef63291b6fa08e6f1d3f14" + }, "transaction": { - "duration": { "us": 1745009 }, + "duration": { + "us": 1745009 + }, "id": "a58333df6d851cf1", "name": "Process payment", "result": "success", "sampled": true, - "span_count": { "started": 2 }, + "span_count": { + "started": 2 + }, "type": "Worker" } - }, - "p95": 1744896, - "averageResponseTime": 1745009, - "transactionsPerMinute": 0.25, - "impact": 100 + } }, { - "name": "GET /api", + "key": { + "service.name": "opbeans-node", + "transaction.name": "GET /api" + }, + "averageResponseTime": 49816.15625, + "transactionsPerMinute": 8, + "impact": 91.32732325394932, "sample": { - "@timestamp": "2020-06-29T06:48:41.454Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, + "@timestamp": "2020-06-29T06:48:06.969Z", + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:43.992834Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:08.306961Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", "socket": { @@ -96,11 +149,18 @@ }, "response": { "headers": { - "Connection": ["close"], - "Content-Type": ["application/json;charset=UTF-8"], - "Date": ["Mon, 29 Jun 2020 06:48:41 GMT"], - "Transfer-Encoding": ["chunked"], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "0" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:06 GMT" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 200 }, @@ -130,61 +190,103 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413321454009 }, - "trace": { "id": "0507830eeff93f7bf1a354e4031097b3" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413286969018 + }, + "trace": { + "id": "87a828bcedd44d9e872d8f552fb04aa6" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 8334 }, - "id": "878250a8b937445d", + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 25229 + }, + "id": "b1843afd04271423", "name": "GET /api", "result": "HTTP 2xx", "sampled": true, - "span_count": { "started": 1 }, + "span_count": { + "started": 1 + }, "type": "request" }, "url": { "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/products/6", - "original": "/api/products/6", - "path": "/api/products/6", + "full": "http://opbeans-node:3000/api/orders/474", + "original": "/api/orders/474", + "path": "/api/orders/474", "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 81888, - "averageResponseTime": 49816.15625, - "transactionsPerMinute": 8, - "impact": 91.32732325394932 + } }, { - "name": "/dashboard", + "key": { + "service.name": "client", + "transaction.name": "/dashboard" + }, + "averageResponseTime": 208000, + "transactionsPerMinute": 0.75, + "impact": 35.56882613781033, "sample": { - "@timestamp": "2020-06-29T06:48:21.621Z", - "agent": { "name": "rum-js", "version": "5.2.0" }, - "client": { "ip": "172.18.0.8" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:22.625275Z" }, + "@timestamp": "2020-06-29T06:48:07.275Z", + "agent": { + "name": "rum-js", + "version": "5.2.0" + }, + "client": { + "ip": "172.18.0.8" + }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:08.291261Z" + }, "http": { - "request": { "referrer": "" }, + "request": { + "referrer": "" + }, "response": { "decoded_body_size": 813, "encoded_body_size": 813, @@ -199,52 +301,73 @@ "version": "8.0.0", "version_major": 8 }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { - "language": { "name": "javascript" }, + "language": { + "name": "javascript" + }, "name": "client", "version": "1.0.0" }, - "source": { "ip": "172.18.0.8" }, - "timestamp": { "us": 1593413301621808 }, - "trace": { "id": "ee0ce8b38b8d5945829fc1c9432538bf" }, + "source": { + "ip": "172.18.0.8" + }, + "timestamp": { + "us": 1593413287275113 + }, + "trace": { + "id": "ca86ffcac7753ec8733933bd8fd45d11" + }, "transaction": { "custom": { "userConfig": { - "featureFlags": ["double-trouble", "4423-hotfix"], + "featureFlags": [ + "double-trouble", + "4423-hotfix" + ], "showDashboard": true } }, - "duration": { "us": 109000 }, - "id": "c546a6716b681bf2", + "duration": { + "us": 342000 + }, + "id": "c40f735132c8e864", "marks": { "agent": { - "domComplete": 98, - "domInteractive": 87, - "timeToFirstByte": 3 + "domComplete": 335, + "domInteractive": 327, + "timeToFirstByte": 16 }, "navigationTiming": { - "connectEnd": 0, - "connectStart": 0, - "domComplete": 98, - "domContentLoadedEventEnd": 87, - "domContentLoadedEventStart": 87, - "domInteractive": 87, - "domLoading": 8, - "domainLookupEnd": 0, - "domainLookupStart": 0, + "connectEnd": 12, + "connectStart": 12, + "domComplete": 335, + "domContentLoadedEventEnd": 327, + "domContentLoadedEventStart": 327, + "domInteractive": 327, + "domLoading": 21, + "domainLookupEnd": 12, + "domainLookupStart": 10, "fetchStart": 0, - "loadEventEnd": 98, - "loadEventStart": 98, - "requestStart": 1, - "responseEnd": 8, - "responseStart": 3 + "loadEventEnd": 335, + "loadEventStart": 335, + "requestStart": 12, + "responseEnd": 17, + "responseStart": 16 } }, "name": "/dashboard", - "page": { "referer": "", "url": "http://opbeans-node:3000/dashboard" }, + "page": { + "referer": "", + "url": "http://opbeans-node:3000/dashboard" + }, "sampled": true, - "span_count": { "started": 8 }, + "span_count": { + "started": 9 + }, "type": "page-load" }, "url": { @@ -261,50 +384,75 @@ "name": "arthurdent" }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "HeadlessChrome", "original": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/79.0.3945.0 Safari/537.36", - "os": { "name": "Linux" }, + "os": { + "name": "Linux" + }, "version": "79.0.3945" } - }, - "p95": 341504, - "averageResponseTime": 208000, - "transactionsPerMinute": 0.75, - "impact": 35.56882613781033 + } }, { - "name": "DispatcherServlet#doGet", + "key": { + "service.name": "opbeans-java", + "transaction.name": "DispatcherServlet#doGet" + }, + "averageResponseTime": 36010.53846153846, + "transactionsPerMinute": 3.25, + "impact": 26.61043592713186, "sample": { - "@timestamp": "2020-06-29T06:48:40.104Z", + "@timestamp": "2020-06-29T06:48:10.529Z", "agent": { "ephemeral_id": "222af346-6dd9-45ef-ac85-d86b67edd2de", "name": "java", "version": "1.17.1-SNAPSHOT" }, - "client": { "ip": "172.18.0.9" }, + "client": { + "ip": "172.18.0.9" + }, "container": { "id": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:46.706956Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:15.757591Z" + }, "host": { "architecture": "amd64", "hostname": "918ebbd99b4f", "ip": "172.18.0.6", "name": "918ebbd99b4f", - "os": { "platform": "Linux" } + "os": { + "platform": "Linux" + } }, "http": { "request": { "headers": { - "Accept": ["*/*"], - "Accept-Encoding": ["gzip, deflate"], - "Host": ["172.18.0.6:3000"], - "User-Agent": ["Python/3.7 aiohttp/3.3.2"] + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Host": [ + "172.18.0.6:3000" + ], + "User-Agent": [ + "Python/3.7 aiohttp/3.3.2" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "172.18.0.9" } + "socket": { + "encrypted": false, + "remote_address": "172.18.0.9" + } }, "response": { "finished": true, @@ -321,77 +469,132 @@ "version": "8.0.0", "version_major": 8 }, - "process": { "pid": 6, "ppid": 1, "title": "/opt/java/openjdk/bin/java" }, - "processor": { "event": "transaction", "name": "transaction" }, + "process": { + "pid": 6, + "ppid": 1, + "title": "/opt/java/openjdk/bin/java" + }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "Servlet API" }, - "language": { "name": "Java", "version": "11.0.7" }, + "framework": { + "name": "Servlet API" + }, + "language": { + "name": "Java", + "version": "11.0.7" + }, "name": "opbeans-java", "node": { "name": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" }, - "runtime": { "name": "Java", "version": "11.0.7" }, + "runtime": { + "name": "Java", + "version": "11.0.7" + }, "version": "None" }, - "source": { "ip": "172.18.0.9" }, - "timestamp": { "us": 1593413320104008 }, - "trace": { "id": "90bd7780b32cc51a7f4c200b1e0c170f" }, + "source": { + "ip": "172.18.0.9" + }, + "timestamp": { + "us": 1593413290529006 + }, + "trace": { + "id": "66e3db4cf016b138a43d319d15174891" + }, "transaction": { - "duration": { "us": 8896 }, - "id": "40b22b21e92bbb20", + "duration": { + "us": 34366 + }, + "id": "7ea720a0175e7ffa", "name": "DispatcherServlet#doGet", "result": "HTTP 2xx", "sampled": true, - "span_count": { "dropped": 0, "started": 1 }, + "span_count": { + "dropped": 0, + "started": 1 + }, "type": "request" }, "url": { "domain": "172.18.0.6", - "full": "http://172.18.0.6:3000/api/orders", - "path": "/api/orders", + "full": "http://172.18.0.6:3000/api/products", + "path": "/api/products", "port": 3000, "scheme": "http" }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "Python/3.7 aiohttp/3.3.2" } - }, - "p95": 34528, - "averageResponseTime": 36010.53846153846, - "transactionsPerMinute": 3.25, - "impact": 26.61043592713186 + } }, { - "name": "POST /api/orders", + "key": { + "service.name": "opbeans-node", + "transaction.name": "POST /api/orders" + }, + "averageResponseTime": 270684, + "transactionsPerMinute": 0.25, + "impact": 15.261616628971955, "sample": { "@timestamp": "2020-06-29T06:48:39.953Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:43.991549Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:43.991549Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { - "body": { "original": "[REDACTED]" }, + "body": { + "original": "[REDACTED]" + }, "headers": { - "Accept": ["application/json"], - "Connection": ["close"], - "Content-Length": ["129"], - "Content-Type": ["application/json"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Accept": [ + "application/json" + ], + "Connection": [ + "close" + ], + "Content-Length": [ + "129" + ], + "Content-Type": [ + "application/json" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "post", "socket": { @@ -401,12 +604,24 @@ }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["13"], - "Content-Type": ["application/json; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:40 GMT"], - "Etag": ["W/\"d-eEOWU4Cnr5DZ23ErRUeYu9oOIks\""], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "13" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:40 GMT" + ], + "Etag": [ + "W/\"d-eEOWU4Cnr5DZ23ErRUeYu9oOIks\"" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 200 }, @@ -436,29 +651,52 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413319953033 }, - "trace": { "id": "52b8fda5f6df745b990740ba18378620" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413319953033 + }, + "trace": { + "id": "52b8fda5f6df745b990740ba18378620" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 270684 }, + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 270684 + }, "id": "a3afc2a112e9c893", "name": "POST /api/orders", "result": "HTTP 2xx", "sampled": true, - "span_count": { "started": 16 }, + "span_count": { + "started": 16 + }, "type": "request" }, "url": { @@ -469,50 +707,77 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 270336, - "averageResponseTime": 270684, - "transactionsPerMinute": 0.25, - "impact": 15.261616628971955 + } }, { - "name": "ResourceHttpRequestHandler", + "key": { + "service.name": "opbeans-java", + "transaction.name": "ResourceHttpRequestHandler" + }, + "averageResponseTime": 14419.42857142857, + "transactionsPerMinute": 3.5, + "impact": 11.30657439844125, "sample": { - "@timestamp": "2020-06-29T06:48:44.376Z", + "@timestamp": "2020-06-29T06:48:06.640Z", "agent": { "ephemeral_id": "222af346-6dd9-45ef-ac85-d86b67edd2de", "name": "java", "version": "1.17.1-SNAPSHOT" }, - "client": { "ip": "172.18.0.9" }, + "client": { + "ip": "172.18.0.9" + }, "container": { "id": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:46.720380Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:15.517678Z" + }, "host": { "architecture": "amd64", "hostname": "918ebbd99b4f", "ip": "172.18.0.6", "name": "918ebbd99b4f", - "os": { "platform": "Linux" } + "os": { + "platform": "Linux" + } }, "http": { "request": { "headers": { - "Accept": ["*/*"], - "Accept-Encoding": ["gzip, deflate"], - "Host": ["172.18.0.6:3000"], - "User-Agent": ["Python/3.7 aiohttp/3.3.2"] + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Host": [ + "172.18.0.6:3000" + ], + "User-Agent": [ + "Python/3.7 aiohttp/3.3.2" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "172.18.0.9" } + "socket": { + "encrypted": false, + "remote_address": "172.18.0.9" + } }, "response": { "finished": true, @@ -529,63 +794,100 @@ "version": "8.0.0", "version_major": 8 }, - "process": { "pid": 6, "ppid": 1, "title": "/opt/java/openjdk/bin/java" }, - "processor": { "event": "transaction", "name": "transaction" }, + "process": { + "pid": 6, + "ppid": 1, + "title": "/opt/java/openjdk/bin/java" + }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "Spring Web MVC", "version": "5.0.6.RELEASE" }, - "language": { "name": "Java", "version": "11.0.7" }, + "framework": { + "name": "Spring Web MVC", + "version": "5.0.6.RELEASE" + }, + "language": { + "name": "Java", + "version": "11.0.7" + }, "name": "opbeans-java", "node": { "name": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" }, - "runtime": { "name": "Java", "version": "11.0.7" }, + "runtime": { + "name": "Java", + "version": "11.0.7" + }, "version": "None" }, - "source": { "ip": "172.18.0.9" }, - "timestamp": { "us": 1593413324376010 }, - "trace": { "id": "7e70dc471913473e7d3bffda9b27d720" }, + "source": { + "ip": "172.18.0.9" + }, + "timestamp": { + "us": 1593413286640008 + }, + "trace": { + "id": "81d8ffb0a39e755eed400f6486e15672" + }, "transaction": { - "duration": { "us": 1420 }, - "id": "5c4e9f4b0846c2f8", + "duration": { + "us": 2953 + }, + "id": "353d42a2f9046e99", "name": "ResourceHttpRequestHandler", "result": "HTTP 4xx", "sampled": true, - "span_count": { "dropped": 0, "started": 0 }, + "span_count": { + "dropped": 0, + "started": 0 + }, "type": "request" }, "url": { "domain": "172.18.0.6", - "full": "http://172.18.0.6:3000/api/types", - "path": "/api/types", + "full": "http://172.18.0.6:3000/api/types/3", + "path": "/api/types/3", "port": 3000, "scheme": "http" }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "Python/3.7 aiohttp/3.3.2" } - }, - "p95": 4120, - "averageResponseTime": 14419.42857142857, - "transactionsPerMinute": 3.5, - "impact": 11.30657439844125 + } }, { - "name": "/orders", + "key": { + "service.name": "client", + "transaction.name": "/orders" + }, + "averageResponseTime": 81500, + "transactionsPerMinute": 0.5, + "impact": 9.072365225837785, "sample": { - "@timestamp": "2020-06-29T06:48:38.358Z", - "agent": { "name": "rum-js", "version": "5.2.0" }, - "client": { "ip": "172.18.0.8" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:39.365914Z" }, + "@timestamp": "2020-06-29T06:48:29.296Z", + "agent": { + "name": "rum-js", + "version": "5.2.0" + }, + "client": { + "ip": "172.18.0.8" + }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:29.986555Z" + }, "http": { - "request": { "referrer": "" }, - "response": { - "decoded_body_size": 813, - "encoded_body_size": 813, - "transfer_size": 962 + "request": { + "referrer": "" } }, "observer": { @@ -596,53 +898,50 @@ "version": "8.0.0", "version_major": 8 }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { - "language": { "name": "javascript" }, + "language": { + "name": "javascript" + }, "name": "client", "version": "1.0.0" }, - "source": { "ip": "172.18.0.8" }, - "timestamp": { "us": 1593413318358392 }, - "trace": { "id": "c1dea08a4128e776fd9965ccf8c8da99" }, + "source": { + "ip": "172.18.0.8" + }, + "timestamp": { + "us": 1593413309296660 + }, + "trace": { + "id": "978b56807e0b7a27cbc41a0dfb665f47" + }, "transaction": { "custom": { "userConfig": { - "featureFlags": ["double-trouble", "4423-hotfix"], + "featureFlags": [ + "double-trouble", + "4423-hotfix" + ], "showDashboard": true } }, - "duration": { "us": 140000 }, - "id": "4f2ea2796645d6e5", - "marks": { - "agent": { - "domComplete": 126, - "domInteractive": 116, - "timeToFirstByte": 3 - }, - "navigationTiming": { - "connectEnd": 0, - "connectStart": 0, - "domComplete": 126, - "domContentLoadedEventEnd": 116, - "domContentLoadedEventStart": 116, - "domInteractive": 116, - "domLoading": 20, - "domainLookupEnd": 0, - "domainLookupStart": 0, - "fetchStart": 0, - "loadEventEnd": 126, - "loadEventStart": 126, - "requestStart": 1, - "responseEnd": 3, - "responseStart": 3 - } + "duration": { + "us": 23000 }, + "id": "c3801eadbdef5c7c", "name": "/orders", - "page": { "referer": "", "url": "http://opbeans-node:3000/orders" }, + "page": { + "referer": "", + "url": "http://opbeans-node:3000/orders" + }, "sampled": true, - "span_count": { "started": 7 }, - "type": "page-load" + "span_count": { + "started": 1 + }, + "type": "route-change" }, "url": { "domain": "opbeans-node", @@ -652,59 +951,94 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "adastra@example.com", "id": "3", "name": "trillian" }, + "user": { + "email": "arthur.dent@example.com", + "id": "1", + "name": "arthurdent" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "HeadlessChrome", "original": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/79.0.3945.0 Safari/537.36", - "os": { "name": "Linux" }, + "os": { + "name": "Linux" + }, "version": "79.0.3945" } - }, - "p95": 140160, - "averageResponseTime": 81500, - "transactionsPerMinute": 0.5, - "impact": 9.072365225837785 + } }, { - "name": "APIRestController#customer", + "key": { + "service.name": "opbeans-java", + "transaction.name": "APIRestController#customer" + }, + "averageResponseTime": 19370.6, + "transactionsPerMinute": 1.25, + "impact": 5.270496679320978, "sample": { - "@timestamp": "2020-06-29T06:48:38.893Z", + "@timestamp": "2020-06-29T06:48:08.631Z", "agent": { "ephemeral_id": "222af346-6dd9-45ef-ac85-d86b67edd2de", "name": "java", "version": "1.17.1-SNAPSHOT" }, - "client": { "ip": "172.18.0.9" }, + "client": { + "ip": "172.18.0.9" + }, "container": { "id": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:46.680126Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:15.536897Z" + }, "host": { "architecture": "amd64", "hostname": "918ebbd99b4f", "ip": "172.18.0.6", "name": "918ebbd99b4f", - "os": { "platform": "Linux" } + "os": { + "platform": "Linux" + } }, "http": { "request": { "headers": { - "Accept": ["*/*"], - "Accept-Encoding": ["gzip, deflate"], - "Host": ["172.18.0.6:3000"], - "User-Agent": ["Python/3.7 aiohttp/3.3.2"] + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Host": [ + "172.18.0.6:3000" + ], + "User-Agent": [ + "Python/3.7 aiohttp/3.3.2" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "172.18.0.9" } + "socket": { + "encrypted": false, + "remote_address": "172.18.0.9" + } }, "response": { "finished": true, "headers": { - "Content-Type": ["application/json;charset=UTF-8"], - "Date": ["Mon, 29 Jun 2020 06:48:38 GMT"], - "Transfer-Encoding": ["chunked"] + "Content-Type": [ + "application/json;charset=UTF-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:08 GMT" + ], + "Transfer-Encoding": [ + "chunked" + ] }, "headers_sent": true, "status_code": 200 @@ -719,59 +1053,101 @@ "version": "8.0.0", "version_major": 8 }, - "process": { "pid": 6, "ppid": 1, "title": "/opt/java/openjdk/bin/java" }, - "processor": { "event": "transaction", "name": "transaction" }, + "process": { + "pid": 6, + "ppid": 1, + "title": "/opt/java/openjdk/bin/java" + }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "Spring Web MVC", "version": "5.0.6.RELEASE" }, - "language": { "name": "Java", "version": "11.0.7" }, + "framework": { + "name": "Spring Web MVC", + "version": "5.0.6.RELEASE" + }, + "language": { + "name": "Java", + "version": "11.0.7" + }, "name": "opbeans-java", "node": { "name": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" }, - "runtime": { "name": "Java", "version": "11.0.7" }, + "runtime": { + "name": "Java", + "version": "11.0.7" + }, "version": "None" }, - "source": { "ip": "172.18.0.9" }, - "timestamp": { "us": 1593413318893006 }, - "trace": { "id": "efcf3446b51d080dbde1339969cf79a0" }, + "source": { + "ip": "172.18.0.9" + }, + "timestamp": { + "us": 1593413288631008 + }, + "trace": { + "id": "c00da24c5c793cd679ce3df47cee8f37" + }, "transaction": { - "duration": { "us": 4594 }, - "id": "31ef64d71933e846", + "duration": { + "us": 76826 + }, + "id": "3c8403055ff75866", "name": "APIRestController#customer", "result": "HTTP 2xx", "sampled": true, - "span_count": { "dropped": 0, "started": 2 }, + "span_count": { + "dropped": 0, + "started": 2 + }, "type": "request" }, "url": { "domain": "172.18.0.6", - "full": "http://172.18.0.6:3000/api/customers/235", - "path": "/api/customers/235", + "full": "http://172.18.0.6:3000/api/customers/56", + "path": "/api/customers/56", "port": 3000, "scheme": "http" }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "Python/3.7 aiohttp/3.3.2" } - }, - "p95": 77280, - "averageResponseTime": 19370.6, - "transactionsPerMinute": 1.25, - "impact": 5.270496679320978 + } }, { - "name": "/products", + "key": { + "service.name": "client", + "transaction.name": "/products" + }, + "averageResponseTime": 77000, + "transactionsPerMinute": 0.25, + "impact": 4.129424578484989, "sample": { "@timestamp": "2020-06-29T06:48:48.824Z", - "agent": { "name": "rum-js", "version": "5.2.0" }, - "client": { "ip": "172.18.0.8" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:49.293664Z" }, + "agent": { + "name": "rum-js", + "version": "5.2.0" + }, + "client": { + "ip": "172.18.0.8" + }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:49.293664Z" + }, "http": { - "request": { "referrer": "" }, + "request": { + "referrer": "" + }, "response": { "decoded_body_size": 813, "encoded_body_size": 813, @@ -786,23 +1162,39 @@ "version": "8.0.0", "version_major": 8 }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { - "language": { "name": "javascript" }, + "language": { + "name": "javascript" + }, "name": "client", "version": "1.0.0" }, - "source": { "ip": "172.18.0.8" }, - "timestamp": { "us": 1593413328824656 }, - "trace": { "id": "f6c4a9197bbd080bd45072970f251525" }, + "source": { + "ip": "172.18.0.8" + }, + "timestamp": { + "us": 1593413328824656 + }, + "trace": { + "id": "f6c4a9197bbd080bd45072970f251525" + }, "transaction": { "custom": { "userConfig": { - "featureFlags": ["double-trouble", "4423-hotfix"], + "featureFlags": [ + "double-trouble", + "4423-hotfix" + ], "showDashboard": true } }, - "duration": { "us": 77000 }, + "duration": { + "us": 77000 + }, "id": "a11ede1968973bc5", "marks": { "agent": { @@ -829,9 +1221,14 @@ } }, "name": "/products", - "page": { "referer": "", "url": "http://opbeans-node:3000/products" }, + "page": { + "referer": "", + "url": "http://opbeans-node:3000/products" + }, "sampled": true, - "span_count": { "started": 5 }, + "span_count": { + "started": 5 + }, "type": "page-load" }, "url": { @@ -842,29 +1239,52 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "z@example.com", "id": "4", "name": "zaphod" }, + "user": { + "email": "z@example.com", + "id": "4", + "name": "zaphod" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "HeadlessChrome", "original": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/79.0.3945.0 Safari/537.36", - "os": { "name": "Linux" }, + "os": { + "name": "Linux" + }, "version": "79.0.3945" } - }, - "p95": 76800, - "averageResponseTime": 77000, - "transactionsPerMinute": 0.25, - "impact": 4.129424578484989 + } }, { - "name": "/customers", + "key": { + "service.name": "client", + "transaction.name": "/customers" + }, + "averageResponseTime": 33500, + "transactionsPerMinute": 0.5, + "impact": 3.5546640380951287, "sample": { - "@timestamp": "2020-06-29T06:48:48.287Z", - "agent": { "name": "rum-js", "version": "5.2.0" }, - "client": { "ip": "172.18.0.8" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:49.292535Z" }, - "http": { "request": { "referrer": "" } }, + "@timestamp": "2020-06-29T06:48:35.071Z", + "agent": { + "name": "rum-js", + "version": "5.2.0" + }, + "client": { + "ip": "172.18.0.8" + }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:36.077184Z" + }, + "http": { + "request": { + "referrer": "" + } + }, "observer": { "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", "hostname": "aa0bd613aa4c", @@ -873,28 +1293,49 @@ "version": "8.0.0", "version_major": 8 }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { - "language": { "name": "javascript" }, + "language": { + "name": "javascript" + }, "name": "client", "version": "1.0.0" }, - "source": { "ip": "172.18.0.8" }, - "timestamp": { "us": 1593413328287946 }, - "trace": { "id": "48d130530a1fc0b2b99d138d3461bad4" }, + "source": { + "ip": "172.18.0.8" + }, + "timestamp": { + "us": 1593413315071116 + }, + "trace": { + "id": "547a92e82a25387321d1b967f2dd0f48" + }, "transaction": { "custom": { "userConfig": { - "featureFlags": ["double-trouble", "4423-hotfix"], + "featureFlags": [ + "double-trouble", + "4423-hotfix" + ], "showDashboard": true } }, - "duration": { "us": 39000 }, - "id": "2f3a2b0fd3016d3e", + "duration": { + "us": 28000 + }, + "id": "d24f9b9dacb83450", "name": "/customers", - "page": { "referer": "", "url": "http://opbeans-node:3000/customers" }, + "page": { + "referer": "", + "url": "http://opbeans-node:3000/customers" + }, "sampled": true, - "span_count": { "started": 1 }, + "span_count": { + "started": 1 + }, "type": "route-change" }, "url": { @@ -905,59 +1346,94 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "adastra@example.com", "id": "3", "name": "trillian" }, + "user": { + "email": "arthur.dent@example.com", + "id": "1", + "name": "arthurdent" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "HeadlessChrome", "original": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/79.0.3945.0 Safari/537.36", - "os": { "name": "Linux" }, + "os": { + "name": "Linux" + }, "version": "79.0.3945" } - }, - "p95": 39040, - "averageResponseTime": 33500, - "transactionsPerMinute": 0.5, - "impact": 3.5546640380951287 + } }, { - "name": "APIRestController#customers", + "key": { + "service.name": "opbeans-java", + "transaction.name": "APIRestController#customers" + }, + "averageResponseTime": 16690.75, + "transactionsPerMinute": 1, + "impact": 3.541042213287889, "sample": { - "@timestamp": "2020-06-29T06:48:43.765Z", + "@timestamp": "2020-06-29T06:48:22.372Z", "agent": { "ephemeral_id": "222af346-6dd9-45ef-ac85-d86b67edd2de", "name": "java", "version": "1.17.1-SNAPSHOT" }, - "client": { "ip": "172.18.0.9" }, + "client": { + "ip": "172.18.0.9" + }, "container": { "id": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:46.716850Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:25.888154Z" + }, "host": { "architecture": "amd64", "hostname": "918ebbd99b4f", "ip": "172.18.0.6", "name": "918ebbd99b4f", - "os": { "platform": "Linux" } + "os": { + "platform": "Linux" + } }, "http": { "request": { "headers": { - "Accept": ["*/*"], - "Accept-Encoding": ["gzip, deflate"], - "Host": ["172.18.0.6:3000"], - "User-Agent": ["Python/3.7 aiohttp/3.3.2"] + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Host": [ + "172.18.0.6:3000" + ], + "User-Agent": [ + "Python/3.7 aiohttp/3.3.2" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "172.18.0.9" } + "socket": { + "encrypted": false, + "remote_address": "172.18.0.9" + } }, "response": { "finished": true, "headers": { - "Content-Type": ["application/json;charset=UTF-8"], - "Date": ["Mon, 29 Jun 2020 06:48:43 GMT"], - "Transfer-Encoding": ["chunked"] + "Content-Type": [ + "application/json;charset=UTF-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:21 GMT" + ], + "Transfer-Encoding": [ + "chunked" + ] }, "headers_sent": true, "status_code": 500 @@ -972,29 +1448,56 @@ "version": "8.0.0", "version_major": 8 }, - "process": { "pid": 6, "ppid": 1, "title": "/opt/java/openjdk/bin/java" }, - "processor": { "event": "transaction", "name": "transaction" }, + "process": { + "pid": 6, + "ppid": 1, + "title": "/opt/java/openjdk/bin/java" + }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "Spring Web MVC", "version": "5.0.6.RELEASE" }, - "language": { "name": "Java", "version": "11.0.7" }, + "framework": { + "name": "Spring Web MVC", + "version": "5.0.6.RELEASE" + }, + "language": { + "name": "Java", + "version": "11.0.7" + }, "name": "opbeans-java", "node": { "name": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" }, - "runtime": { "name": "Java", "version": "11.0.7" }, + "runtime": { + "name": "Java", + "version": "11.0.7" + }, "version": "None" }, - "source": { "ip": "172.18.0.9" }, - "timestamp": { "us": 1593413323765008 }, - "trace": { "id": "affce4cea9b60bd5b635dc1bd2e4ce79" }, + "source": { + "ip": "172.18.0.9" + }, + "timestamp": { + "us": 1593413302372009 + }, + "trace": { + "id": "21dd795dc3a260b1bf7ebbbac1e86fb8" + }, "transaction": { - "duration": { "us": 10880 }, - "id": "cfe0a84b49b4a340", + "duration": { + "us": 14795 + }, + "id": "0157fc513282138f", "name": "APIRestController#customers", "result": "HTTP 5xx", "sampled": true, - "span_count": { "dropped": 0, "started": 2 }, + "span_count": { + "dropped": 0, + "started": 2 + }, "type": "request" }, "url": { @@ -1005,40 +1508,61 @@ "scheme": "http" }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "Python/3.7 aiohttp/3.3.2" } - }, - "p95": 26432, - "averageResponseTime": 16690.75, - "transactionsPerMinute": 1, - "impact": 3.541042213287889 + } }, { - "name": "GET /log-message", + "key": { + "service.name": "opbeans-node", + "transaction.name": "GET /log-message" + }, + "averageResponseTime": 32667.5, + "transactionsPerMinute": 0.5, + "impact": 3.458966408120217, "sample": { - "@timestamp": "2020-06-29T06:48:28.944Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, + "@timestamp": "2020-06-29T06:48:25.944Z", + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:39.370695Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:29.976822Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", "socket": { @@ -1048,12 +1572,24 @@ }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["24"], - "Content-Type": ["text/html; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:28 GMT"], - "Etag": ["W/\"18-MS3VbhH7auHMzO0fUuNF6v14N/M\""], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "24" + ], + "Content-Type": [ + "text/html; charset=utf-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:25 GMT" + ], + "Etag": [ + "W/\"18-MS3VbhH7auHMzO0fUuNF6v14N/M\"" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 500 }, @@ -1083,29 +1619,52 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413308944016 }, - "trace": { "id": "afabe4cb397616f5ec35a2f3da49ba62" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413305944023 + }, + "trace": { + "id": "cd2ad726ad164d701c5d3103cbab0c81" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 26788 }, - "id": "cc8c4261387507cf", + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 38547 + }, + "id": "9e41667eb64dea55", "name": "GET /log-message", "result": "HTTP 5xx", "sampled": true, - "span_count": { "started": 0 }, + "span_count": { + "started": 0 + }, "type": "request" }, "url": { @@ -1116,60 +1675,82 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 38528, - "averageResponseTime": 32667.5, - "transactionsPerMinute": 0.5, - "impact": 3.458966408120217 + } }, { - "name": "APIRestController#stats", + "key": { + "service.name": "opbeans-java", + "transaction.name": "APIRestController#stats" + }, + "averageResponseTime": 15535, + "transactionsPerMinute": 1, + "impact": 3.275330415465657, "sample": { - "@timestamp": "2020-06-29T06:48:42.549Z", + "@timestamp": "2020-06-29T06:48:09.912Z", "agent": { "ephemeral_id": "222af346-6dd9-45ef-ac85-d86b67edd2de", "name": "java", "version": "1.17.1-SNAPSHOT" }, - "client": { "ip": "172.18.0.9" }, + "client": { + "ip": "172.18.0.9" + }, "container": { "id": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:46.715898Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:15.543824Z" + }, "host": { "architecture": "amd64", "hostname": "918ebbd99b4f", "ip": "172.18.0.6", "name": "918ebbd99b4f", - "os": { "platform": "Linux" } + "os": { + "platform": "Linux" + } }, "http": { "request": { "headers": { - "Accept": ["*/*"], - "Accept-Encoding": ["gzip, deflate"], - "Host": ["172.18.0.6:3000"], - "User-Agent": ["Python/3.7 aiohttp/3.3.2"] + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Host": [ + "172.18.0.6:3000" + ], + "User-Agent": [ + "Python/3.7 aiohttp/3.3.2" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "172.18.0.9" } + "socket": { + "encrypted": false, + "remote_address": "172.18.0.9" + } }, "response": { "finished": true, - "headers": { - "Content-Type": ["application/json;charset=UTF-8"], - "Date": ["Mon, 29 Jun 2020 06:48:42 GMT"], - "Transfer-Encoding": ["chunked"] - }, "headers_sent": true, - "status_code": 200 + "status_code": 500 }, "version": "1.1" }, @@ -1181,29 +1762,56 @@ "version": "8.0.0", "version_major": 8 }, - "process": { "pid": 6, "ppid": 1, "title": "/opt/java/openjdk/bin/java" }, - "processor": { "event": "transaction", "name": "transaction" }, + "process": { + "pid": 6, + "ppid": 1, + "title": "/opt/java/openjdk/bin/java" + }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "Spring Web MVC", "version": "5.0.6.RELEASE" }, - "language": { "name": "Java", "version": "11.0.7" }, + "framework": { + "name": "Spring Web MVC", + "version": "5.0.6.RELEASE" + }, + "language": { + "name": "Java", + "version": "11.0.7" + }, "name": "opbeans-java", "node": { "name": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" }, - "runtime": { "name": "Java", "version": "11.0.7" }, + "runtime": { + "name": "Java", + "version": "11.0.7" + }, "version": "None" }, - "source": { "ip": "172.18.0.9" }, - "timestamp": { "us": 1593413322549008 }, - "trace": { "id": "c3556e143784f94d4b4220dd40687fae" }, + "source": { + "ip": "172.18.0.9" + }, + "timestamp": { + "us": 1593413289912007 + }, + "trace": { + "id": "a17ceae4e18d50430ca15ecca5a3e69f" + }, "transaction": { - "duration": { "us": 9166 }, - "id": "ac40e567f63c3eef", + "duration": { + "us": 10930 + }, + "id": "9fb330060bb73271", "name": "APIRestController#stats", - "result": "HTTP 2xx", + "result": "HTTP 5xx", "sampled": true, - "span_count": { "dropped": 0, "started": 5 }, + "span_count": { + "dropped": 0, + "started": 5 + }, "type": "request" }, "url": { @@ -1214,40 +1822,61 @@ "scheme": "http" }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "Python/3.7 aiohttp/3.3.2" } - }, - "p95": 32064, - "averageResponseTime": 15535, - "transactionsPerMinute": 1, - "impact": 3.275330415465657 + } }, { - "name": "GET /api/customers", + "key": { + "service.name": "opbeans-node", + "transaction.name": "GET /api/customers" + }, + "averageResponseTime": 20092, + "transactionsPerMinute": 0.75, + "impact": 3.168195050736987, "sample": { - "@timestamp": "2020-06-29T06:48:37.952Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, + "@timestamp": "2020-06-29T06:48:28.444Z", + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:39.492402Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:29.982737Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", "socket": { @@ -1257,12 +1886,24 @@ }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["186769"], - "Content-Type": ["application/json; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:37 GMT"], - "Etag": ["W/\"2d991-yG3J8W/roH7fSxXTudZrO27Ax9s\""], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "186769" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:28 GMT" + ], + "Etag": [ + "W/\"2d991-yG3J8W/roH7fSxXTudZrO27Ax9s\"" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 200 }, @@ -1292,29 +1933,52 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413317952016 }, - "trace": { "id": "5d99327edae38ed735e8d7a6028cf719" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413308444015 + }, + "trace": { + "id": "792fb0b00256164e88b277ec40b65e14" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 16824 }, - "id": "071808429ec9d00b", + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 26471 + }, + "id": "6c1f848752563d2b", "name": "GET /api/customers", "result": "HTTP 2xx", "sampled": true, - "span_count": { "started": 2 }, + "span_count": { + "started": 2 + }, "type": "request" }, "url": { @@ -1325,42 +1989,67 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 26368, - "averageResponseTime": 20092, - "transactionsPerMinute": 0.75, - "impact": 3.168195050736987 + } }, { - "name": "GET /api/products/:id", + "key": { + "service.name": "opbeans-node", + "transaction.name": "GET /api/products/:id" + }, + "averageResponseTime": 13516.5, + "transactionsPerMinute": 1, + "impact": 2.8112687551548836, "sample": { - "@timestamp": "2020-06-29T06:48:24.977Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, + "@timestamp": "2020-06-29T06:47:57.555Z", + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:27.004717Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:47:59.085077Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", "socket": { @@ -1370,12 +2059,24 @@ }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["231"], - "Content-Type": ["application/json; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:24 GMT"], - "Etag": ["W/\"e7-kkuzj37GZDzXDh0CWqh5Gan0VO4\""], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "231" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:47:57 GMT" + ], + "Etag": [ + "W/\"e7-6JlJegaJ+ir0C8I8EmmOjms1dnc\"" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 200 }, @@ -1401,79 +2102,127 @@ "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", "ecosystem-workload.config.js" ], - "pid": 137, + "pid": 87, "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413304977014 }, - "trace": { "id": "b9b290bca14c99962fa9a1c509401630" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413277555176 + }, + "trace": { + "id": "8365e1763f19e4067b88521d4d9247a0" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 4482 }, - "id": "b8d8284ff1fc25d6", + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 37709 + }, + "id": "be2722a418272f10", "name": "GET /api/products/:id", "result": "HTTP 2xx", "sampled": true, - "span_count": { "started": 1 }, + "span_count": { + "started": 1 + }, "type": "request" }, "url": { "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/products/3", - "original": "/api/products/3", - "path": "/api/products/3", + "full": "http://opbeans-node:3000/api/products/1", + "original": "/api/products/1", + "path": "/api/products/1", "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 37856, - "averageResponseTime": 13516.5, - "transactionsPerMinute": 1, - "impact": 2.8112687551548836 + } }, { - "name": "GET /api/types", + "key": { + "service.name": "opbeans-node", + "transaction.name": "GET /api/types" + }, + "averageResponseTime": 26992.5, + "transactionsPerMinute": 0.5, + "impact": 2.8066131947777255, "sample": { - "@timestamp": "2020-06-29T06:48:26.443Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, + "@timestamp": "2020-06-29T06:47:52.935Z", + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:29.977518Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:47:55.471071Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", "socket": { @@ -1483,12 +2232,24 @@ }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["112"], - "Content-Type": ["application/json; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:26 GMT"], - "Etag": ["W/\"70-1z6hT7P1WHgBgS/BeUEVeHhOCQU\""], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "112" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:47:52 GMT" + ], + "Etag": [ + "W/\"70-1z6hT7P1WHgBgS/BeUEVeHhOCQU\"" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 200 }, @@ -1514,33 +2275,56 @@ "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", "ecosystem-workload.config.js" ], - "pid": 137, + "pid": 63, "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413306443014 }, - "trace": { "id": "be3f4eb50d253afc032c90eacaa85072" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413272935117 + }, + "trace": { + "id": "2946c536a33d163d0c984d00d1f3839a" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 8892 }, - "id": "ccce129bb8c6b125", + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 45093 + }, + "id": "103482fda88b9400", "name": "GET /api/types", "result": "HTTP 2xx", "sampled": true, - "span_count": { "started": 2 }, + "span_count": { + "started": 2 + }, "type": "request" }, "url": { @@ -1551,42 +2335,67 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 45248, - "averageResponseTime": 26992.5, - "transactionsPerMinute": 0.5, - "impact": 2.8066131947777255 + } }, { - "name": "GET static file", + "key": { + "service.name": "opbeans-node", + "transaction.name": "GET static file" + }, + "averageResponseTime": 3492.9285714285716, + "transactionsPerMinute": 3.5, + "impact": 2.5144049360435208, "sample": { - "@timestamp": "2020-06-29T06:48:40.953Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, + "@timestamp": "2020-06-29T06:47:53.427Z", + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:43.992454Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:47:55.472070Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", "socket": { @@ -1596,15 +2405,33 @@ }, "response": { "headers": { - "Accept-Ranges": ["bytes"], - "Cache-Control": ["public, max-age=0"], - "Connection": ["close"], - "Content-Length": ["15086"], - "Content-Type": ["image/x-icon"], - "Date": ["Mon, 29 Jun 2020 06:48:40 GMT"], - "Etag": ["W/\"3aee-1725aff14f0\""], - "Last-Modified": ["Thu, 28 May 2020 11:16:06 GMT"], - "X-Powered-By": ["Express"] + "Accept-Ranges": [ + "bytes" + ], + "Cache-Control": [ + "public, max-age=0" + ], + "Connection": [ + "close" + ], + "Content-Length": [ + "15086" + ], + "Content-Type": [ + "image/x-icon" + ], + "Date": [ + "Mon, 29 Jun 2020 06:47:53 GMT" + ], + "Etag": [ + "W/\"3aee-1725aff14f0\"" + ], + "Last-Modified": [ + "Thu, 28 May 2020 11:16:06 GMT" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 200 }, @@ -1624,32 +2451,53 @@ "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", "ecosystem-workload.config.js" ], - "pid": 137, + "pid": 63, "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413320953016 }, - "trace": { "id": "292393440bbe04385f3c2e3715ac35fe" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413273427016 + }, + "trace": { + "id": "ec8a804fedf28fcf81d5682d69a16970" + }, "transaction": { - "duration": { "us": 1671 }, - "id": "d1d964ca1865dce3", + "duration": { + "us": 4934 + }, + "id": "ab90a62901b770e6", "name": "GET static file", "result": "HTTP 2xx", "sampled": true, - "span_count": { "started": 0 }, + "span_count": { + "started": 0 + }, "type": "request" }, "url": { @@ -1661,55 +2509,84 @@ "scheme": "http" }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 11900, - "averageResponseTime": 3492.9285714285716, - "transactionsPerMinute": 3.5, - "impact": 2.5144049360435208 + } }, { - "name": "APIRestController#customerWhoBought", + "key": { + "service.name": "opbeans-java", + "transaction.name": "APIRestController#customerWhoBought" + }, + "averageResponseTime": 3742.153846153846, + "transactionsPerMinute": 3.25, + "impact": 2.4998634943716573, "sample": { - "@timestamp": "2020-06-29T06:48:44.982Z", + "@timestamp": "2020-06-29T06:48:11.166Z", "agent": { "ephemeral_id": "222af346-6dd9-45ef-ac85-d86b67edd2de", "name": "java", "version": "1.17.1-SNAPSHOT" }, - "client": { "ip": "172.18.0.9" }, + "client": { + "ip": "172.18.0.9" + }, "container": { "id": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:46.721114Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:15.763228Z" + }, "host": { "architecture": "amd64", "hostname": "918ebbd99b4f", "ip": "172.18.0.6", "name": "918ebbd99b4f", - "os": { "platform": "Linux" } + "os": { + "platform": "Linux" + } }, "http": { "request": { "headers": { - "Accept": ["*/*"], - "Accept-Encoding": ["gzip, deflate"], - "Host": ["172.18.0.6:3000"], - "User-Agent": ["Python/3.7 aiohttp/3.3.2"] + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Host": [ + "172.18.0.6:3000" + ], + "User-Agent": [ + "Python/3.7 aiohttp/3.3.2" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "172.18.0.9" } + "socket": { + "encrypted": false, + "remote_address": "172.18.0.9" + } }, "response": { "finished": true, "headers": { - "Content-Type": ["application/json;charset=UTF-8"], - "Date": ["Mon, 29 Jun 2020 06:48:44 GMT"], - "Transfer-Encoding": ["chunked"] + "Content-Type": [ + "application/json;charset=UTF-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:10 GMT" + ], + "Transfer-Encoding": [ + "chunked" + ] }, "headers_sent": true, "status_code": 200 @@ -1724,73 +2601,121 @@ "version": "8.0.0", "version_major": 8 }, - "process": { "pid": 6, "ppid": 1, "title": "/opt/java/openjdk/bin/java" }, - "processor": { "event": "transaction", "name": "transaction" }, + "process": { + "pid": 6, + "ppid": 1, + "title": "/opt/java/openjdk/bin/java" + }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "Spring Web MVC", "version": "5.0.6.RELEASE" }, - "language": { "name": "Java", "version": "11.0.7" }, + "framework": { + "name": "Spring Web MVC", + "version": "5.0.6.RELEASE" + }, + "language": { + "name": "Java", + "version": "11.0.7" + }, "name": "opbeans-java", "node": { "name": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" }, - "runtime": { "name": "Java", "version": "11.0.7" }, + "runtime": { + "name": "Java", + "version": "11.0.7" + }, "version": "None" }, - "source": { "ip": "172.18.0.9" }, - "timestamp": { "us": 1593413324982008 }, - "trace": { "id": "e5ce8ba877f69510e7abc3f0d11c1e4f" }, + "source": { + "ip": "172.18.0.9" + }, + "timestamp": { + "us": 1593413291166005 + }, + "trace": { + "id": "fa0d353eb7967b344ed37674f40b2884" + }, "transaction": { - "duration": { "us": 2797 }, - "id": "b8c1bd3b31b197d3", + "duration": { + "us": 4453 + }, + "id": "bce4ce4b09ded6ca", "name": "APIRestController#customerWhoBought", "result": "HTTP 2xx", "sampled": true, - "span_count": { "dropped": 0, "started": 1 }, + "span_count": { + "dropped": 0, + "started": 2 + }, "type": "request" }, "url": { "domain": "172.18.0.6", - "full": "http://172.18.0.6:3000/api/products/5/customers", - "path": "/api/products/5/customers", + "full": "http://172.18.0.6:3000/api/products/3/customers", + "path": "/api/products/3/customers", "port": 3000, "scheme": "http" }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "Python/3.7 aiohttp/3.3.2" } - }, - "p95": 4464, - "averageResponseTime": 3742.153846153846, - "transactionsPerMinute": 3.25, - "impact": 2.4998634943716573 + } }, { - "name": "GET /log-error", + "key": { + "service.name": "opbeans-node", + "transaction.name": "GET /log-error" + }, + "averageResponseTime": 35846, + "transactionsPerMinute": 0.25, + "impact": 1.7640550505645587, "sample": { "@timestamp": "2020-06-29T06:48:07.467Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:18.533253Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:18.533253Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", "socket": { @@ -1800,12 +2725,24 @@ }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["24"], - "Content-Type": ["text/html; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:07 GMT"], - "Etag": ["W/\"18-MS3VbhH7auHMzO0fUuNF6v14N/M\""], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "24" + ], + "Content-Type": [ + "text/html; charset=utf-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:07 GMT" + ], + "Etag": [ + "W/\"18-MS3VbhH7auHMzO0fUuNF6v14N/M\"" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 500 }, @@ -1835,29 +2772,52 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413287467017 }, - "trace": { "id": "d518b2c4d72cd2aaf1e39bad7ebcbdbb" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413287467017 + }, + "trace": { + "id": "d518b2c4d72cd2aaf1e39bad7ebcbdbb" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 35846 }, + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 35846 + }, "id": "c7a30c1b076907ec", "name": "GET /log-error", "result": "HTTP 5xx", "sampled": true, - "span_count": { "started": 0 }, + "span_count": { + "started": 0 + }, "type": "request" }, "url": { @@ -1868,57 +2828,90 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 35840, - "averageResponseTime": 35846, - "transactionsPerMinute": 0.25, - "impact": 1.7640550505645587 + } }, { - "name": "APIRestController#topProducts", + "key": { + "service.name": "opbeans-java", + "transaction.name": "APIRestController#topProducts" + }, + "averageResponseTime": 4825, + "transactionsPerMinute": 1.75, + "impact": 1.6450221426498186, "sample": { - "@timestamp": "2020-06-29T06:48:45.587Z", + "@timestamp": "2020-06-29T06:48:11.778Z", "agent": { "ephemeral_id": "222af346-6dd9-45ef-ac85-d86b67edd2de", "name": "java", "version": "1.17.1-SNAPSHOT" }, - "client": { "ip": "172.18.0.9" }, + "client": { + "ip": "172.18.0.9" + }, "container": { "id": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:46.770758Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:15.764351Z" + }, "host": { "architecture": "amd64", "hostname": "918ebbd99b4f", "ip": "172.18.0.6", "name": "918ebbd99b4f", - "os": { "platform": "Linux" } + "os": { + "platform": "Linux" + } }, "http": { "request": { "headers": { - "Accept": ["*/*"], - "Accept-Encoding": ["gzip, deflate"], - "Host": ["172.18.0.6:3000"], - "User-Agent": ["Python/3.7 aiohttp/3.3.2"] + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Host": [ + "172.18.0.6:3000" + ], + "User-Agent": [ + "Python/3.7 aiohttp/3.3.2" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "172.18.0.9" } + "socket": { + "encrypted": false, + "remote_address": "172.18.0.9" + } }, "response": { "finished": true, "headers": { - "Content-Type": ["application/json;charset=UTF-8"], - "Date": ["Mon, 29 Jun 2020 06:48:44 GMT"], - "Transfer-Encoding": ["chunked"] + "Content-Type": [ + "application/json;charset=UTF-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:11 GMT" + ], + "Transfer-Encoding": [ + "chunked" + ] }, "headers_sent": true, "status_code": 200 @@ -1933,29 +2926,56 @@ "version": "8.0.0", "version_major": 8 }, - "process": { "pid": 6, "ppid": 1, "title": "/opt/java/openjdk/bin/java" }, - "processor": { "event": "transaction", "name": "transaction" }, + "process": { + "pid": 6, + "ppid": 1, + "title": "/opt/java/openjdk/bin/java" + }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "Spring Web MVC", "version": "5.0.6.RELEASE" }, - "language": { "name": "Java", "version": "11.0.7" }, + "framework": { + "name": "Spring Web MVC", + "version": "5.0.6.RELEASE" + }, + "language": { + "name": "Java", + "version": "11.0.7" + }, "name": "opbeans-java", "node": { "name": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" }, - "runtime": { "name": "Java", "version": "11.0.7" }, + "runtime": { + "name": "Java", + "version": "11.0.7" + }, "version": "None" }, - "source": { "ip": "172.18.0.9" }, - "timestamp": { "us": 1593413325587007 }, - "trace": { "id": "4470d0cc076e22018e2dd258a25c8812" }, + "source": { + "ip": "172.18.0.9" + }, + "timestamp": { + "us": 1593413291778008 + }, + "trace": { + "id": "d65e9816f1f6db3961867f7b6d1d4e6a" + }, "transaction": { - "duration": { "us": 4070 }, - "id": "cb860b712121d0d8", + "duration": { + "us": 4168 + }, + "id": "a72f4bb8149ecdc5", "name": "APIRestController#topProducts", "result": "HTTP 2xx", "sampled": true, - "span_count": { "dropped": 0, "started": 1 }, + "span_count": { + "dropped": 0, + "started": 2 + }, "type": "request" }, "url": { @@ -1966,40 +2986,61 @@ "scheme": "http" }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "Python/3.7 aiohttp/3.3.2" } - }, - "p95": 7344, - "averageResponseTime": 4825, - "transactionsPerMinute": 1.75, - "impact": 1.6450221426498186 + } }, { - "name": "GET /api/products/top", + "key": { + "service.name": "opbeans-node", + "transaction.name": "GET /api/products/top" + }, + "averageResponseTime": 33097, + "transactionsPerMinute": 0.25, + "impact": 1.6060533780113861, "sample": { "@timestamp": "2020-06-29T06:48:01.200Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:02.734903Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:02.734903Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", "socket": { @@ -2009,12 +3050,24 @@ }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["2"], - "Content-Type": ["application/json; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:01 GMT"], - "Etag": ["W/\"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w\""], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "2" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:01 GMT" + ], + "Etag": [ + "W/\"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w\"" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 200 }, @@ -2044,29 +3097,52 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413281200133 }, - "trace": { "id": "195f32efeb6f91e2f71b6bc8bb74ae3a" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413281200133 + }, + "trace": { + "id": "195f32efeb6f91e2f71b6bc8bb74ae3a" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 33097 }, + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 33097 + }, "id": "22e72956dfc8967a", "name": "GET /api/products/top", "result": "HTTP 2xx", "sampled": true, - "span_count": { "started": 1 }, + "span_count": { + "started": 1 + }, "type": "request" }, "url": { @@ -2077,42 +3153,67 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 33024, - "averageResponseTime": 33097, - "transactionsPerMinute": 0.25, - "impact": 1.6060533780113861 + } }, { - "name": "GET /api/products", + "key": { + "service.name": "opbeans-node", + "transaction.name": "GET /api/products" + }, + "averageResponseTime": 6583, + "transactionsPerMinute": 1, + "impact": 1.2172278724376455, "sample": { - "@timestamp": "2020-06-29T06:48:23.477Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, + "@timestamp": "2020-06-29T06:48:21.475Z", + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:27.001228Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:26.996210Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", "socket": { @@ -2122,12 +3223,24 @@ }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["1023"], - "Content-Type": ["application/json; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:23 GMT"], - "Etag": ["W/\"3ff-VyOxcDApb+a/lnjkm9FeTOGSDrs\""], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "1023" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:21 GMT" + ], + "Etag": [ + "W/\"3ff-VyOxcDApb+a/lnjkm9FeTOGSDrs\"" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 200 }, @@ -2157,29 +3270,52 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413303477028 }, - "trace": { "id": "9f26158b9a3915577b3cccc17636ea01" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413301475015 + }, + "trace": { + "id": "389b26b16949c7f783223de4f14b788c" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 7150 }, - "id": "27ff4add22ac2e1b", + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 6775 + }, + "id": "d2d4088a0b104fb4", "name": "GET /api/products", "result": "HTTP 2xx", "sampled": true, - "span_count": { "started": 2 }, + "span_count": { + "started": 2 + }, "type": "request" }, "url": { @@ -2190,46 +3326,79 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 8160, - "averageResponseTime": 6583, - "transactionsPerMinute": 1, - "impact": 1.2172278724376455 + } }, { - "name": "POST /api", + "key": { + "service.name": "opbeans-node", + "transaction.name": "POST /api" + }, + "averageResponseTime": 20011, + "transactionsPerMinute": 0.25, + "impact": 0.853921734857215, "sample": { "@timestamp": "2020-06-29T06:48:25.478Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:27.005671Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:27.005671Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { - "body": { "original": "[REDACTED]" }, + "body": { + "original": "[REDACTED]" + }, "headers": { - "Accept": ["application/json"], - "Connection": ["close"], - "Content-Length": ["129"], - "Content-Type": ["application/json"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Accept": [ + "application/json" + ], + "Connection": [ + "close" + ], + "Content-Length": [ + "129" + ], + "Content-Type": [ + "application/json" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "post", "socket": { @@ -2239,12 +3408,24 @@ }, "response": { "headers": { - "Allow": ["GET"], - "Connection": ["close"], - "Content-Type": ["application/json;charset=UTF-8"], - "Date": ["Mon, 29 Jun 2020 06:48:25 GMT"], - "Transfer-Encoding": ["chunked"], - "X-Powered-By": ["Express"] + "Allow": [ + "GET" + ], + "Connection": [ + "close" + ], + "Content-Type": [ + "application/json;charset=UTF-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:25 GMT" + ], + "Transfer-Encoding": [ + "chunked" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 405 }, @@ -2274,29 +3455,52 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413305478010 }, - "trace": { "id": "4bd9027dd1e355ec742970e2d6333124" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413305478010 + }, + "trace": { + "id": "4bd9027dd1e355ec742970e2d6333124" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 20011 }, + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 20011 + }, "id": "94104435cf151478", "name": "POST /api", "result": "HTTP 4xx", "sampled": true, - "span_count": { "started": 1 }, + "span_count": { + "started": 1 + }, "type": "request" }, "url": { @@ -2307,42 +3511,67 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 19968, - "averageResponseTime": 20011, - "transactionsPerMinute": 0.25, - "impact": 0.853921734857215 + } }, { - "name": "GET /api/types/:id", + "key": { + "service.name": "opbeans-node", + "transaction.name": "GET /api/types/:id" + }, + "averageResponseTime": 8181, + "transactionsPerMinute": 0.5, + "impact": 0.6441916136689552, "sample": { - "@timestamp": "2020-06-29T06:48:12.972Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, + "@timestamp": "2020-06-29T06:47:53.928Z", + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:18.543436Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:47:55.472718Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", "socket": { @@ -2352,12 +3581,24 @@ }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["205"], - "Content-Type": ["application/json; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:12 GMT"], - "Etag": ["W/\"cd-pFMi1QOVY6YqWe+nwcbZVviCths\""], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "205" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:47:53 GMT" + ], + "Etag": [ + "W/\"cd-pFMi1QOVY6YqWe+nwcbZVviCths\"" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 200 }, @@ -2383,33 +3624,56 @@ "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", "ecosystem-workload.config.js" ], - "pid": 137, + "pid": 63, "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413292972011 }, - "trace": { "id": "aea65cef5f902dda5d8e38f9fb38864d" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413273928016 + }, + "trace": { + "id": "0becaafb422bfeb69e047bf7153aa469" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 6300 }, - "id": "a5bdbe43ac05fae2", + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 10062 + }, + "id": "0cee4574091bda3b", "name": "GET /api/types/:id", "result": "HTTP 2xx", "sampled": true, - "span_count": { "started": 2 }, + "span_count": { + "started": 2 + }, "type": "request" }, "url": { @@ -2420,42 +3684,67 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 10080, - "averageResponseTime": 8181, - "transactionsPerMinute": 0.5, - "impact": 0.6441916136689552 + } }, { - "name": "GET /api/stats", + "key": { + "service.name": "opbeans-node", + "transaction.name": "GET /api/stats" + }, + "averageResponseTime": 5098, + "transactionsPerMinute": 0.75, + "impact": 0.582807187955318, "sample": { - "@timestamp": "2020-06-29T06:48:39.451Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, + "@timestamp": "2020-06-29T06:48:34.949Z", + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:43.984824Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:39.479316Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", "socket": { @@ -2465,12 +3754,24 @@ }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["92"], - "Content-Type": ["application/json; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:39 GMT"], - "Etag": ["W/\"5c-6I+bqIiLxvkWuwBUnTxhBoK4lBk\""], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "92" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:34 GMT" + ], + "Etag": [ + "W/\"5c-6I+bqIiLxvkWuwBUnTxhBoK4lBk\"" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 200 }, @@ -2500,29 +3801,52 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413319451016 }, - "trace": { "id": "a05787cb03a0af0863fab5e05de942f1" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413314949017 + }, + "trace": { + "id": "616b3b77abd5534c61d6c0438469aee2" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 5050 }, - "id": "a7e004eba8f021ce", + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 5459 + }, + "id": "5b4971de59d2099d", "name": "GET /api/stats", "result": "HTTP 2xx", "sampled": true, - "span_count": { "started": 4 }, + "span_count": { + "started": 4 + }, "type": "request" }, "url": { @@ -2533,42 +3857,67 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 5440, - "averageResponseTime": 5098, - "transactionsPerMinute": 0.75, - "impact": 0.582807187955318 + } }, { - "name": "GET /api/orders", + "key": { + "service.name": "opbeans-node", + "transaction.name": "GET /api/orders" + }, + "averageResponseTime": 7624.5, + "transactionsPerMinute": 0.5, + "impact": 0.5802207655235637, "sample": { "@timestamp": "2020-06-29T06:48:35.450Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:39.483715Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:39.483715Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", "socket": { @@ -2578,12 +3927,24 @@ }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["2"], - "Content-Type": ["application/json; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:35 GMT"], - "Etag": ["W/\"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w\""], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "2" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:35 GMT" + ], + "Etag": [ + "W/\"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w\"" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 200 }, @@ -2613,29 +3974,52 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413315450014 }, - "trace": { "id": "2da70ccf10599b271f65273d169cde9f" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413315450014 + }, + "trace": { + "id": "2da70ccf10599b271f65273d169cde9f" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 8784 }, + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 8784 + }, "id": "a3f4a4f339758440", "name": "GET /api/orders", "result": "HTTP 2xx", "sampled": true, - "span_count": { "started": 2 }, + "span_count": { + "started": 2 + }, "type": "request" }, "url": { @@ -2646,42 +4030,67 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 8800, - "averageResponseTime": 7624.5, - "transactionsPerMinute": 0.5, - "impact": 0.5802207655235637 + } }, { - "name": "GET /api/orders/:id", + "key": { + "service.name": "opbeans-node", + "transaction.name": "GET /api/orders/:id" + }, + "averageResponseTime": 4749.666666666667, + "transactionsPerMinute": 0.75, + "impact": 0.5227447114845778, "sample": { "@timestamp": "2020-06-29T06:48:35.951Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:39.484133Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:39.484133Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", "socket": { @@ -2691,10 +4100,18 @@ }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["0"], - "Date": ["Mon, 29 Jun 2020 06:48:35 GMT"], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "0" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:35 GMT" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 404 }, @@ -2724,29 +4141,52 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413315951017 }, - "trace": { "id": "95979caa80e6622cbbb2d308800c3016" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413315951017 + }, + "trace": { + "id": "95979caa80e6622cbbb2d308800c3016" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 3210 }, + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 3210 + }, "id": "30344988dace0b43", "name": "GET /api/orders/:id", "result": "HTTP 4xx", "sampled": true, - "span_count": { "started": 1 }, + "span_count": { + "started": 1 + }, "type": "request" }, "url": { @@ -2757,57 +4197,90 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 7184, - "averageResponseTime": 4749.666666666667, - "transactionsPerMinute": 0.75, - "impact": 0.5227447114845778 + } }, { - "name": "APIRestController#products", + "key": { + "service.name": "opbeans-java", + "transaction.name": "APIRestController#products" + }, + "averageResponseTime": 6787, + "transactionsPerMinute": 0.5, + "impact": 0.4839483750082622, "sample": { - "@timestamp": "2020-06-29T06:48:27.824Z", + "@timestamp": "2020-06-29T06:48:13.595Z", "agent": { "ephemeral_id": "222af346-6dd9-45ef-ac85-d86b67edd2de", "name": "java", "version": "1.17.1-SNAPSHOT" }, - "client": { "ip": "172.18.0.9" }, + "client": { + "ip": "172.18.0.9" + }, "container": { "id": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:36.087688Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:15.755614Z" + }, "host": { "architecture": "amd64", "hostname": "918ebbd99b4f", "ip": "172.18.0.6", "name": "918ebbd99b4f", - "os": { "platform": "Linux" } + "os": { + "platform": "Linux" + } }, "http": { "request": { "headers": { - "Accept": ["*/*"], - "Accept-Encoding": ["gzip, deflate"], - "Host": ["172.18.0.6:3000"], - "User-Agent": ["Python/3.7 aiohttp/3.3.2"] + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Host": [ + "172.18.0.6:3000" + ], + "User-Agent": [ + "Python/3.7 aiohttp/3.3.2" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "172.18.0.9" } + "socket": { + "encrypted": false, + "remote_address": "172.18.0.9" + } }, "response": { "finished": true, "headers": { - "Content-Type": ["application/json;charset=UTF-8"], - "Date": ["Mon, 29 Jun 2020 06:48:27 GMT"], - "Transfer-Encoding": ["chunked"] + "Content-Type": [ + "application/json;charset=UTF-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:12 GMT" + ], + "Transfer-Encoding": [ + "chunked" + ] }, "headers_sent": true, "status_code": 200 @@ -2822,29 +4295,56 @@ "version": "8.0.0", "version_major": 8 }, - "process": { "pid": 6, "ppid": 1, "title": "/opt/java/openjdk/bin/java" }, - "processor": { "event": "transaction", "name": "transaction" }, + "process": { + "pid": 6, + "ppid": 1, + "title": "/opt/java/openjdk/bin/java" + }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "Spring Web MVC", "version": "5.0.6.RELEASE" }, - "language": { "name": "Java", "version": "11.0.7" }, + "framework": { + "name": "Spring Web MVC", + "version": "5.0.6.RELEASE" + }, + "language": { + "name": "Java", + "version": "11.0.7" + }, "name": "opbeans-java", "node": { "name": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" }, - "runtime": { "name": "Java", "version": "11.0.7" }, + "runtime": { + "name": "Java", + "version": "11.0.7" + }, "version": "None" }, - "source": { "ip": "172.18.0.9" }, - "timestamp": { "us": 1593413307824008 }, - "trace": { "id": "a6adb17bd5a5d1c0eabb9f36cb626dd5" }, + "source": { + "ip": "172.18.0.9" + }, + "timestamp": { + "us": 1593413293595007 + }, + "trace": { + "id": "8519b6c3dbc32a0582228506526e1d74" + }, "transaction": { - "duration": { "us": 5645 }, - "id": "df3e726eaa003d96", + "duration": { + "us": 7929 + }, + "id": "b0354de660cd3698", "name": "APIRestController#products", "result": "HTTP 2xx", "sampled": true, - "span_count": { "dropped": 0, "started": 3 }, + "span_count": { + "dropped": 0, + "started": 3 + }, "type": "request" }, "url": { @@ -2855,40 +4355,61 @@ "scheme": "http" }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "Python/3.7 aiohttp/3.3.2" } - }, - "p95": 7904, - "averageResponseTime": 6787, - "transactionsPerMinute": 0.5, - "impact": 0.4839483750082622 + } }, { - "name": "GET /api/products/:id/customers", + "key": { + "service.name": "opbeans-node", + "transaction.name": "GET /api/products/:id/customers" + }, + "averageResponseTime": 4757, + "transactionsPerMinute": 0.5, + "impact": 0.25059559560997896, "sample": { - "@timestamp": "2020-06-29T06:48:41.956Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, + "@timestamp": "2020-06-29T06:48:22.977Z", + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:43.994692Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:27.000765Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", "socket": { @@ -2898,12 +4419,24 @@ }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["2"], - "Content-Type": ["application/json; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:41 GMT"], - "Etag": ["W/\"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w\""], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "2" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:22 GMT" + ], + "Etag": [ + "W/\"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w\"" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 200 }, @@ -2933,90 +4466,146 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413321956016 }, - "trace": { "id": "f735ac5fca8f83eebc748f4a2e009e61" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413302977008 + }, + "trace": { + "id": "da8f22fe652ccb6680b3029ab6efd284" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 3896 }, - "id": "b24ce95c855f83a4", + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 5618 + }, + "id": "bc51c1523afaf57a", "name": "GET /api/products/:id/customers", "result": "HTTP 2xx", "sampled": true, - "span_count": { "started": 1 }, + "span_count": { + "started": 1 + }, "type": "request" }, "url": { "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/products/5/customers", - "original": "/api/products/5/customers", - "path": "/api/products/5/customers", + "full": "http://opbeans-node:3000/api/products/3/customers", + "original": "/api/products/3/customers", + "path": "/api/products/3/customers", "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 5616, - "averageResponseTime": 4757, - "transactionsPerMinute": 0.5, - "impact": 0.25059559560997896 + } }, { - "name": "APIRestController#product", + "key": { + "service.name": "opbeans-java", + "transaction.name": "APIRestController#product" + }, + "averageResponseTime": 4713.5, + "transactionsPerMinute": 0.5, + "impact": 0.24559517890858723, "sample": { - "@timestamp": "2020-06-29T06:48:41.941Z", + "@timestamp": "2020-06-29T06:48:36.383Z", "agent": { "ephemeral_id": "222af346-6dd9-45ef-ac85-d86b67edd2de", "name": "java", "version": "1.17.1-SNAPSHOT" }, - "client": { "ip": "172.18.0.9" }, + "client": { + "ip": "172.18.0.9" + }, "container": { "id": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:46.709268Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:46.666467Z" + }, "host": { "architecture": "amd64", "hostname": "918ebbd99b4f", "ip": "172.18.0.6", "name": "918ebbd99b4f", - "os": { "platform": "Linux" } + "os": { + "platform": "Linux" + } }, "http": { "request": { "headers": { - "Accept": ["*/*"], - "Accept-Encoding": ["gzip, deflate"], - "Host": ["172.18.0.6:3000"], - "User-Agent": ["Python/3.7 aiohttp/3.3.2"] + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Host": [ + "172.18.0.6:3000" + ], + "User-Agent": [ + "Python/3.7 aiohttp/3.3.2" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "172.18.0.9" } + "socket": { + "encrypted": false, + "remote_address": "172.18.0.9" + } }, "response": { "finished": true, "headers": { - "Content-Type": ["application/json;charset=UTF-8"], - "Date": ["Mon, 29 Jun 2020 06:48:41 GMT"], - "Transfer-Encoding": ["chunked"] + "Content-Type": [ + "application/json;charset=UTF-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:36 GMT" + ], + "Transfer-Encoding": [ + "chunked" + ] }, "headers_sent": true, "status_code": 200 @@ -3031,81 +4620,131 @@ "version": "8.0.0", "version_major": 8 }, - "process": { "pid": 6, "ppid": 1, "title": "/opt/java/openjdk/bin/java" }, - "processor": { "event": "transaction", "name": "transaction" }, + "process": { + "pid": 6, + "ppid": 1, + "title": "/opt/java/openjdk/bin/java" + }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "Spring Web MVC", "version": "5.0.6.RELEASE" }, - "language": { "name": "Java", "version": "11.0.7" }, + "framework": { + "name": "Spring Web MVC", + "version": "5.0.6.RELEASE" + }, + "language": { + "name": "Java", + "version": "11.0.7" + }, "name": "opbeans-java", "node": { "name": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" }, - "runtime": { "name": "Java", "version": "11.0.7" }, + "runtime": { + "name": "Java", + "version": "11.0.7" + }, "version": "None" }, - "source": { "ip": "172.18.0.9" }, - "timestamp": { "us": 1593413321941007 }, - "trace": { "id": "88a2b9ca970cdd38dfa1b5646d26b897" }, + "source": { + "ip": "172.18.0.9" + }, + "timestamp": { + "us": 1593413316383008 + }, + "trace": { + "id": "386b450aef87fc079b20136eda542af1" + }, "transaction": { - "duration": { "us": 4539 }, - "id": "24ee0e4812cfed62", + "duration": { + "us": 4888 + }, + "id": "5a4aa02158b5658c", "name": "APIRestController#product", "result": "HTTP 2xx", "sampled": true, - "span_count": { "dropped": 0, "started": 2 }, + "span_count": { + "dropped": 0, + "started": 3 + }, "type": "request" }, "url": { "domain": "172.18.0.6", - "full": "http://172.18.0.6:3000/api/products/4", - "path": "/api/products/4", + "full": "http://172.18.0.6:3000/api/products/1", + "path": "/api/products/1", "port": 3000, "scheme": "http" }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "Python/3.7 aiohttp/3.3.2" } - }, - "p95": 4864, - "averageResponseTime": 4713.5, - "transactionsPerMinute": 0.5, - "impact": 0.24559517890858723 + } }, { - "name": "APIRestController#order", + "key": { + "service.name": "opbeans-java", + "transaction.name": "APIRestController#order" + }, + "averageResponseTime": 3392.5, + "transactionsPerMinute": 0.5, + "impact": 0.09374344413758617, "sample": { - "@timestamp": "2020-06-29T06:48:33.314Z", + "@timestamp": "2020-06-29T06:48:07.416Z", "agent": { "ephemeral_id": "222af346-6dd9-45ef-ac85-d86b67edd2de", "name": "java", "version": "1.17.1-SNAPSHOT" }, - "client": { "ip": "172.18.0.9" }, + "client": { + "ip": "172.18.0.9" + }, "container": { "id": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:36.137777Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:15.534378Z" + }, "host": { "architecture": "amd64", "hostname": "918ebbd99b4f", "ip": "172.18.0.6", "name": "918ebbd99b4f", - "os": { "platform": "Linux" } + "os": { + "platform": "Linux" + } }, "http": { "request": { "headers": { - "Accept": ["*/*"], - "Accept-Encoding": ["gzip, deflate"], - "Host": ["172.18.0.6:3000"], - "User-Agent": ["Python/3.7 aiohttp/3.3.2"] + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Host": [ + "172.18.0.6:3000" + ], + "User-Agent": [ + "Python/3.7 aiohttp/3.3.2" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "172.18.0.9" } + "socket": { + "encrypted": false, + "remote_address": "172.18.0.9" + } }, "response": { "finished": true, @@ -3122,88 +4761,144 @@ "version": "8.0.0", "version_major": 8 }, - "process": { "pid": 6, "ppid": 1, "title": "/opt/java/openjdk/bin/java" }, - "processor": { "event": "transaction", "name": "transaction" }, + "process": { + "pid": 6, + "ppid": 1, + "title": "/opt/java/openjdk/bin/java" + }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "Spring Web MVC", "version": "5.0.6.RELEASE" }, - "language": { "name": "Java", "version": "11.0.7" }, + "framework": { + "name": "Spring Web MVC", + "version": "5.0.6.RELEASE" + }, + "language": { + "name": "Java", + "version": "11.0.7" + }, "name": "opbeans-java", "node": { "name": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" }, - "runtime": { "name": "Java", "version": "11.0.7" }, + "runtime": { + "name": "Java", + "version": "11.0.7" + }, "version": "None" }, - "source": { "ip": "172.18.0.9" }, - "timestamp": { "us": 1593413313314007 }, - "trace": { "id": "aaf67f944393124080d1e4de804dc6eb" }, + "source": { + "ip": "172.18.0.9" + }, + "timestamp": { + "us": 1593413287416007 + }, + "trace": { + "id": "25c46380df3d44a192ed07279a08b329" + }, "transaction": { - "duration": { "us": 2503 }, - "id": "f7f9f5e0f8a3a0d4", + "duration": { + "us": 4282 + }, + "id": "d4d5b23c685d2ee5", "name": "APIRestController#order", "result": "HTTP 2xx", "sampled": true, - "span_count": { "dropped": 0, "started": 1 }, + "span_count": { + "dropped": 0, + "started": 1 + }, "type": "request" }, "url": { "domain": "172.18.0.6", - "full": "http://172.18.0.6:3000/api/orders/906", - "path": "/api/orders/906", + "full": "http://172.18.0.6:3000/api/orders/391", + "path": "/api/orders/391", "port": 3000, "scheme": "http" }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "Python/3.7 aiohttp/3.3.2" } - }, - "p95": 4272, - "averageResponseTime": 3392.5, - "transactionsPerMinute": 0.5, - "impact": 0.09374344413758617 + } }, { - "name": "APIRestController#orders", + "key": { + "service.name": "opbeans-java", + "transaction.name": "APIRestController#orders" + }, + "averageResponseTime": 3147, + "transactionsPerMinute": 0.5, + "impact": 0.06552270160444405, "sample": { - "@timestamp": "2020-06-29T06:48:39.500Z", + "@timestamp": "2020-06-29T06:48:16.028Z", "agent": { "ephemeral_id": "222af346-6dd9-45ef-ac85-d86b67edd2de", "name": "java", "version": "1.17.1-SNAPSHOT" }, - "client": { "ip": "172.18.0.9" }, + "client": { + "ip": "172.18.0.9" + }, "container": { "id": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:46.706280Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:25.800962Z" + }, "host": { "architecture": "amd64", "hostname": "918ebbd99b4f", "ip": "172.18.0.6", "name": "918ebbd99b4f", - "os": { "platform": "Linux" } + "os": { + "platform": "Linux" + } }, "http": { "request": { "headers": { - "Accept": ["*/*"], - "Accept-Encoding": ["gzip, deflate"], - "Host": ["172.18.0.6:3000"], - "User-Agent": ["Python/3.7 aiohttp/3.3.2"] + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Host": [ + "172.18.0.6:3000" + ], + "User-Agent": [ + "Python/3.7 aiohttp/3.3.2" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "172.18.0.9" } + "socket": { + "encrypted": false, + "remote_address": "172.18.0.9" + } }, "response": { "finished": true, "headers": { - "Content-Type": ["application/json;charset=UTF-8"], - "Date": ["Mon, 29 Jun 2020 06:48:38 GMT"], - "Transfer-Encoding": ["chunked"] + "Content-Type": [ + "application/json;charset=UTF-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:15 GMT" + ], + "Transfer-Encoding": [ + "chunked" + ] }, "headers_sent": true, "status_code": 200 @@ -3218,29 +4913,56 @@ "version": "8.0.0", "version_major": 8 }, - "process": { "pid": 6, "ppid": 1, "title": "/opt/java/openjdk/bin/java" }, - "processor": { "event": "transaction", "name": "transaction" }, + "process": { + "pid": 6, + "ppid": 1, + "title": "/opt/java/openjdk/bin/java" + }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "Spring Web MVC", "version": "5.0.6.RELEASE" }, - "language": { "name": "Java", "version": "11.0.7" }, + "framework": { + "name": "Spring Web MVC", + "version": "5.0.6.RELEASE" + }, + "language": { + "name": "Java", + "version": "11.0.7" + }, "name": "opbeans-java", "node": { "name": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" }, - "runtime": { "name": "Java", "version": "11.0.7" }, + "runtime": { + "name": "Java", + "version": "11.0.7" + }, "version": "None" }, - "source": { "ip": "172.18.0.9" }, - "timestamp": { "us": 1593413319500008 }, - "trace": { "id": "f89b02f09a2e7a7f2cc3478f53d4a495" }, + "source": { + "ip": "172.18.0.9" + }, + "timestamp": { + "us": 1593413296028008 + }, + "trace": { + "id": "4110227ecacbccf79894165ae5df932d" + }, "transaction": { - "duration": { "us": 3391 }, - "id": "41c8c4300ee2ccda", + "duration": { + "us": 2903 + }, + "id": "8e3732f0f0da942b", "name": "APIRestController#orders", "result": "HTTP 2xx", "sampled": true, - "span_count": { "dropped": 0, "started": 2 }, + "span_count": { + "dropped": 0, + "started": 1 + }, "type": "request" }, "url": { @@ -3251,40 +4973,61 @@ "scheme": "http" }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "Python/3.7 aiohttp/3.3.2" } - }, - "p95": 3376, - "averageResponseTime": 3147, - "transactionsPerMinute": 0.5, - "impact": 0.06552270160444405 + } }, { - "name": "GET /throw-error", + "key": { + "service.name": "opbeans-node", + "transaction.name": "GET /throw-error" + }, + "averageResponseTime": 2577, + "transactionsPerMinute": 0.5, + "impact": 0, "sample": { - "@timestamp": "2020-06-29T06:48:42.954Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, + "@timestamp": "2020-06-29T06:48:19.975Z", + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:43.996435Z" }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:21.012520Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", "socket": { @@ -3294,13 +5037,27 @@ }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["148"], - "Content-Security-Policy": ["default-src 'none'"], - "Content-Type": ["text/html; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:42 GMT"], - "X-Content-Type-Options": ["nosniff"], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "148" + ], + "Content-Security-Policy": [ + "default-src 'none'" + ], + "Content-Type": [ + "text/html; charset=utf-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:19 GMT" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 500 }, @@ -3330,29 +5087,52 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413322954016 }, - "trace": { "id": "9d5aee7443a43db9820f622a10dfac6e" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413299975019 + }, + "trace": { + "id": "106f3a55b0b0ea327d1bbe4be66c3bcc" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 1928 }, - "id": "8e6fc8c3f99e8fc9", + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 3226 + }, + "id": "247b9141552a9e73", "name": "GET /throw-error", "result": "HTTP 5xx", "sampled": true, - "span_count": { "started": 0 }, + "span_count": { + "started": 0 + }, "type": "request" }, "url": { @@ -3363,16 +5143,18 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 3224, - "averageResponseTime": 2577, - "transactionsPerMinute": 0.5, - "impact": 0 + } } ] diff --git a/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts b/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts index e96cb20a68fda..b4a037436adb8 100644 --- a/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts +++ b/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; +import { sortBy, omit } from 'lodash'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import expectTopTraces from './expectation/top_traces.expectation.json'; @@ -46,8 +47,28 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.body.items.length).to.be(33); }); - it('returns the correct buckets and samples', async () => { - expect(response.body.items).to.eql(expectTopTraces); + it('returns the correct buckets', async () => { + const responseWithoutSamples = sortBy( + response.body.items.map((item: any) => omit(item, 'sample')), + 'impact' + ); + + const expectedTracesWithoutSamples = sortBy( + expectTopTraces.map((item: any) => omit(item, 'sample')), + 'impact' + ); + + expect(responseWithoutSamples).to.eql(expectedTracesWithoutSamples); + }); + + it('returns a sample', async () => { + // sample should provide enough information to deeplink to a transaction detail page + response.body.items.forEach((item: any) => { + expect(item.sample.trace.id).to.be.an('string'); + expect(item.sample.transaction.id).to.be.an('string'); + expect(item.sample.service.name).to.be(item.key['service.name']); + expect(item.sample.transaction.name).to.be(item.key['transaction.name']); + }); }); }); }); diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/top_transaction_groups.json b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/top_transaction_groups.json index 7d314e75e4d1d..29c55d4ef1b5c 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/top_transaction_groups.json +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/top_transaction_groups.json @@ -1,38 +1,86 @@ { "items": [ { - "name": "GET /api", + "key": "GET /api", + "averageResponseTime": 51175.73170731707, + "transactionsPerMinute": 10.25, + "impact": 100, + "p95": 259040, "sample": { - "@timestamp": "2020-06-29T06:48:41.454Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, - "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:43.992834Z" }, + "@timestamp": "2020-06-29T06:48:06.862Z", + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.8" + }, + "container": { + "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:08.305742Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Connection": [ + "keep-alive" + ], + "Host": [ + "opbeans-node:3000" + ], + "Referer": [ + "http://opbeans-node:3000/dashboard" + ], + "Traceparent": [ + "00-ca86ffcac7753ec8733933bd8fd45d11-5dcb98c9c9021cfc-01" + ], + "User-Agent": [ + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/79.0.3945.0 Safari/537.36" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "::ffff:172.18.0.7" } + "socket": { + "encrypted": false, + "remote_address": "::ffff:172.18.0.8" + } }, "response": { "headers": { - "Connection": ["close"], - "Content-Type": ["application/json;charset=UTF-8"], - "Date": ["Mon, 29 Jun 2020 06:48:41 GMT"], - "Transfer-Encoding": ["chunked"], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Type": [ + "application/json;charset=UTF-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:06 GMT" + ], + "Transfer-Encoding": [ + "chunked" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 200 }, @@ -52,6 +100,9 @@ "version": "8.0.0", "version_major": 8 }, + "parent": { + "id": "5dcb98c9c9021cfc" + }, "process": { "args": [ "/usr/local/bin/node", @@ -62,87 +113,164 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", - "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "node": { + "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413321454009 }, - "trace": { "id": "0507830eeff93f7bf1a354e4031097b3" }, + "source": { + "ip": "172.18.0.8" + }, + "timestamp": { + "us": 1593413286862021 + }, + "trace": { + "id": "ca86ffcac7753ec8733933bd8fd45d11" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 8334 }, - "id": "878250a8b937445d", + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 15738 + }, + "id": "c95371db21c6f407", "name": "GET /api", "result": "HTTP 2xx", "sampled": true, - "span_count": { "started": 1 }, + "span_count": { + "started": 1 + }, "type": "request" }, "url": { "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/products/6", - "original": "/api/products/6", - "path": "/api/products/6", + "full": "http://opbeans-node:3000/api/products/top", + "original": "/api/products/top", + "path": "/api/products/top", "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, - "name": "Other", - "original": "workload/2.4.3" + "device": { + "name": "Other" + }, + "name": "HeadlessChrome", + "original": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/79.0.3945.0 Safari/537.36", + "os": { + "name": "Linux" + }, + "version": "79.0.3945" } - }, - "p95": 259040, - "averageResponseTime": 51175.73170731707, - "transactionsPerMinute": 10.25, - "impact": 100 + } }, { - "name": "POST /api/orders", + "key": "POST /api/orders", + "averageResponseTime": 270684, + "transactionsPerMinute": 0.25, + "impact": 12.686265169840583, + "p95": 270336, "sample": { "@timestamp": "2020-06-29T06:48:39.953Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, - "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:43.991549Z" }, + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, + "container": { + "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:43.991549Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { - "body": { "original": "[REDACTED]" }, + "body": { + "original": "[REDACTED]" + }, "headers": { - "Accept": ["application/json"], - "Connection": ["close"], - "Content-Length": ["129"], - "Content-Type": ["application/json"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Accept": [ + "application/json" + ], + "Connection": [ + "close" + ], + "Content-Length": [ + "129" + ], + "Content-Type": [ + "application/json" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "post", - "socket": { "encrypted": false, "remote_address": "::ffff:172.18.0.7" } + "socket": { + "encrypted": false, + "remote_address": "::ffff:172.18.0.7" + } }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["13"], - "Content-Type": ["application/json; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:40 GMT"], - "Etag": ["W/\"d-eEOWU4Cnr5DZ23ErRUeYu9oOIks\""], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "13" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:40 GMT" + ], + "Etag": [ + "W/\"d-eEOWU4Cnr5DZ23ErRUeYu9oOIks\"" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 200 }, @@ -172,27 +300,52 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", - "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "node": { + "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413319953033 }, - "trace": { "id": "52b8fda5f6df745b990740ba18378620" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413319953033 + }, + "trace": { + "id": "52b8fda5f6df745b990740ba18378620" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 270684 }, + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 270684 + }, "id": "a3afc2a112e9c893", "name": "POST /api/orders", "result": "HTTP 2xx", "sampled": true, - "span_count": { "started": 16 }, + "span_count": { + "started": 16 + }, "type": "request" }, "url": { @@ -203,52 +356,92 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 270336, - "averageResponseTime": 270684, - "transactionsPerMinute": 0.25, - "impact": 12.686265169840583 + } }, { - "name": "GET /api/customers", + "key": "GET /api/customers", + "averageResponseTime": 16896.8, + "transactionsPerMinute": 1.25, + "impact": 3.790160870423129, + "p95": 26432, "sample": { - "@timestamp": "2020-06-29T06:48:37.952Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, - "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:39.492402Z" }, + "@timestamp": "2020-06-29T06:48:28.444Z", + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, + "container": { + "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:29.982737Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "::ffff:172.18.0.7" } + "socket": { + "encrypted": false, + "remote_address": "::ffff:172.18.0.7" + } }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["186769"], - "Content-Type": ["application/json; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:37 GMT"], - "Etag": ["W/\"2d991-yG3J8W/roH7fSxXTudZrO27Ax9s\""], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "186769" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:28 GMT" + ], + "Etag": [ + "W/\"2d991-yG3J8W/roH7fSxXTudZrO27Ax9s\"" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 200 }, @@ -278,27 +471,52 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", - "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "node": { + "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413317952016 }, - "trace": { "id": "5d99327edae38ed735e8d7a6028cf719" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413308444015 + }, + "trace": { + "id": "792fb0b00256164e88b277ec40b65e14" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 16824 }, - "id": "071808429ec9d00b", + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 26471 + }, + "id": "6c1f848752563d2b", "name": "GET /api/customers", "result": "HTTP 2xx", "sampled": true, - "span_count": { "started": 2 }, + "span_count": { + "started": 2 + }, "type": "request" }, "url": { @@ -309,52 +527,92 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 26432, - "averageResponseTime": 16896.8, - "transactionsPerMinute": 1.25, - "impact": 3.790160870423129 + } }, { - "name": "GET /log-message", + "key": "GET /log-message", + "averageResponseTime": 32667.5, + "transactionsPerMinute": 0.5, + "impact": 2.875276331059301, + "p95": 38528, "sample": { - "@timestamp": "2020-06-29T06:48:28.944Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, - "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:39.370695Z" }, + "@timestamp": "2020-06-29T06:48:25.944Z", + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, + "container": { + "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:29.976822Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "::ffff:172.18.0.7" } + "socket": { + "encrypted": false, + "remote_address": "::ffff:172.18.0.7" + } }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["24"], - "Content-Type": ["text/html; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:28 GMT"], - "Etag": ["W/\"18-MS3VbhH7auHMzO0fUuNF6v14N/M\""], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "24" + ], + "Content-Type": [ + "text/html; charset=utf-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:25 GMT" + ], + "Etag": [ + "W/\"18-MS3VbhH7auHMzO0fUuNF6v14N/M\"" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 500 }, @@ -384,27 +642,52 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", - "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "node": { + "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413308944016 }, - "trace": { "id": "afabe4cb397616f5ec35a2f3da49ba62" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413305944023 + }, + "trace": { + "id": "cd2ad726ad164d701c5d3103cbab0c81" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 26788 }, - "id": "cc8c4261387507cf", + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 38547 + }, + "id": "9e41667eb64dea55", "name": "GET /log-message", "result": "HTTP 5xx", "sampled": true, - "span_count": { "started": 0 }, + "span_count": { + "started": 0 + }, "type": "request" }, "url": { @@ -415,51 +698,89 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 38528, - "averageResponseTime": 32667.5, - "transactionsPerMinute": 0.5, - "impact": 2.875276331059301 + } }, { - "name": "GET /*", + "key": "GET /*", + "averageResponseTime": 3262.95, + "transactionsPerMinute": 5, + "impact": 2.8716452680799467, + "p95": 4472, "sample": { - "@timestamp": "2020-06-29T06:48:42.454Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, - "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:43.995202Z" }, + "@timestamp": "2020-06-29T06:48:25.064Z", + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, + "container": { + "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:27.005197Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "Wget" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "::ffff:172.18.0.7" } + "socket": { + "encrypted": false, + "remote_address": "::ffff:172.18.0.7" + } }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["813"], - "Content-Type": ["text/html"], - "Date": ["Mon, 29 Jun 2020 06:48:42 GMT"], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "813" + ], + "Content-Type": [ + "text/html" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:25 GMT" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 200 }, @@ -479,7 +800,9 @@ "version": "8.0.0", "version_major": 8 }, - "parent": { "id": "5baa6c3bedc93f9d" }, + "parent": { + "id": "f673ceaf4583f0f2" + }, "process": { "args": [ "/usr/local/bin/node", @@ -490,27 +813,52 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", - "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "node": { + "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413322454015 }, - "trace": { "id": "022b01256b291a4c417199d91ec8755f" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413305064023 + }, + "trace": { + "id": "30c12f4d8ef77a5be1b4464e5d2235bc" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 1737 }, - "id": "eff3e45e0d9529d9", + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 3004 + }, + "id": "18a00dfdb919a978", "name": "GET /*", "result": "HTTP 2xx", "sampled": true, - "span_count": { "started": 0 }, + "span_count": { + "started": 0 + }, "type": "request" }, "url": { @@ -521,59 +869,104 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, - "name": "Other", - "original": "workload/2.4.3" + "device": { + "name": "Other" + }, + "name": "Wget", + "original": "Wget" } - }, - "p95": 4472, - "averageResponseTime": 3262.95, - "transactionsPerMinute": 5, - "impact": 2.8716452680799467 + } }, { - "name": "GET /api/orders", + "key": "GET /api/orders", + "averageResponseTime": 7615.625, + "transactionsPerMinute": 2, + "impact": 2.6645791239678345, + "p95": 11616, "sample": { - "@timestamp": "2020-06-29T06:48:40.106Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.6" }, - "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:43.988104Z" }, + "@timestamp": "2020-06-29T06:48:28.782Z", + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.8" + }, + "container": { + "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:29.983252Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { "Accept": [ - "text/plain, application/json, application/x-jackson-smile, application/cbor, application/*+json, */*" + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Connection": [ + "keep-alive" ], - "Connection": ["keep-alive"], - "Elastic-Apm-Traceparent": [ - "00-90bd7780b32cc51a7f4c200b1e0c170f-5ff346d602ce27b0-01" + "Host": [ + "opbeans-node:3000" ], - "Host": ["opbeans-node:3000"], - "Traceparent": ["00-90bd7780b32cc51a7f4c200b1e0c170f-5ff346d602ce27b0-01"], - "User-Agent": ["Java/11.0.7"] + "Referer": [ + "http://opbeans-node:3000/orders" + ], + "Traceparent": [ + "00-978b56807e0b7a27cbc41a0dfb665f47-3358a24e09e23561-01" + ], + "User-Agent": [ + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/79.0.3945.0 Safari/537.36" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "::ffff:172.18.0.6" } + "socket": { + "encrypted": false, + "remote_address": "::ffff:172.18.0.8" + } }, "response": { "headers": { - "Connection": ["keep-alive"], - "Content-Length": ["2"], - "Content-Type": ["application/json; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:40 GMT"], - "Etag": ["W/\"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w\""], - "X-Powered-By": ["Express"] + "Connection": [ + "keep-alive" + ], + "Content-Length": [ + "2" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:28 GMT" + ], + "Etag": [ + "W/\"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w\"" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 200 }, @@ -593,7 +986,9 @@ "version": "8.0.0", "version_major": 8 }, - "parent": { "id": "5ff346d602ce27b0" }, + "parent": { + "id": "3358a24e09e23561" + }, "process": { "args": [ "/usr/local/bin/node", @@ -604,27 +999,52 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", - "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "node": { + "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.6" }, - "timestamp": { "us": 1593413320106015 }, - "trace": { "id": "90bd7780b32cc51a7f4c200b1e0c170f" }, + "source": { + "ip": "172.18.0.8" + }, + "timestamp": { + "us": 1593413308782015 + }, + "trace": { + "id": "978b56807e0b7a27cbc41a0dfb665f47" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 7424 }, - "id": "f3dd00d12c594cba", + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 7134 + }, + "id": "a6d8f3c5c98903e1", "name": "GET /api/orders", "result": "HTTP 2xx", "sampled": true, - "span_count": { "started": 2 }, + "span_count": { + "started": 2 + }, "type": "request" }, "url": { @@ -635,60 +1055,96 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Spider" }, - "name": "Java", - "original": "Java/11.0.7", - "version": "0.7." + "device": { + "name": "Other" + }, + "name": "HeadlessChrome", + "original": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/79.0.3945.0 Safari/537.36", + "os": { + "name": "Linux" + }, + "version": "79.0.3945" } - }, - "p95": 11616, - "averageResponseTime": 7615.625, - "transactionsPerMinute": 2, - "impact": 2.6645791239678345 + } }, { - "name": "GET /api/products", + "key": "GET /api/products", + "averageResponseTime": 8585, + "transactionsPerMinute": 1.75, + "impact": 2.624924094061731, + "p95": 22112, "sample": { - "@timestamp": "2020-06-29T06:48:27.452Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.6" }, - "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:29.978463Z" }, + "@timestamp": "2020-06-29T06:48:21.475Z", + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, + "container": { + "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:26.996210Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Accept": [ - "text/plain, application/json, application/x-jackson-smile, application/cbor, application/*+json, */*" + "Connection": [ + "close" ], - "Connection": ["keep-alive"], - "Elastic-Apm-Traceparent": [ - "00-27b168a328e0fd442377de8eaa0bf582-2c86873dedb66c2c-01" + "Host": [ + "opbeans-node:3000" ], - "Host": ["opbeans-node:3000"], - "Traceparent": ["00-27b168a328e0fd442377de8eaa0bf582-2c86873dedb66c2c-01"], - "User-Agent": ["Java/11.0.7"] + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "::ffff:172.18.0.6" } + "socket": { + "encrypted": false, + "remote_address": "::ffff:172.18.0.7" + } }, "response": { "headers": { - "Connection": ["keep-alive"], - "Content-Length": ["1023"], - "Content-Type": ["application/json; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:27 GMT"], - "Etag": ["W/\"3ff-VyOxcDApb+a/lnjkm9FeTOGSDrs\""], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "1023" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:21 GMT" + ], + "Etag": [ + "W/\"3ff-VyOxcDApb+a/lnjkm9FeTOGSDrs\"" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 200 }, @@ -708,7 +1164,6 @@ "version": "8.0.0", "version_major": 8 }, - "parent": { "id": "2c86873dedb66c2c" }, "process": { "args": [ "/usr/local/bin/node", @@ -719,27 +1174,52 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", - "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "node": { + "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.6" }, - "timestamp": { "us": 1593413307452013 }, - "trace": { "id": "27b168a328e0fd442377de8eaa0bf582" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413301475015 + }, + "trace": { + "id": "389b26b16949c7f783223de4f14b788c" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 4292 }, - "id": "141ecc2dfd55eeea", + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 6775 + }, + "id": "d2d4088a0b104fb4", "name": "GET /api/products", "result": "HTTP 2xx", "sampled": true, - "span_count": { "started": 2 }, + "span_count": { + "started": 2 + }, "type": "request" }, "url": { @@ -750,53 +1230,92 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Spider" }, - "name": "Java", - "original": "Java/11.0.7", - "version": "0.7." + "device": { + "name": "Other" + }, + "name": "Other", + "original": "workload/2.4.3" } - }, - "p95": 22112, - "averageResponseTime": 8585, - "transactionsPerMinute": 1.75, - "impact": 2.624924094061731 + } }, { - "name": "GET /api/products/:id", + "key": "GET /api/products/:id", + "averageResponseTime": 13516.5, + "transactionsPerMinute": 1, + "impact": 2.3368756900811305, + "p95": 37856, "sample": { - "@timestamp": "2020-06-29T06:48:24.977Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, - "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:27.004717Z" }, + "@timestamp": "2020-06-29T06:47:57.555Z", + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, + "container": { + "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:47:59.085077Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "::ffff:172.18.0.7" } + "socket": { + "encrypted": false, + "remote_address": "::ffff:172.18.0.7" + } }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["231"], - "Content-Type": ["application/json; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:24 GMT"], - "Etag": ["W/\"e7-kkuzj37GZDzXDh0CWqh5Gan0VO4\""], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "231" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:47:57 GMT" + ], + "Etag": [ + "W/\"e7-6JlJegaJ+ir0C8I8EmmOjms1dnc\"" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 200 }, @@ -822,87 +1341,152 @@ "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", "ecosystem-workload.config.js" ], - "pid": 137, + "pid": 87, "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", - "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "node": { + "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413304977014 }, - "trace": { "id": "b9b290bca14c99962fa9a1c509401630" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413277555176 + }, + "trace": { + "id": "8365e1763f19e4067b88521d4d9247a0" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 4482 }, - "id": "b8d8284ff1fc25d6", + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 37709 + }, + "id": "be2722a418272f10", "name": "GET /api/products/:id", "result": "HTTP 2xx", "sampled": true, - "span_count": { "started": 1 }, + "span_count": { + "started": 1 + }, "type": "request" }, "url": { "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/products/3", - "original": "/api/products/3", - "path": "/api/products/3", + "full": "http://opbeans-node:3000/api/products/1", + "original": "/api/products/1", + "path": "/api/products/1", "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 37856, - "averageResponseTime": 13516.5, - "transactionsPerMinute": 1, - "impact": 2.3368756900811305 + } }, { - "name": "GET /api/types", + "key": "GET /api/types", + "averageResponseTime": 26992.5, + "transactionsPerMinute": 0.5, + "impact": 2.3330057413794503, + "p95": 45248, "sample": { - "@timestamp": "2020-06-29T06:48:26.443Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, - "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:29.977518Z" }, + "@timestamp": "2020-06-29T06:47:52.935Z", + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, + "container": { + "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:47:55.471071Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "::ffff:172.18.0.7" } + "socket": { + "encrypted": false, + "remote_address": "::ffff:172.18.0.7" + } }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["112"], - "Content-Type": ["application/json; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:26 GMT"], - "Etag": ["W/\"70-1z6hT7P1WHgBgS/BeUEVeHhOCQU\""], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "112" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:47:52 GMT" + ], + "Etag": [ + "W/\"70-1z6hT7P1WHgBgS/BeUEVeHhOCQU\"" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 200 }, @@ -928,31 +1512,56 @@ "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", "ecosystem-workload.config.js" ], - "pid": 137, + "pid": 63, "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", - "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "node": { + "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413306443014 }, - "trace": { "id": "be3f4eb50d253afc032c90eacaa85072" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413272935117 + }, + "trace": { + "id": "2946c536a33d163d0c984d00d1f3839a" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 8892 }, - "id": "ccce129bb8c6b125", + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 45093 + }, + "id": "103482fda88b9400", "name": "GET /api/types", "result": "HTTP 2xx", "sampled": true, - "span_count": { "started": 2 }, + "span_count": { + "started": 2 + }, "type": "request" }, "url": { @@ -963,55 +1572,101 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 45248, - "averageResponseTime": 26992.5, - "transactionsPerMinute": 0.5, - "impact": 2.3330057413794503 + } }, { - "name": "GET static file", + "key": "GET static file", + "averageResponseTime": 3492.9285714285716, + "transactionsPerMinute": 3.5, + "impact": 2.0901067389184496, + "p95": 11900, "sample": { - "@timestamp": "2020-06-29T06:48:40.953Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, - "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:43.992454Z" }, + "@timestamp": "2020-06-29T06:47:53.427Z", + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, + "container": { + "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:47:55.472070Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "::ffff:172.18.0.7" } + "socket": { + "encrypted": false, + "remote_address": "::ffff:172.18.0.7" + } }, "response": { "headers": { - "Accept-Ranges": ["bytes"], - "Cache-Control": ["public, max-age=0"], - "Connection": ["close"], - "Content-Length": ["15086"], - "Content-Type": ["image/x-icon"], - "Date": ["Mon, 29 Jun 2020 06:48:40 GMT"], - "Etag": ["W/\"3aee-1725aff14f0\""], - "Last-Modified": ["Thu, 28 May 2020 11:16:06 GMT"], - "X-Powered-By": ["Express"] + "Accept-Ranges": [ + "bytes" + ], + "Cache-Control": [ + "public, max-age=0" + ], + "Connection": [ + "close" + ], + "Content-Length": [ + "15086" + ], + "Content-Type": [ + "image/x-icon" + ], + "Date": [ + "Mon, 29 Jun 2020 06:47:53 GMT" + ], + "Etag": [ + "W/\"3aee-1725aff14f0\"" + ], + "Last-Modified": [ + "Thu, 28 May 2020 11:16:06 GMT" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 200 }, @@ -1031,30 +1686,53 @@ "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", "ecosystem-workload.config.js" ], - "pid": 137, + "pid": 63, "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", - "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "node": { + "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413320953016 }, - "trace": { "id": "292393440bbe04385f3c2e3715ac35fe" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413273427016 + }, + "trace": { + "id": "ec8a804fedf28fcf81d5682d69a16970" + }, "transaction": { - "duration": { "us": 1671 }, - "id": "d1d964ca1865dce3", + "duration": { + "us": 4934 + }, + "id": "ab90a62901b770e6", "name": "GET static file", "result": "HTTP 2xx", "sampled": true, - "span_count": { "started": 0 }, + "span_count": { + "started": 0 + }, "type": "request" }, "url": { @@ -1066,56 +1744,86 @@ "scheme": "http" }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 11900, - "averageResponseTime": 3492.9285714285716, - "transactionsPerMinute": 3.5, - "impact": 2.0901067389184496 + } }, { - "name": "GET /api/products/top", + "key": "GET /api/products/top", + "averageResponseTime": 22958.5, + "transactionsPerMinute": 0.5, + "impact": 1.9475397398343375, + "p95": 33216, "sample": { - "@timestamp": "2020-06-29T06:48:18.211Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.8" }, - "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:21.007197Z" }, + "@timestamp": "2020-06-29T06:48:01.200Z", + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, + "container": { + "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:02.734903Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Accept": ["*/*"], - "Accept-Encoding": ["gzip, deflate"], - "Connection": ["keep-alive"], - "Host": ["opbeans-node:3000"], - "Referer": ["http://opbeans-node:3000/dashboard"], - "Traceparent": ["00-4879105b2de793ca54ce7299aff0f5ce-0d67fab9c9dec84d-01"], + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], "User-Agent": [ - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/79.0.3945.0 Safari/537.36" + "workload/2.4.3" ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "::ffff:172.18.0.8" } + "socket": { + "encrypted": false, + "remote_address": "::ffff:172.18.0.7" + } }, "response": { "headers": { - "Connection": ["keep-alive"], - "Content-Length": ["2"], - "Content-Type": ["application/json; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:18 GMT"], - "Etag": ["W/\"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w\""], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "2" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:01 GMT" + ], + "Etag": [ + "W/\"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w\"" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 200 }, @@ -1135,38 +1843,62 @@ "version": "8.0.0", "version_major": 8 }, - "parent": { "id": "0d67fab9c9dec84d" }, "process": { "args": [ "/usr/local/bin/node", "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", "ecosystem-workload.config.js" ], - "pid": 137, + "pid": 115, "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", - "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "node": { + "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.8" }, - "timestamp": { "us": 1593413298211013 }, - "trace": { "id": "4879105b2de793ca54ce7299aff0f5ce" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413281200133 + }, + "trace": { + "id": "195f32efeb6f91e2f71b6bc8bb74ae3a" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 12820 }, - "id": "b15b12c837ab8b89", + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 33097 + }, + "id": "22e72956dfc8967a", "name": "GET /api/products/top", "result": "HTTP 2xx", "sampled": true, - "span_count": { "started": 1 }, + "span_count": { + "started": 1 + }, "type": "request" }, "url": { @@ -1177,56 +1909,103 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, - "name": "HeadlessChrome", - "original": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/79.0.3945.0 Safari/537.36", - "os": { "name": "Linux" }, - "version": "79.0.3945" + "device": { + "name": "Other" + }, + "name": "Other", + "original": "workload/2.4.3" } - }, - "p95": 33216, - "averageResponseTime": 22958.5, - "transactionsPerMinute": 0.5, - "impact": 1.9475397398343375 + } }, { - "name": "GET /api/stats", + "key": "GET /api/stats", + "averageResponseTime": 7105.333333333333, + "transactionsPerMinute": 1.5, + "impact": 1.7905918202662048, + "p95": 15136, "sample": { - "@timestamp": "2020-06-29T06:48:39.451Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, - "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:43.984824Z" }, + "@timestamp": "2020-06-29T06:48:21.150Z", + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.8" + }, + "container": { + "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:26.993832Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Connection": [ + "keep-alive" + ], + "Host": [ + "opbeans-node:3000" + ], + "If-None-Match": [ + "W/\"5c-6I+bqIiLxvkWuwBUnTxhBoK4lBk\"" + ], + "Referer": [ + "http://opbeans-node:3000/dashboard" + ], + "Traceparent": [ + "00-ee0ce8b38b8d5945829fc1c9432538bf-39d52cd5f528d363-01" + ], + "User-Agent": [ + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/79.0.3945.0 Safari/537.36" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "::ffff:172.18.0.7" } + "socket": { + "encrypted": false, + "remote_address": "::ffff:172.18.0.8" + } }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["92"], - "Content-Type": ["application/json; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:39 GMT"], - "Etag": ["W/\"5c-6I+bqIiLxvkWuwBUnTxhBoK4lBk\""], - "X-Powered-By": ["Express"] + "Connection": [ + "keep-alive" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:21 GMT" + ], + "Etag": [ + "W/\"5c-6I+bqIiLxvkWuwBUnTxhBoK4lBk\"" + ], + "X-Powered-By": [ + "Express" + ] }, - "status_code": 200 + "status_code": 304 }, "version": "1.1" }, @@ -1244,6 +2023,9 @@ "version": "8.0.0", "version_major": 8 }, + "parent": { + "id": "39d52cd5f528d363" + }, "process": { "args": [ "/usr/local/bin/node", @@ -1254,27 +2036,52 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", - "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "node": { + "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413319451016 }, - "trace": { "id": "a05787cb03a0af0863fab5e05de942f1" }, + "source": { + "ip": "172.18.0.8" + }, + "timestamp": { + "us": 1593413301150014 + }, + "trace": { + "id": "ee0ce8b38b8d5945829fc1c9432538bf" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 5050 }, - "id": "a7e004eba8f021ce", + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 7273 + }, + "id": "05d5b62182c59a54", "name": "GET /api/stats", - "result": "HTTP 2xx", + "result": "HTTP 3xx", "sampled": true, - "span_count": { "started": 4 }, + "span_count": { + "started": 4 + }, "type": "request" }, "url": { @@ -1285,52 +2092,96 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, - "name": "Other", - "original": "workload/2.4.3" + "device": { + "name": "Other" + }, + "name": "HeadlessChrome", + "original": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/79.0.3945.0 Safari/537.36", + "os": { + "name": "Linux" + }, + "version": "79.0.3945" } - }, - "p95": 15136, - "averageResponseTime": 7105.333333333333, - "transactionsPerMinute": 1.5, - "impact": 1.7905918202662048 + } }, { - "name": "GET /log-error", + "key": "GET /log-error", + "averageResponseTime": 35846, + "transactionsPerMinute": 0.25, + "impact": 1.466376117925459, + "p95": 35840, "sample": { "@timestamp": "2020-06-29T06:48:07.467Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, - "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:18.533253Z" }, + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, + "container": { + "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:18.533253Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "::ffff:172.18.0.7" } + "socket": { + "encrypted": false, + "remote_address": "::ffff:172.18.0.7" + } }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["24"], - "Content-Type": ["text/html; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:07 GMT"], - "Etag": ["W/\"18-MS3VbhH7auHMzO0fUuNF6v14N/M\""], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "24" + ], + "Content-Type": [ + "text/html; charset=utf-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:07 GMT" + ], + "Etag": [ + "W/\"18-MS3VbhH7auHMzO0fUuNF6v14N/M\"" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 500 }, @@ -1360,27 +2211,52 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", - "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "node": { + "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413287467017 }, - "trace": { "id": "d518b2c4d72cd2aaf1e39bad7ebcbdbb" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413287467017 + }, + "trace": { + "id": "d518b2c4d72cd2aaf1e39bad7ebcbdbb" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 35846 }, + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 35846 + }, "id": "c7a30c1b076907ec", "name": "GET /log-error", "result": "HTTP 5xx", "sampled": true, - "span_count": { "started": 0 }, + "span_count": { + "started": 0 + }, "type": "request" }, "url": { @@ -1391,56 +2267,104 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 35840, - "averageResponseTime": 35846, - "transactionsPerMinute": 0.25, - "impact": 1.466376117925459 + } }, { - "name": "POST /api", + "key": "POST /api", + "averageResponseTime": 20011, + "transactionsPerMinute": 0.25, + "impact": 0.7098250353192541, + "p95": 19968, "sample": { "@timestamp": "2020-06-29T06:48:25.478Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, - "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:27.005671Z" }, + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, + "container": { + "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:27.005671Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { - "body": { "original": "[REDACTED]" }, + "body": { + "original": "[REDACTED]" + }, "headers": { - "Accept": ["application/json"], - "Connection": ["close"], - "Content-Length": ["129"], - "Content-Type": ["application/json"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Accept": [ + "application/json" + ], + "Connection": [ + "close" + ], + "Content-Length": [ + "129" + ], + "Content-Type": [ + "application/json" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "post", - "socket": { "encrypted": false, "remote_address": "::ffff:172.18.0.7" } + "socket": { + "encrypted": false, + "remote_address": "::ffff:172.18.0.7" + } }, "response": { "headers": { - "Allow": ["GET"], - "Connection": ["close"], - "Content-Type": ["application/json;charset=UTF-8"], - "Date": ["Mon, 29 Jun 2020 06:48:25 GMT"], - "Transfer-Encoding": ["chunked"], - "X-Powered-By": ["Express"] + "Allow": [ + "GET" + ], + "Connection": [ + "close" + ], + "Content-Type": [ + "application/json;charset=UTF-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:25 GMT" + ], + "Transfer-Encoding": [ + "chunked" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 405 }, @@ -1470,27 +2394,52 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", - "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "node": { + "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413305478010 }, - "trace": { "id": "4bd9027dd1e355ec742970e2d6333124" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413305478010 + }, + "trace": { + "id": "4bd9027dd1e355ec742970e2d6333124" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 20011 }, + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 20011 + }, "id": "94104435cf151478", "name": "POST /api", "result": "HTTP 4xx", "sampled": true, - "span_count": { "started": 1 }, + "span_count": { + "started": 1 + }, "type": "request" }, "url": { @@ -1501,52 +2450,92 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 19968, - "averageResponseTime": 20011, - "transactionsPerMinute": 0.25, - "impact": 0.7098250353192541 + } }, { - "name": "GET /api/types/:id", + "key": "GET /api/types/:id", + "averageResponseTime": 8181, + "transactionsPerMinute": 0.5, + "impact": 0.5354862351657939, + "p95": 10080, "sample": { - "@timestamp": "2020-06-29T06:48:12.972Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, - "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:18.543436Z" }, + "@timestamp": "2020-06-29T06:47:53.928Z", + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, + "container": { + "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:47:55.472718Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "::ffff:172.18.0.7" } + "socket": { + "encrypted": false, + "remote_address": "::ffff:172.18.0.7" + } }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["205"], - "Content-Type": ["application/json; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:12 GMT"], - "Etag": ["W/\"cd-pFMi1QOVY6YqWe+nwcbZVviCths\""], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "205" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:47:53 GMT" + ], + "Etag": [ + "W/\"cd-pFMi1QOVY6YqWe+nwcbZVviCths\"" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 200 }, @@ -1572,31 +2561,56 @@ "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", "ecosystem-workload.config.js" ], - "pid": 137, + "pid": 63, "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", - "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "node": { + "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413292972011 }, - "trace": { "id": "aea65cef5f902dda5d8e38f9fb38864d" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413273928016 + }, + "trace": { + "id": "0becaafb422bfeb69e047bf7153aa469" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 6300 }, - "id": "a5bdbe43ac05fae2", + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 10062 + }, + "id": "0cee4574091bda3b", "name": "GET /api/types/:id", "result": "HTTP 2xx", "sampled": true, - "span_count": { "started": 2 }, + "span_count": { + "started": 2 + }, "type": "request" }, "url": { @@ -1607,50 +2621,86 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 10080, - "averageResponseTime": 8181, - "transactionsPerMinute": 0.5, - "impact": 0.5354862351657939 + } }, { - "name": "GET /api/orders/:id", + "key": "GET /api/orders/:id", + "averageResponseTime": 4749.666666666667, + "transactionsPerMinute": 0.75, + "impact": 0.43453312891085794, + "p95": 7184, "sample": { "@timestamp": "2020-06-29T06:48:35.951Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, - "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:39.484133Z" }, + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, + "container": { + "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:39.484133Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "::ffff:172.18.0.7" } + "socket": { + "encrypted": false, + "remote_address": "::ffff:172.18.0.7" + } }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["0"], - "Date": ["Mon, 29 Jun 2020 06:48:35 GMT"], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "0" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:35 GMT" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 404 }, @@ -1680,27 +2730,52 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", - "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "node": { + "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413315951017 }, - "trace": { "id": "95979caa80e6622cbbb2d308800c3016" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413315951017 + }, + "trace": { + "id": "95979caa80e6622cbbb2d308800c3016" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 3210 }, + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 3210 + }, "id": "30344988dace0b43", "name": "GET /api/orders/:id", "result": "HTTP 4xx", "sampled": true, - "span_count": { "started": 1 }, + "span_count": { + "started": 1 + }, "type": "request" }, "url": { @@ -1711,52 +2786,92 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 7184, - "averageResponseTime": 4749.666666666667, - "transactionsPerMinute": 0.75, - "impact": 0.43453312891085794 + } }, { - "name": "GET /api/products/:id/customers", + "key": "GET /api/products/:id/customers", + "averageResponseTime": 4757, + "transactionsPerMinute": 0.5, + "impact": 0.20830834986820673, + "p95": 5616, "sample": { - "@timestamp": "2020-06-29T06:48:41.956Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, - "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:43.994692Z" }, + "@timestamp": "2020-06-29T06:48:22.977Z", + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, + "container": { + "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:27.000765Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "::ffff:172.18.0.7" } + "socket": { + "encrypted": false, + "remote_address": "::ffff:172.18.0.7" + } }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["2"], - "Content-Type": ["application/json; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:41 GMT"], - "Etag": ["W/\"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w\""], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "2" + ], + "Content-Type": [ + "application/json; charset=utf-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:22 GMT" + ], + "Etag": [ + "W/\"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w\"" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 200 }, @@ -1786,84 +2901,151 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", - "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "node": { + "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413321956016 }, - "trace": { "id": "f735ac5fca8f83eebc748f4a2e009e61" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413302977008 + }, + "trace": { + "id": "da8f22fe652ccb6680b3029ab6efd284" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 3896 }, - "id": "b24ce95c855f83a4", + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 5618 + }, + "id": "bc51c1523afaf57a", "name": "GET /api/products/:id/customers", "result": "HTTP 2xx", "sampled": true, - "span_count": { "started": 1 }, + "span_count": { + "started": 1 + }, "type": "request" }, "url": { "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/products/5/customers", - "original": "/api/products/5/customers", - "path": "/api/products/5/customers", + "full": "http://opbeans-node:3000/api/products/3/customers", + "original": "/api/products/3/customers", + "path": "/api/products/3/customers", "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 5616, - "averageResponseTime": 4757, - "transactionsPerMinute": 0.5, - "impact": 0.20830834986820673 + } }, { - "name": "GET /throw-error", + "key": "GET /throw-error", + "averageResponseTime": 2577, + "transactionsPerMinute": 0.5, + "impact": 0, + "p95": 3224, "sample": { - "@timestamp": "2020-06-29T06:48:42.954Z", - "agent": { "name": "nodejs", "version": "3.6.1" }, - "client": { "ip": "172.18.0.7" }, - "container": { "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "ecs": { "version": "1.5.0" }, - "event": { "ingested": "2020-06-29T06:48:43.996435Z" }, + "@timestamp": "2020-06-29T06:48:19.975Z", + "agent": { + "name": "nodejs", + "version": "3.6.1" + }, + "client": { + "ip": "172.18.0.7" + }, + "container": { + "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "ingested": "2020-06-29T06:48:21.012520Z" + }, "host": { "architecture": "x64", "hostname": "41712ded148f", "ip": "172.18.0.7", "name": "41712ded148f", - "os": { "platform": "linux" } + "os": { + "platform": "linux" + } }, "http": { "request": { "headers": { - "Connection": ["close"], - "Host": ["opbeans-node:3000"], - "User-Agent": ["workload/2.4.3"] + "Connection": [ + "close" + ], + "Host": [ + "opbeans-node:3000" + ], + "User-Agent": [ + "workload/2.4.3" + ] }, "method": "get", - "socket": { "encrypted": false, "remote_address": "::ffff:172.18.0.7" } + "socket": { + "encrypted": false, + "remote_address": "::ffff:172.18.0.7" + } }, "response": { "headers": { - "Connection": ["close"], - "Content-Length": ["148"], - "Content-Security-Policy": ["default-src 'none'"], - "Content-Type": ["text/html; charset=utf-8"], - "Date": ["Mon, 29 Jun 2020 06:48:42 GMT"], - "X-Content-Type-Options": ["nosniff"], - "X-Powered-By": ["Express"] + "Connection": [ + "close" + ], + "Content-Length": [ + "148" + ], + "Content-Security-Policy": [ + "default-src 'none'" + ], + "Content-Type": [ + "text/html; charset=utf-8" + ], + "Date": [ + "Mon, 29 Jun 2020 06:48:19 GMT" + ], + "X-Content-Type-Options": [ + "nosniff" + ], + "X-Powered-By": [ + "Express" + ] }, "status_code": 500 }, @@ -1893,27 +3075,52 @@ "ppid": 1, "title": "node /app/server.js" }, - "processor": { "event": "transaction", "name": "transaction" }, + "processor": { + "event": "transaction", + "name": "transaction" + }, "service": { "environment": "production", - "framework": { "name": "express", "version": "4.17.1" }, - "language": { "name": "javascript" }, + "framework": { + "name": "express", + "version": "4.17.1" + }, + "language": { + "name": "javascript" + }, "name": "opbeans-node", - "node": { "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" }, - "runtime": { "name": "node", "version": "12.18.1" }, + "node": { + "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" + }, + "runtime": { + "name": "node", + "version": "12.18.1" + }, "version": "1.0.0" }, - "source": { "ip": "172.18.0.7" }, - "timestamp": { "us": 1593413322954016 }, - "trace": { "id": "9d5aee7443a43db9820f622a10dfac6e" }, + "source": { + "ip": "172.18.0.7" + }, + "timestamp": { + "us": 1593413299975019 + }, + "trace": { + "id": "106f3a55b0b0ea327d1bbe4be66c3bcc" + }, "transaction": { - "custom": { "shoppingBasketCount": 42 }, - "duration": { "us": 1928 }, - "id": "8e6fc8c3f99e8fc9", + "custom": { + "shoppingBasketCount": 42 + }, + "duration": { + "us": 3226 + }, + "id": "247b9141552a9e73", "name": "GET /throw-error", "result": "HTTP 5xx", "sampled": true, - "span_count": { "started": 0 }, + "span_count": { + "started": 0 + }, "type": "request" }, "url": { @@ -1924,19 +3131,21 @@ "port": 3000, "scheme": "http" }, - "user": { "email": "kimchy@elastic.co", "id": "42", "name": "kimchy" }, + "user": { + "email": "kimchy@elastic.co", + "id": "42", + "name": "kimchy" + }, "user_agent": { - "device": { "name": "Other" }, + "device": { + "name": "Other" + }, "name": "Other", "original": "workload/2.4.3" } - }, - "p95": 3224, - "averageResponseTime": 2577, - "transactionsPerMinute": 0.5, - "impact": 0 + } } ], "isAggregationAccurate": true, - "bucketSize": 100 -} + "bucketSize": 1000 +} \ No newline at end of file diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/top_transaction_groups.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/top_transaction_groups.ts index 43b2ad5414c7a..94559a3e4aa54 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/top_transaction_groups.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/top_transaction_groups.ts @@ -4,9 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; +import { sortBy } from 'lodash'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import expectedTransactionGroups from './expectation/top_transaction_groups.json'; +function sortTransactionGroups(items: any[]) { + return sortBy(items, 'impact'); +} + +function omitSampleFromTransactionGroups(items: any[]) { + return sortTransactionGroups(items).map(({ sample, ...item }) => ({ ...item })); +} + export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); @@ -48,15 +57,19 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns the correct buckets (when ignoring samples)', async () => { - function omitSample(items: any[]) { - return items.map(({ sample, ...item }) => ({ ...item })); - } - - expect(omitSample(response.body.items)).to.eql(omitSample(expectedTransactionGroups.items)); + expect(omitSampleFromTransactionGroups(response.body.items)).to.eql( + omitSampleFromTransactionGroups(expectedTransactionGroups.items) + ); }); it('returns the correct buckets and samples', async () => { - expect(response.body.items).to.eql(expectedTransactionGroups.items); + // sample should provide enough information to deeplink to a transaction detail page + response.body.items.forEach((item: any) => { + expect(item.sample.trace.id).to.be.an('string'); + expect(item.sample.transaction.id).to.be.an('string'); + expect(item.sample.service.name).to.be('opbeans-node'); + expect(item.sample.transaction.name).to.be(item.key); + }); }); }); }); From fb4ee91f0ca40d2158d0756e1aac18b01ba69761 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Tue, 28 Jul 2020 09:55:57 -0400 Subject: [PATCH 83/96] [Security Solution][Resolver] Fix resolver isStart event bug (#73357) * Check if category is array * Adding more tests and renaming to isStart * Handling the case where start is not at the front --- .../common/endpoint/generate_data.test.ts | 21 ++-- .../common/endpoint/generate_data.ts | 25 +++-- .../common/endpoint/models/event.test.ts | 96 ++++++++++++++----- .../common/endpoint/models/event.ts | 7 +- .../resolver/utils/children_helper.test.ts | 4 +- .../routes/resolver/utils/children_helper.ts | 4 +- 6 files changed, 111 insertions(+), 46 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts index fcea86be4ae9e..debe4a3da6a6f 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import _ from 'lodash'; import { EndpointDocGenerator, Event, @@ -79,9 +80,9 @@ describe('data generator', () => { const timestamp = new Date().getTime(); const processEvent = generator.generateEvent({ timestamp }); expect(processEvent['@timestamp']).toEqual(timestamp); - expect(processEvent.event.category).toEqual('process'); + expect(processEvent.event.category).toEqual(['process']); expect(processEvent.event.kind).toEqual('event'); - expect(processEvent.event.type).toEqual('start'); + expect(processEvent.event.type).toEqual(['start']); expect(processEvent.agent).not.toBeNull(); expect(processEvent.host).not.toBeNull(); expect(processEvent.process.entity_id).not.toBeNull(); @@ -94,7 +95,7 @@ describe('data generator', () => { expect(processEvent['@timestamp']).toEqual(timestamp); expect(processEvent.event.category).toEqual('dns'); expect(processEvent.event.kind).toEqual('event'); - expect(processEvent.event.type).toEqual('start'); + expect(processEvent.event.type).toEqual(['start']); expect(processEvent.agent).not.toBeNull(); expect(processEvent.host).not.toBeNull(); expect(processEvent.process.entity_id).not.toBeNull(); @@ -332,6 +333,12 @@ describe('data generator', () => { describe('creates alert ancestor tree', () => { let events: Event[]; + const isCategoryProcess = (event: Event) => { + return ( + _.isEqual(event.event.category, ['process']) || _.isEqual(event.event.category, 'process') + ); + }; + beforeEach(() => { events = generator.createAlertEventAncestry({ ancestors: 3, @@ -343,11 +350,7 @@ describe('data generator', () => { it('with n-1 process events', () => { for (let i = events.length - 2; i > 0; ) { const parentEntityIdOfChild = events[i].process.parent?.entity_id; - for ( - ; - --i >= -1 && (events[i].event.kind !== 'event' || events[i].event.category !== 'process'); - - ) { + for (; --i >= -1 && (events[i].event.kind !== 'event' || !isCategoryProcess(events[i])); ) { // related event - skip it } expect(i).toBeGreaterThanOrEqual(0); @@ -361,7 +364,7 @@ describe('data generator', () => { ; previousProcessEventIndex >= -1 && (events[previousProcessEventIndex].event.kind !== 'event' || - events[previousProcessEventIndex].event.category !== 'process'); + !isCategoryProcess(events[previousProcessEventIndex])); previousProcessEventIndex-- ) { // related event - skip it diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index 66e786cb02e63..97ac5c9030a3d 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -35,7 +35,7 @@ interface EventOptions { timestamp?: number; entityID?: string; parentEntityID?: string; - eventType?: string; + eventType?: string | string[]; eventCategory?: string | string[]; processName?: string; ancestry?: string[]; @@ -572,9 +572,9 @@ export class EndpointDocGenerator { }, ...detailRecordForEventType, event: { - category: options.eventCategory ? options.eventCategory : 'process', + category: options.eventCategory ? options.eventCategory : ['process'], kind: 'event', - type: options.eventType ? options.eventType : 'start', + type: options.eventType ? options.eventType : ['start'], id: this.seededUUIDv4(), }, host: this.commonInfo.host, @@ -633,7 +633,12 @@ export class EndpointDocGenerator { // place the event in the right array depending on its category if (event.event.kind === 'event') { - if (event.event.category === 'process') { + if ( + (Array.isArray(event.event.category) && + event.event.category.length === 1 && + event.event.category[0] === 'process') || + event.event.category === 'process' + ) { node.lifecycle.push(event); } else { node.relatedEvents.push(event); @@ -812,8 +817,8 @@ export class EndpointDocGenerator { timestamp: timestamp + termProcessDuration * 1000, entityID: root.process.entity_id, parentEntityID: root.process.parent?.entity_id, - eventCategory: 'process', - eventType: 'end', + eventCategory: ['process'], + eventType: ['end'], }) ); } @@ -838,8 +843,8 @@ export class EndpointDocGenerator { timestamp: timestamp + termProcessDuration * 1000, entityID: ancestor.process.entity_id, parentEntityID: ancestor.process.parent?.entity_id, - eventCategory: 'process', - eventType: 'end', + eventCategory: ['process'], + eventType: ['end'], ancestry: ancestor.process.Ext?.ancestry, ancestryArrayLimit: opts.ancestryArraySize, }) @@ -936,8 +941,8 @@ export class EndpointDocGenerator { timestamp: timestamp + processDuration * 1000, entityID: child.process.entity_id, parentEntityID: child.process.parent?.entity_id, - eventCategory: 'process', - eventType: 'end', + eventCategory: ['process'], + eventType: ['end'], ancestry: child.process.Ext?.ancestry, ancestryArrayLimit: opts.ancestryArraySize, }); diff --git a/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts b/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts index a0bf00f0274e6..62f923aa6d793 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts @@ -4,38 +4,90 @@ * you may not use this file except in compliance with the Elastic License. */ import { EndpointDocGenerator } from '../generate_data'; -import { descriptiveName } from './event'; +import { descriptiveName, isStart } from './event'; +import { ResolverEvent } from '../types'; -describe('Event descriptive names', () => { +describe('Generated documents', () => { let generator: EndpointDocGenerator; beforeEach(() => { generator = new EndpointDocGenerator('seed'); }); - it('returns the right name for a registry event', () => { - const extensions = { registry: { key: `HKLM/Windows/Software/abc` } }; - const event = generator.generateEvent({ eventCategory: 'registry', extensions }); - expect(descriptiveName(event)).toEqual({ subject: `HKLM/Windows/Software/abc` }); - }); + describe('Event descriptive names', () => { + it('returns the right name for a registry event', () => { + const extensions = { registry: { key: `HKLM/Windows/Software/abc` } }; + const event = generator.generateEvent({ eventCategory: 'registry', extensions }); + expect(descriptiveName(event)).toEqual({ subject: `HKLM/Windows/Software/abc` }); + }); - it('returns the right name for a network event', () => { - const randomIP = `${generator.randomIP()}`; - const extensions = { network: { direction: 'outbound', forwarded_ip: randomIP } }; - const event = generator.generateEvent({ eventCategory: 'network', extensions }); - expect(descriptiveName(event)).toEqual({ subject: `${randomIP}`, descriptor: 'outbound' }); - }); + it('returns the right name for a network event', () => { + const randomIP = `${generator.randomIP()}`; + const extensions = { network: { direction: 'outbound', forwarded_ip: randomIP } }; + const event = generator.generateEvent({ eventCategory: 'network', extensions }); + expect(descriptiveName(event)).toEqual({ subject: `${randomIP}`, descriptor: 'outbound' }); + }); - it('returns the right name for a file event', () => { - const extensions = { file: { path: 'C:\\My Documents\\business\\January\\processName' } }; - const event = generator.generateEvent({ eventCategory: 'file', extensions }); - expect(descriptiveName(event)).toEqual({ - subject: 'C:\\My Documents\\business\\January\\processName', + it('returns the right name for a file event', () => { + const extensions = { file: { path: 'C:\\My Documents\\business\\January\\processName' } }; + const event = generator.generateEvent({ eventCategory: 'file', extensions }); + expect(descriptiveName(event)).toEqual({ + subject: 'C:\\My Documents\\business\\January\\processName', + }); + }); + + it('returns the right name for a dns event', () => { + const extensions = { dns: { question: { name: `${generator.randomIP()}` } } }; + const event = generator.generateEvent({ eventCategory: 'dns', extensions }); + expect(descriptiveName(event)).toEqual({ subject: extensions.dns.question.name }); }); }); - it('returns the right name for a dns event', () => { - const extensions = { dns: { question: { name: `${generator.randomIP()}` } } }; - const event = generator.generateEvent({ eventCategory: 'dns', extensions }); - expect(descriptiveName(event)).toEqual({ subject: extensions.dns.question.name }); + describe('Start events', () => { + it('is a start event when event.type is a string', () => { + const event: ResolverEvent = generator.generateEvent({ + eventType: 'start', + }); + expect(isStart(event)).toBeTruthy(); + }); + + it('is a start event when event.type is an array of strings', () => { + const event: ResolverEvent = generator.generateEvent({ + eventType: ['start'], + }); + expect(isStart(event)).toBeTruthy(); + }); + + it('is a start event when event.type is an array of strings and contains start', () => { + let event: ResolverEvent = generator.generateEvent({ + eventType: ['bogus', 'start', 'creation'], + }); + expect(isStart(event)).toBeTruthy(); + + event = generator.generateEvent({ + eventType: ['start', 'bogus'], + }); + expect(isStart(event)).toBeTruthy(); + }); + + it('is not a start event when event.type is not start', () => { + const event: ResolverEvent = generator.generateEvent({ + eventType: ['end'], + }); + expect(isStart(event)).toBeFalsy(); + }); + + it('is not a start event when event.type is empty', () => { + const event: ResolverEvent = generator.generateEvent({ + eventType: [], + }); + expect(isStart(event)).toBeFalsy(); + }); + + it('is not a start event when event.type is bogus', () => { + const event: ResolverEvent = generator.generateEvent({ + eventType: ['bogus'], + }); + expect(isStart(event)).toBeFalsy(); + }); }); }); diff --git a/x-pack/plugins/security_solution/common/endpoint/models/event.ts b/x-pack/plugins/security_solution/common/endpoint/models/event.ts index f53da8fb1f096..216b5cc028588 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/event.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/event.ts @@ -9,10 +9,15 @@ export function isLegacyEvent(event: ResolverEvent): event is LegacyEndpointEven return (event as LegacyEndpointEvent).endgame !== undefined; } -export function isProcessStart(event: ResolverEvent): boolean { +export function isStart(event: ResolverEvent): boolean { if (isLegacyEvent(event)) { return event.event?.type === 'process_start' || event.event?.action === 'fork_event'; } + + if (Array.isArray(event.event.type)) { + return event.event.type.includes('start'); + } + return event.event.type === 'start'; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts index ca5b5aef0f651..01dd59b2611d9 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts @@ -10,12 +10,12 @@ import { TreeNode, } from '../../../../../common/endpoint/generate_data'; import { ChildrenNodesHelper } from './children_helper'; -import { eventId, isProcessStart } from '../../../../../common/endpoint/models/event'; +import { eventId, isStart } from '../../../../../common/endpoint/models/event'; function getStartEvents(events: Event[]): Event[] { const startEvents: Event[] = []; for (const event of events) { - if (isProcessStart(event)) { + if (isStart(event)) { startEvents.push(event); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts index 01e356682ac47..d3ca7a54c16d2 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts @@ -7,7 +7,7 @@ import { entityId, parentEntityId, - isProcessStart, + isStart, getAncestryAsArray, } from '../../../../../common/endpoint/models/event'; import { @@ -99,7 +99,7 @@ export class ChildrenNodesHelper { for (const event of startEvents) { const parentID = parentEntityId(event); const entityID = entityId(event); - if (parentID && entityID && isProcessStart(event)) { + if (parentID && entityID && isStart(event)) { // don't actually add the start event to the node, because that'll be done in // a different call const childNode = this.getOrCreateChildNode(entityID); From 5e8e01fd0f3ea5516a2f68bef9e7a9877b9e549a Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 28 Jul 2020 15:00:41 +0100 Subject: [PATCH 84/96] removed ESO migration from alerting (#73420) This PR removes the use of ESO migration from alerting as we do not actually need this until the RBAC work lands, which should be 7.10. This allows us to concentrate the challenges of introducing RBAC into one single release which hopefully will help us better mitigate potential regressions. --- .../alerts/server/saved_objects/index.ts | 2 - .../server/saved_objects/migrations.test.ts | 121 ------------------ .../alerts/server/saved_objects/migrations.ts | 53 -------- .../spaces_only/tests/alerting/index.ts | 3 - .../spaces_only/tests/alerting/migrations.ts | 43 ------- 5 files changed, 222 deletions(-) delete mode 100644 x-pack/plugins/alerts/server/saved_objects/migrations.test.ts delete mode 100644 x-pack/plugins/alerts/server/saved_objects/migrations.ts delete mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts diff --git a/x-pack/plugins/alerts/server/saved_objects/index.ts b/x-pack/plugins/alerts/server/saved_objects/index.ts index 06ce8d673e6b7..c98d9bcbd9ae5 100644 --- a/x-pack/plugins/alerts/server/saved_objects/index.ts +++ b/x-pack/plugins/alerts/server/saved_objects/index.ts @@ -6,7 +6,6 @@ import { SavedObjectsServiceSetup } from 'kibana/server'; import mappings from './mappings.json'; -import { getMigrations } from './migrations'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; export function setupSavedObjects( @@ -17,7 +16,6 @@ export function setupSavedObjects( name: 'alert', hidden: true, namespaceType: 'single', - migrations: getMigrations(encryptedSavedObjects), mappings: mappings.alert, }); diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts deleted file mode 100644 index 19f4e918b7862..0000000000000 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import uuid from 'uuid'; -import { getMigrations } from './migrations'; -import { RawAlert } from '../types'; -import { SavedObjectUnsanitizedDoc } from 'kibana/server'; -import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; -import { migrationMocks } from 'src/core/server/mocks'; - -const { log } = migrationMocks.createContext(); -const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); - -describe('7.9.0', () => { - beforeEach(() => { - jest.resetAllMocks(); - encryptedSavedObjectsSetup.createMigration.mockImplementation( - (shouldMigrateWhenPredicate, migration) => migration - ); - }); - - test('changes nothing on alerts by other plugins', () => { - const migration790 = getMigrations(encryptedSavedObjectsSetup)['7.9.0']; - const alert = getMockData({}); - expect(migration790(alert, { log })).toMatchObject(alert); - - expect(encryptedSavedObjectsSetup.createMigration).toHaveBeenCalledWith( - expect.any(Function), - expect.any(Function) - ); - }); - - test('migrates the consumer for alerting', () => { - const migration790 = getMigrations(encryptedSavedObjectsSetup)['7.9.0']; - const alert = getMockData({ - consumer: 'alerting', - }); - expect(migration790(alert, { log })).toMatchObject({ - ...alert, - attributes: { - ...alert.attributes, - consumer: 'alerts', - }, - }); - }); -}); - -describe('7.10.0', () => { - beforeEach(() => { - jest.resetAllMocks(); - encryptedSavedObjectsSetup.createMigration.mockImplementation( - (shouldMigrateWhenPredicate, migration) => migration - ); - }); - - test('changes nothing on alerts by other plugins', () => { - const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; - const alert = getMockData({}); - expect(migration710(alert, { log })).toMatchObject(alert); - - expect(encryptedSavedObjectsSetup.createMigration).toHaveBeenCalledWith( - expect.any(Function), - expect.any(Function) - ); - }); - - test('migrates the consumer for metrics', () => { - const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; - const alert = getMockData({ - consumer: 'metrics', - }); - expect(migration710(alert, { log })).toMatchObject({ - ...alert, - attributes: { - ...alert.attributes, - consumer: 'infrastructure', - }, - }); - }); -}); - -function getMockData( - overwrites: Record = {} -): SavedObjectUnsanitizedDoc { - return { - attributes: { - enabled: true, - name: 'abc', - tags: ['foo'], - alertTypeId: '123', - consumer: 'bar', - apiKey: '', - apiKeyOwner: '', - schedule: { interval: '10s' }, - throttle: null, - params: { - bar: true, - }, - muteAll: false, - mutedInstanceIds: [], - createdBy: new Date().toISOString(), - updatedBy: new Date().toISOString(), - createdAt: new Date().toISOString(), - actions: [ - { - group: 'default', - actionRef: '1', - actionTypeId: '1', - params: { - foo: true, - }, - }, - ], - ...overwrites, - }, - id: uuid.v4(), - type: 'alert', - }; -} diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.ts deleted file mode 100644 index 57a4005887093..0000000000000 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { - SavedObjectMigrationMap, - SavedObjectUnsanitizedDoc, - SavedObjectMigrationFn, -} from '../../../../../src/core/server'; -import { RawAlert } from '../types'; -import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; - -export function getMigrations( - encryptedSavedObjects: EncryptedSavedObjectsPluginSetup -): SavedObjectMigrationMap { - return { - /** - * In v7.9.0 we changed the Alerting plugin so it uses the `consumer` value of `alerts` - * prior to that we were using `alerting` and we need to keep these in sync - */ - '7.9.0': changeAlertingConsumer(encryptedSavedObjects, 'alerting', 'alerts'), - /** - * In v7.10.0 we changed the Matrics plugin so it uses the `consumer` value of `infrastructure` - * prior to that we were using `metrics` and we need to keep these in sync - */ - '7.10.0': changeAlertingConsumer(encryptedSavedObjects, 'metrics', 'infrastructure'), - }; -} - -function changeAlertingConsumer( - encryptedSavedObjects: EncryptedSavedObjectsPluginSetup, - from: string, - to: string -): SavedObjectMigrationFn { - return encryptedSavedObjects.createMigration( - function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { - return doc.attributes.consumer === from; - }, - (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => { - const { - attributes: { consumer }, - } = doc; - return { - ...doc, - attributes: { - ...doc.attributes, - consumer: consumer === from ? to : consumer, - }, - }; - } - ); -} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts index 0970738b630c4..a23f0fa835313 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts @@ -27,8 +27,5 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./alerts_space1')); loadTestFile(require.resolve('./alerts_default_space')); loadTestFile(require.resolve('./builtin_alert_types')); - - // note that this test will destroy existing spaces - loadTestFile(require.resolve('./migrations')); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts deleted file mode 100644 index d0e1be12e762f..0000000000000 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { getUrlPrefix } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -// eslint-disable-next-line import/no-default-export -export default function createGetTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - describe('migrations', () => { - before(async () => { - await esArchiver.load('alerts'); - }); - - after(async () => { - await esArchiver.unload('alerts'); - }); - - it('7.9.0 migrates the `alerting` consumer to be the `alerts`', async () => { - const response = await supertest.get( - `${getUrlPrefix(``)}/api/alerts/alert/74f3e6d7-b7bb-477d-ac28-92ee22728e6e` - ); - - expect(response.status).to.eql(200); - expect(response.body.consumer).to.equal('alerts'); - }); - - it('7.10.0 migrates the `metrics` consumer to be the `infrastructure`', async () => { - const response = await supertest.get( - `${getUrlPrefix(``)}/api/alerts/alert/74f3e6d7-b7bb-477d-ac28-fdf248d5f2a4` - ); - - expect(response.status).to.eql(200); - expect(response.body.consumer).to.equal('infrastructure'); - }); - }); -} From dca4a2359724e9121df0b919f57ef2fb14bada4c Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Tue, 28 Jul 2020 08:09:35 -0600 Subject: [PATCH 85/96] [Security Solution] Full screen fixes for Timeline based views (#73421) ## Full screen fixes for Timeline based views - Fixes an issue where sometimes, Global navigation is hidden until the page is scrolled when exiting full screen mode - Improves performance by adding an intent delay before showing the draggable wrapper hover menu - Removes an unnecessary CSS transition ### Sometimes, Global navigation is hidden until the page is scrolled when exiting full screen mode Sometimes, after exiting `Full screen` mode in a page, for example, the `Detections` page, the global navigation, e.g. `Overview Detections Hosts...` is hidden until the page is scrolled. To reproduce: 1) Navigate to the `Detections` page 2) Click the `Full screen` button in the table 3) Without scrolling the full screen view, click the `Exit full screen` button **Expected result** - [x] The global navigation e.g. `Overview Detections Hosts...` is visible above the search bar, per the screenshot below: ![correct-global-navigation](https://user-images.githubusercontent.com/4459398/87717870-571bef80-c76e-11ea-8b7b-1850094326b3.png) 4) Once again, click the `Full screen` button in the table 5) This time, expand an event, which will scroll the view 6) Once again, click the `Exit full screen` button **Expected result** - [x] The global navigation e.g. `Overview Detections Hosts...` is visible above the search bar **Actual result** - [ ] Sometimes, the global navigation e.g. `Overview Detections Hosts...` is **not** visible until the page is scrolled --- .../security_solution/common/constants.ts | 1 + .../index.test.tsx | 3 + .../drag_and_drop/draggable_wrapper.test.tsx | 6 + .../drag_and_drop/provider_container.tsx | 7 -- .../filters_global/filters_global.test.tsx | 104 +++++++++++++++++- .../filters_global/filters_global.tsx | 29 +++-- .../public/common/components/page/index.tsx | 13 ++- .../components/with_hover_actions/index.tsx | 30 +++-- .../containers/use_full_screen/index.tsx | 12 +- .../detection_engine/detection_engine.tsx | 5 +- .../detection_engine/rules/details/index.tsx | 5 +- .../public/hosts/pages/details/index.tsx | 5 +- .../public/hosts/pages/hosts.tsx | 5 +- .../public/network/pages/ip_details/index.tsx | 2 +- .../public/network/pages/network.tsx | 5 +- .../public/overview/pages/overview.tsx | 2 +- .../fields_browser/field_name.test.tsx | 6 + 17 files changed, 206 insertions(+), 34 deletions(-) diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index f934d90c740a5..c74cf888a2db6 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -32,6 +32,7 @@ export const DEFAULT_INTERVAL_PAUSE = true; export const DEFAULT_INTERVAL_TYPE = 'manual'; export const DEFAULT_INTERVAL_VALUE = 300000; // ms export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges'; +export const SCROLLING_DISABLED_CLASS_NAME = 'scrolling-disabled'; export const FILTERS_GLOBAL_HEIGHT = 109; // px export const FULL_SCREEN_TOGGLED_CLASS_NAME = 'fullScreenToggled'; export const NO_ALERT_INDEX = 'no-alert-index-049FC71A-4C2C-446F-9901-37XMC5024C51'; diff --git a/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/index.test.tsx index 2af6569394e8f..eced73e9c3d67 100644 --- a/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/index.test.tsx @@ -45,6 +45,7 @@ describe('AddFilterToGlobalSearchBar Component', () => { ); beforeEach(() => { + jest.useFakeTimers(); store = createStore( state, SUB_PLUGINS_REDUCER, @@ -159,6 +160,8 @@ describe('AddFilterToGlobalSearchBar Component', () => { wrapper.find('[data-test-subj="withHoverActionsButton"]').simulate('mouseenter'); wrapper.update(); + jest.runAllTimers(); + wrapper.update(); wrapper .find('[data-test-subj="hover-actions-container"] [data-euiicon-type]') diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx index e17fc7b9ef9bd..ebfa9ac22bdc7 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx @@ -22,6 +22,10 @@ describe('DraggableWrapper', () => { const message = 'draggable wrapper content'; const mount = useMountAppended(); + beforeEach(() => { + jest.useFakeTimers(); + }); + describe('rendering', () => { test('it renders against the snapshot', () => { const wrapper = shallow( @@ -78,6 +82,8 @@ describe('DraggableWrapper', () => { wrapper.find('[data-test-subj="withHoverActionsButton"]').simulate('mouseenter'); wrapper.update(); + jest.runAllTimers(); + wrapper.update(); expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(true); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/provider_container.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/provider_container.tsx index 06cb8ee2e1a46..8db6d073f9687 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/provider_container.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/provider_container.tsx @@ -13,13 +13,6 @@ interface ProviderContainerProps { } const ProviderContainerComponent = styled.div` - &, - &::before, - &::after { - transition: background ${({ theme }) => theme.eui.euiAnimSpeedFast} ease, - color ${({ theme }) => theme.eui.euiAnimSpeedFast} ease; - } - ${({ isDragging }) => !isDragging && css` diff --git a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.test.tsx b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.test.tsx index ffac0496e9f1a..9fda60b1f289d 100644 --- a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.test.tsx @@ -4,20 +4,120 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallow } from 'enzyme'; +import { mount, ReactWrapper, shallow } from 'enzyme'; import React from 'react'; +import { StickyContainer } from 'react-sticky'; import '../../mock/match_media'; import { FiltersGlobal } from './filters_global'; +import { TestProviders } from '../../mock/test_providers'; describe('rendering', () => { test('renders correctly', () => { const wrapper = shallow( - +

      {'Additional filters here.'}

      ); expect(wrapper).toMatchSnapshot(); }); + + describe('full screen mode', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + wrapper = mount( + + + +

      {'Filter content'}

      +
      +
      +
      + ); + }); + + test('it does NOT render the sticky container', () => { + expect(wrapper.find('[data-test-subj="sticky-filters-global-container"]').exists()).toBe( + false + ); + }); + + test('it renders the non-sticky container', () => { + expect(wrapper.find('[data-test-subj="non-sticky-global-container"]').exists()).toBe(true); + }); + + test('it does NOT render the container with a `display: none` style when `show` is true (the default)', () => { + expect( + wrapper.find('[data-test-subj="non-sticky-global-container"]').first() + ).not.toHaveStyleRule('display', 'none'); + }); + }); + + describe('non-full screen mode', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + wrapper = mount( + + + +

      {'Filter content'}

      +
      +
      +
      + ); + }); + + test('it renders the sticky container', () => { + expect(wrapper.find('[data-test-subj="sticky-filters-global-container"]').exists()).toBe( + true + ); + }); + + test('it does NOT render the non-sticky container', () => { + expect(wrapper.find('[data-test-subj="non-sticky-global-container"]').exists()).toBe(false); + }); + + test('it does NOT render the container with a `display: none` style when `show` is true (the default)', () => { + expect( + wrapper.find('[data-test-subj="sticky-filters-global-container"]').first() + ).not.toHaveStyleRule('display', 'none'); + }); + }); + + describe('when show is false', () => { + test('in full screen mode it renders the container with a `display: none` style', () => { + const wrapper = mount( + + + +

      {'Filter content'}

      +
      +
      +
      + ); + + expect( + wrapper.find('[data-test-subj="non-sticky-global-container"]').first() + ).toHaveStyleRule('display', 'none'); + }); + + test('in non-full screen mode it renders the container with a `display: none` style', () => { + const wrapper = mount( + + + +

      {'Filter content'}

      +
      +
      +
      + ); + + expect( + wrapper.find('[data-test-subj="sticky-filters-global-container"]').first() + ).toHaveStyleRule('display', 'none'); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx index b52438486406e..80e7209492fa5 100644 --- a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx +++ b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx @@ -47,20 +47,33 @@ const FiltersGlobalContainer = styled.header<{ show: boolean }>` FiltersGlobalContainer.displayName = 'FiltersGlobalContainer'; +const NO_STYLE: React.CSSProperties = {}; + export interface FiltersGlobalProps { children: React.ReactNode; + globalFullScreen: boolean; show?: boolean; } -export const FiltersGlobal = React.memo(({ children, show = true }) => ( - - {({ style, isSticky }) => ( - - +export const FiltersGlobal = React.memo( + ({ children, globalFullScreen, show = true }) => + globalFullScreen ? ( + + {children} - )} - -)); + ) : ( + + {({ style, isSticky }) => ( + + + {children} + + + )} + + ) +); + FiltersGlobal.displayName = 'FiltersGlobal'; diff --git a/x-pack/plugins/security_solution/public/common/components/page/index.tsx b/x-pack/plugins/security_solution/public/common/components/page/index.tsx index 8737fa95c94a2..8bf0690bfd0ad 100644 --- a/x-pack/plugins/security_solution/public/common/components/page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/page/index.tsx @@ -7,7 +7,10 @@ import { EuiBadge, EuiDescriptionList, EuiFlexGroup, EuiIcon, EuiPage } from '@elastic/eui'; import styled, { createGlobalStyle } from 'styled-components'; -import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants'; +import { + FULL_SCREEN_TOGGLED_CLASS_NAME, + SCROLLING_DISABLED_CLASS_NAME, +} from '../../../../common/constants'; /* SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly @@ -63,6 +66,14 @@ export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimar .${FULL_SCREEN_TOGGLED_CLASS_NAME} { ${({ theme }) => `background-color: ${theme.eui.euiColorPrimary} !important`}; } + + .${SCROLLING_DISABLED_CLASS_NAME} body { + overflow-y: hidden; + } + + .${SCROLLING_DISABLED_CLASS_NAME} #kibana-body { + overflow-y: hidden; + } `; export const DescriptionListStyled = styled(EuiDescriptionList)` diff --git a/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx b/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx index 9e28345ffbbcf..b4abdd4b91805 100644 --- a/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx @@ -10,6 +10,11 @@ import styled from 'styled-components'; import { IS_DRAGGING_CLASS_NAME } from '../drag_and_drop/helpers'; +/** + * To avoid expensive changes to the DOM, delay showing the popover menu + */ +const HOVER_INTENT_DELAY = 100; // ms + // eslint-disable-next-line @typescript-eslint/no-explicit-any const WithHoverActionsPopover = (styled(EuiPopover as any)` .euiPopover__anchor { @@ -51,18 +56,27 @@ export const WithHoverActions = React.memo( ({ alwaysShow = false, closePopOverTrigger, hoverContent, render }) => { const [isOpen, setIsOpen] = useState(hoverContent != null && alwaysShow); const [showHoverContent, setShowHoverContent] = useState(false); + const [hoverTimeout, setHoverTimeout] = useState(undefined); + const onMouseEnter = useCallback(() => { - // NOTE: the following read from the DOM is expensive, but not as - // expensive as the default behavior, which adds a div to the body, - // which-in turn performs a more expensive change to the layout - if (!document.body.classList.contains(IS_DRAGGING_CLASS_NAME)) { - setShowHoverContent(true); - } - }, []); + setHoverTimeout( + Number( + setTimeout(() => { + // NOTE: the following read from the DOM is expensive, but not as + // expensive as the default behavior, which adds a div to the body, + // which-in turn performs a more expensive change to the layout + if (!document.body.classList.contains(IS_DRAGGING_CLASS_NAME)) { + setShowHoverContent(true); + } + }, HOVER_INTENT_DELAY) + ) + ); + }, [setHoverTimeout, setShowHoverContent]); const onMouseLeave = useCallback(() => { + clearTimeout(hoverTimeout); setShowHoverContent(false); - }, []); + }, [hoverTimeout, setShowHoverContent]); const content = useMemo( () => ( diff --git a/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx b/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx index b8050034d34a6..aa0d90a216035 100644 --- a/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx @@ -6,6 +6,7 @@ import { useCallback, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { SCROLLING_DISABLED_CLASS_NAME } from '../../../../common/constants'; import { inputsSelectors } from '../../store'; import { inputsActions } from '../../store/actions'; @@ -16,7 +17,16 @@ export const useFullScreen = () => { const timelineFullScreen = useSelector(inputsSelectors.timelineFullScreenSelector) ?? false; const setGlobalFullScreen = useCallback( - (fullScreen: boolean) => dispatch(inputsActions.setFullScreen({ id: 'global', fullScreen })), + (fullScreen: boolean) => { + if (fullScreen) { + document.body.classList.add(SCROLLING_DISABLED_CLASS_NAME); + } else { + document.body.classList.remove(SCROLLING_DISABLED_CLASS_NAME); + setTimeout(() => window.scrollTo(0, 0), 0); + } + + dispatch(inputsActions.setFullScreen({ id: 'global', fullScreen })); + }, [dispatch] ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index 090fdc4980933..8385fcc71682d 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -156,7 +156,10 @@ export const DetectionEnginePageComponent: React.FC = ({ {indicesExist ? ( - + diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 9c130a7d351fa..90424e1fb9dd0 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -366,7 +366,10 @@ export const RuleDetailsPageComponent: FC = ({ {indicesExist ? ( - + diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index 781aa711ff0d9..b6c1727ee6afa 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -104,7 +104,10 @@ const HostDetailsComponent = React.memo( {indicesExist ? ( - + diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index 1219effa5ff6d..1d0b73f80a69a 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -98,7 +98,10 @@ export const HostsComponent = React.memo( {indicesExist ? ( - + diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx index e06f5489a3fc2..42469a4bf29da 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx @@ -90,7 +90,7 @@ export const IPDetailsComponent: React.FC {indicesExist ? ( - + diff --git a/x-pack/plugins/security_solution/public/network/pages/network.tsx b/x-pack/plugins/security_solution/public/network/pages/network.tsx index ca8da4eb711e5..f516f2a2de346 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.tsx @@ -106,7 +106,10 @@ const NetworkComponent = React.memo( {indicesExist ? ( - + diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index 6563f3c2b824d..423aa597a0129 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -71,7 +71,7 @@ const OverviewComponent: React.FC = ({ <> {indicesExist ? ( - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx index ddd5c6f07e8b5..2e48215a89473 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx @@ -28,6 +28,10 @@ const defaultProps = { }; describe('FieldName', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + test('it renders the field name', () => { const wrapper = mount( @@ -48,6 +52,8 @@ describe('FieldName', () => { ); wrapper.find('[data-test-subj="withHoverActionsButton"]').at(0).simulate('mouseenter'); wrapper.update(); + jest.runAllTimers(); + wrapper.update(); expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(true); }); From f4104743e388c9cec6f91d153bfc4145f541cabb Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 28 Jul 2020 15:20:24 +0100 Subject: [PATCH 86/96] [Alerting] Control Alerts Management via feature controls & privileges (#72029) This PR removes the alerting and actions ui privileges (alerting:show, actions:show, etc...) and instead relies on the standard Kibana feature control model to decide whether management displays the Alerts Management section under management. --- examples/alerting_example/server/plugin.ts | 13 ++++- x-pack/plugins/actions/server/feature.ts | 9 ++++ .../alerting_builtins/server/feature.ts | 13 ++++- x-pack/plugins/alerts/server/plugin.ts | 10 ++++ x-pack/plugins/apm/server/feature.ts | 9 ++++ x-pack/plugins/infra/server/features.ts | 13 ++++- .../security_solution/server/plugin.ts | 13 ++++- .../public/application/app.tsx | 18 +++---- .../public/application/home.tsx | 40 ++++++-------- .../public/application/lib/capabilities.ts | 13 ----- .../components/alert_details.test.tsx | 3 -- .../sections/alert_form/alert_form.tsx | 22 ++++---- .../components/alerts_list.test.tsx | 28 ++-------- .../alerts_list/components/alerts_list.tsx | 53 ++++++++++--------- .../triggers_actions_ui/public/plugin.ts | 26 +++------ x-pack/plugins/uptime/server/kibana.index.ts | 13 ++++- 16 files changed, 157 insertions(+), 139 deletions(-) diff --git a/examples/alerting_example/server/plugin.ts b/examples/alerting_example/server/plugin.ts index 49352cc285693..e74cad28f77f4 100644 --- a/examples/alerting_example/server/plugin.ts +++ b/examples/alerting_example/server/plugin.ts @@ -44,6 +44,9 @@ export class AlertingExamplePlugin implements Plugin { + return { + management: { + insightsAndAlerting: { + triggersActions: true, + }, + }, + }; + }); + this.isESOUsingEphemeralEncryptionKey = plugins.encryptedSavedObjects.usingEphemeralEncryptionKey; diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index 38e75f75ad04b..971bc96234376 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -17,6 +17,9 @@ export const APM_FEATURE = { navLinkId: 'apm', app: ['apm', 'kibana'], catalogue: ['apm'], + management: { + insightsAndAlerting: ['triggersActions'], + }, alerting: Object.values(AlertType), // see x-pack/plugins/features/common/feature_kibana_privileges.ts privileges: { @@ -31,6 +34,9 @@ export const APM_FEATURE = { alerting: { all: Object.values(AlertType), }, + management: { + insightsAndAlerting: ['triggersActions'], + }, ui: ['show', 'save', 'alerting:show', 'alerting:save'], }, read: { @@ -44,6 +50,9 @@ export const APM_FEATURE = { alerting: { all: Object.values(AlertType), }, + management: { + insightsAndAlerting: ['triggersActions'], + }, ui: ['show', 'alerting:show', 'alerting:save'], }, }, diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index fdbd1ec894022..3e32cebf19ac2 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -19,6 +19,9 @@ export const METRICS_FEATURE = { navLinkId: 'metrics', app: ['infra', 'metrics', 'kibana'], catalogue: ['infraops'], + management: { + insightsAndAlerting: ['triggersActions'], + }, alerting: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], privileges: { all: { @@ -32,7 +35,10 @@ export const METRICS_FEATURE = { alerting: { all: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], }, - ui: ['show', 'configureSource', 'save', 'alerting:show'], + management: { + insightsAndAlerting: ['triggersActions'], + }, + ui: ['show', 'configureSource', 'save'], }, read: { app: ['infra', 'metrics', 'kibana'], @@ -45,7 +51,10 @@ export const METRICS_FEATURE = { alerting: { all: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], }, - ui: ['show', 'alerting:show'], + management: { + insightsAndAlerting: ['triggersActions'], + }, + ui: ['show'], }, }, }; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 06cd3138ca564..8fc413236dd2c 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -174,6 +174,9 @@ export class Plugin implements IPlugin { }; export const AppWithoutRouter = ({ sectionsRegex }: { sectionsRegex: string }) => { - const { capabilities } = useAppDependencies(); - const canShowAlerts = hasShowAlertsCapability(capabilities); - const DEFAULT_SECTION: Section = canShowAlerts ? 'alerts' : 'connectors'; return ( - {canShowAlerts && ( - - )} - + + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx index eeb8a77717333..15099242b6e17 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx @@ -25,7 +25,7 @@ import { Section, routeToConnectors, routeToAlerts } from './constants'; import { getCurrentBreadcrumb } from './lib/breadcrumb'; import { getCurrentDocTitle } from './lib/doc_title'; import { useAppDependencies } from './app_context'; -import { hasShowActionsCapability, hasShowAlertsCapability } from './lib/capabilities'; +import { hasShowActionsCapability } from './lib/capabilities'; import { ActionsConnectorsList } from './sections/actions_connectors_list/components/actions_connectors_list'; import { AlertsList } from './sections/alerts_list/components/alerts_list'; @@ -45,23 +45,17 @@ export const TriggersActionsUIHome: React.FunctionComponent = []; - if (canShowAlerts) { - tabs.push({ - id: 'alerts', - name: ( - - ), - }); - } + tabs.push({ + id: 'alerts', + name: ( + + ), + }); if (canShowActions) { tabs.push({ @@ -151,17 +145,15 @@ export const TriggersActionsUIHome: React.FunctionComponent )} - {canShowAlerts && ( - ( - - - - )} - /> - )} + ( + + + + )} + /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts index 065a782ee96a2..9e89a38377a4d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { BUILT_IN_ALERTS_FEATURE_ID } from '../../../../alerting_builtins/common'; import { Alert, AlertType } from '../../types'; /** @@ -15,18 +14,6 @@ import { Alert, AlertType } from '../../types'; type Capabilities = Record; -const apps = ['apm', 'siem', 'uptime', 'infrastructure', 'actions', BUILT_IN_ALERTS_FEATURE_ID]; - -function hasCapability(capabilities: Capabilities, capability: string) { - return apps.some((app) => capabilities[app]?.[capability]); -} - -function createCapabilityCheck(capability: string) { - return (capabilities: Capabilities) => hasCapability(capabilities, capability); -} - -export const hasShowAlertsCapability = createCapabilityCheck('alerting:show'); - export const hasShowActionsCapability = (capabilities: Capabilities) => capabilities?.actions?.show; export const hasSaveActionsCapability = (capabilities: Capabilities) => capabilities?.actions?.save; export const hasExecuteActionsCapability = (capabilities: Capabilities) => diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index a620a0db45408..16d1a5c7c9c65 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -29,9 +29,6 @@ jest.mock('../../../app_context', () => ({ http: jest.fn(), capabilities: { get: jest.fn(() => ({})), - securitySolution: { - 'alerting:show': true, - }, }, actionTypeRegistry: jest.fn(), alertTypeRegistry: { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 9d54baf359af5..c0674e6c4a5f7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -244,15 +244,7 @@ export const AlertForm = ({ ) : null} {AlertParamsExpressionComponent ? ( - - - - - - } - > + - ) : ( + ) : alertTypesIndex ? ( + ) : ( + )} ); }; +const CenterJustifiedSpinner = () => ( + + + + + +); + const NoAuthorizedAlertTypes = ({ operation }: { operation: string }) => ( { http: mockes.http, uiSettings: mockes.uiSettings, navigateToApp, - capabilities: { - ...capabilities, - securitySolution: { - 'alerting:show': true, - }, - }, + capabilities, history: scopedHistoryMock.create(), setBreadcrumbs: jest.fn(), actionTypeRegistry: actionTypeRegistry as any, @@ -223,12 +218,7 @@ describe('alerts_list component with items', () => { http: mockes.http, uiSettings: mockes.uiSettings, navigateToApp, - capabilities: { - ...capabilities, - securitySolution: { - 'alerting:show': true, - }, - }, + capabilities, history: scopedHistoryMock.create(), setBreadcrumbs: jest.fn(), actionTypeRegistry: actionTypeRegistry as any, @@ -303,12 +293,7 @@ describe('alerts_list component empty with show only capability', () => { http: mockes.http, uiSettings: mockes.uiSettings, navigateToApp, - capabilities: { - ...capabilities, - securitySolution: { - 'alerting:show': true, - }, - }, + capabilities, history: scopedHistoryMock.create(), setBreadcrumbs: jest.fn(), actionTypeRegistry: { @@ -417,12 +402,7 @@ describe('alerts_list with show only capability', () => { http: mockes.http, uiSettings: mockes.uiSettings, navigateToApp, - capabilities: { - ...capabilities, - securitySolution: { - 'alerting:show': true, - }, - }, + capabilities, history: scopedHistoryMock.create(), setBreadcrumbs: jest.fn(), actionTypeRegistry: actionTypeRegistry as any, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 8cb7afbda0e70..2b2897a2181b1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -93,7 +93,7 @@ export const AlertsList: React.FunctionComponent = () => { useEffect(() => { loadAlertsData(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [page, searchText, typesFilter, actionTypesFilter]); + }, [alertTypesState, page, searchText, typesFilter, actionTypesFilter]); useEffect(() => { (async () => { @@ -136,30 +136,33 @@ export const AlertsList: React.FunctionComponent = () => { }, []); async function loadAlertsData() { - setAlertsState({ ...alertsState, isLoading: true }); - try { - const alertsResponse = await loadAlerts({ - http, - page, - searchText, - typesFilter, - actionTypesFilter, - }); - setAlertsState({ - isLoading: false, - data: alertsResponse.data, - totalItemCount: alertsResponse.total, - }); - } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.unableToLoadAlertsMessage', - { - defaultMessage: 'Unable to load alerts', - } - ), - }); - setAlertsState({ ...alertsState, isLoading: false }); + const hasAnyAuthorizedAlertType = alertTypesState.data.size > 0; + if (hasAnyAuthorizedAlertType) { + setAlertsState({ ...alertsState, isLoading: true }); + try { + const alertsResponse = await loadAlerts({ + http, + page, + searchText, + typesFilter, + actionTypesFilter, + }); + setAlertsState({ + isLoading: false, + data: alertsResponse.data, + totalItemCount: alertsResponse.total, + }); + } catch (e) { + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.unableToLoadAlertsMessage', + { + defaultMessage: 'Unable to load alerts', + } + ), + }); + setAlertsState({ ...alertsState, isLoading: false }); + } } } diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index af4d2784cfa67..25a917c7a1a15 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -14,13 +14,11 @@ import { import { i18n } from '@kbn/i18n'; import { registerBuiltInActionTypes } from './application/components/builtin_action_types'; import { registerBuiltInAlertTypes } from './application/components/builtin_alert_types'; -import { hasShowActionsCapability, hasShowAlertsCapability } from './application/lib/capabilities'; import { ActionTypeModel, AlertTypeModel } from './types'; import { TypeRegistry } from './application/type_registry'; import { ManagementSetup, ManagementAppMountParams, - ManagementApp, } from '../../../../src/plugins/management/public'; import { boot } from './application/boot'; import { ChartsPluginStart } from '../../../../src/plugins/charts/public'; @@ -50,10 +48,14 @@ interface PluginsStart { export class Plugin implements - CorePlugin { + CorePlugin< + TriggersAndActionsUIPublicPluginSetup, + TriggersAndActionsUIPublicPluginStart, + PluginsSetup, + PluginsStart + > { private actionTypeRegistry: TypeRegistry; private alertTypeRegistry: TypeRegistry; - private managementApp?: ManagementApp; constructor(initializerContext: PluginInitializerContext) { const actionTypeRegistry = new TypeRegistry(); @@ -67,7 +69,7 @@ export class Plugin const actionTypeRegistry = this.actionTypeRegistry; const alertTypeRegistry = this.alertTypeRegistry; - this.managementApp = plugins.management.sections.section.insightsAndAlerting.registerApp({ + plugins.management.sections.section.insightsAndAlerting.registerApp({ id: 'triggersActions', title: i18n.translate('xpack.triggersActionsUI.managementSection.displayName', { defaultMessage: 'Alerts and Actions', @@ -116,19 +118,7 @@ export class Plugin }; } - public start(core: CoreStart): TriggersAndActionsUIPublicPluginStart { - const { capabilities } = core.application; - - const canShowActions = hasShowActionsCapability(capabilities); - const canShowAlerts = hasShowAlertsCapability(capabilities); - const managementApp = this.managementApp as ManagementApp; - - // Don't register routes when user doesn't have access to the application - if (canShowActions || canShowAlerts) { - managementApp.enable(); - } else { - managementApp.disable(); - } + public start(): TriggersAndActionsUIPublicPluginStart { return { actionTypeRegistry: this.actionTypeRegistry, alertTypeRegistry: this.alertTypeRegistry, diff --git a/x-pack/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts index a2d5f58bbec14..2bf0d84a49de1 100644 --- a/x-pack/plugins/uptime/server/kibana.index.ts +++ b/x-pack/plugins/uptime/server/kibana.index.ts @@ -35,6 +35,9 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor icon: 'uptimeApp', app: ['uptime', 'kibana'], catalogue: ['uptime'], + management: { + insightsAndAlerting: ['triggersActions'], + }, alerting: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'], privileges: { all: { @@ -48,7 +51,10 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor alerting: { all: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'], }, - ui: ['save', 'configureSettings', 'show', 'alerting:show'], + management: { + insightsAndAlerting: ['triggersActions'], + }, + ui: ['save', 'configureSettings', 'show'], }, read: { app: ['uptime', 'kibana'], @@ -61,7 +67,10 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor alerting: { all: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'], }, - ui: ['show', 'alerting:show'], + management: { + insightsAndAlerting: ['triggersActions'], + }, + ui: ['show'], }, }, }); From 330c966f4f7837d337a46268469ed3528d0062dd Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 28 Jul 2020 16:30:58 +0200 Subject: [PATCH 87/96] [Uptime] Reduce miscellaneous uptime bundle size (#70632) Co-authored-by: Elastic Machine --- .../triggers_actions_ui/public/index.ts | 1 + .../plugins/uptime/common/constants/index.ts | 2 - x-pack/plugins/uptime/public/app.ts | 7 -- x-pack/plugins/uptime/public/apps/index.ts | 7 -- x-pack/plugins/uptime/public/apps/plugin.ts | 39 ++++---- .../render_app.tsx} | 44 ++++----- .../plugins/uptime/public/apps/template.html | 1 - .../uptime/public/{ => apps}/uptime_app.tsx | 23 +++-- .../public/apps/uptime_overview_fetcher.ts | 15 ++- .../components/monitor/ml/manage_ml_job.tsx | 2 +- .../monitor/ml/ml_flyout_container.tsx | 3 +- .../embeddables/__tests__/map_config.test.ts | 2 +- .../location_map/embeddables/map_config.ts | 2 +- .../alerts/alerts_containers/alert_tls.tsx | 3 - .../alerts/anomaly_alert/anomaly_alert.tsx | 3 +- .../alerts/toggle_alert_flyout_button.tsx | 2 +- .../overview/kuery_bar/kuery_bar.tsx | 7 +- .../actions_popover/actions_popover.tsx | 4 +- .../actions_popover/integration_group.tsx | 9 +- .../monitor_status_list.tsx | 5 +- .../monitor_list_status_column.tsx | 5 +- .../contexts/uptime_settings_context.tsx | 2 +- .../public/contexts/uptime_theme_context.tsx | 2 +- x-pack/plugins/uptime/public/index.ts | 2 +- .../__tests__/monitor_status.test.ts | 21 ++--- .../lib/alert_types/duration_anomaly.tsx | 18 +--- .../lazy_wrapper/duration_anomaly.tsx | 32 +++++++ .../lazy_wrapper/monitor_status.tsx | 32 +++++++ .../alert_types/lazy_wrapper/tls_alert.tsx | 32 +++++++ .../lazy_wrapper/validate_monitor_status.ts | 56 +++++++++++ .../public/lib/alert_types/monitor_status.tsx | 94 +++++-------------- .../lib/alert_types/monitor_status_title.tsx | 17 ---- .../uptime/public/lib/alert_types/tls.tsx | 17 +--- .../observability_integration/build_href.ts | 9 +- .../observability_integration/get_apm_href.ts | 3 +- .../get_infra_href.ts | 12 ++- .../get_logging_href.ts | 12 ++- x-pack/plugins/uptime/public/lib/lib.ts | 13 --- .../uptime/public/pages/certificates.tsx | 3 +- .../plugins/uptime/public/pages/settings.tsx | 12 ++- .../plugins/uptime/public/state/api/utils.ts | 6 +- .../public/state/effects/index_status.ts | 2 +- x-pack/plugins/uptime/server/kibana.index.ts | 2 +- .../server/lib/alerts/duration_anomaly.ts | 2 +- .../uptime/server/lib/alerts/status_check.ts | 2 +- .../plugins/uptime/server/lib/alerts/tls.ts | 3 +- 46 files changed, 327 insertions(+), 265 deletions(-) delete mode 100644 x-pack/plugins/uptime/public/app.ts delete mode 100644 x-pack/plugins/uptime/public/apps/index.ts rename x-pack/plugins/uptime/public/{lib/adapters/framework/new_platform_adapter.tsx => apps/render_app.tsx} (72%) delete mode 100644 x-pack/plugins/uptime/public/apps/template.html rename x-pack/plugins/uptime/public/{ => apps}/uptime_app.tsx (84%) create mode 100644 x-pack/plugins/uptime/public/lib/alert_types/lazy_wrapper/duration_anomaly.tsx create mode 100644 x-pack/plugins/uptime/public/lib/alert_types/lazy_wrapper/monitor_status.tsx create mode 100644 x-pack/plugins/uptime/public/lib/alert_types/lazy_wrapper/tls_alert.tsx create mode 100644 x-pack/plugins/uptime/public/lib/alert_types/lazy_wrapper/validate_monitor_status.ts delete mode 100644 x-pack/plugins/uptime/public/lib/alert_types/monitor_status_title.tsx diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index 1048e15eb1184..7808e2a7f608d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -19,6 +19,7 @@ export { ActionType, ActionTypeRegistryContract, AlertTypeParamsExpressionProps, + ValidationResult, ActionVariable, } from './types'; export { diff --git a/x-pack/plugins/uptime/common/constants/index.ts b/x-pack/plugins/uptime/common/constants/index.ts index 0ddb995301266..29ae9e47dfb8a 100644 --- a/x-pack/plugins/uptime/common/constants/index.ts +++ b/x-pack/plugins/uptime/common/constants/index.ts @@ -4,13 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './alerts'; export { CHART_FORMAT_LIMITS } from './chart_format_limits'; export { CLIENT_DEFAULTS } from './client_defaults'; export { CONTEXT_DEFAULTS } from './context_defaults'; export * from './capabilities'; export * from './settings_defaults'; -export { PLUGIN } from './plugin'; export { QUERY } from './query'; export * from './ui'; export * from './rest_api'; diff --git a/x-pack/plugins/uptime/public/app.ts b/x-pack/plugins/uptime/public/app.ts deleted file mode 100644 index b068f8a9becda..0000000000000 --- a/x-pack/plugins/uptime/public/app.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import './apps/index'; diff --git a/x-pack/plugins/uptime/public/apps/index.ts b/x-pack/plugins/uptime/public/apps/index.ts deleted file mode 100644 index 65b80d08d4f20..0000000000000 --- a/x-pack/plugins/uptime/public/apps/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { UptimePlugin } from './plugin'; diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index 9af4dea9dbb44..cf750434ab324 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -12,10 +12,11 @@ import { AppMountParameters, } from 'kibana/public'; import { DEFAULT_APP_CATEGORIES } from '../../../../../src/core/public'; -import { UMFrontendLibs } from '../lib/lib'; -import { PLUGIN } from '../../common/constants'; -import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; -import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public'; + +import { + FeatureCatalogueCategory, + HomePublicPluginSetup, +} from '../../../../../src/plugins/home/public'; import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; import { TriggersAndActionsUIPublicPluginSetup, @@ -26,10 +27,8 @@ import { DataPublicPluginStart, } from '../../../../../src/plugins/data/public'; import { alertTypeInitializers } from '../lib/alert_types'; -import { kibanaService } from '../state/kibana_service'; -import { fetchIndexStatus } from '../state/api'; -import { ObservabilityPluginSetup } from '../../../observability/public'; -import { fetchUptimeOverviewData } from './uptime_overview_fetcher'; +import { FetchDataParams, ObservabilityPluginSetup } from '../../../observability/public'; +import { PLUGIN } from '../../common/constants/plugin'; export interface ClientPluginsSetup { data: DataPublicPluginSetup; @@ -66,14 +65,23 @@ export class UptimePlugin category: FeatureCatalogueCategory.DATA, }); } + const getUptimeDataHelper = async () => { + const [coreStart] = await core.getStartServices(); + const { UptimeDataHelper } = await import('./uptime_overview_fetcher'); + return UptimeDataHelper(coreStart); + }; plugins.observability.dashboard.register({ appName: 'uptime', hasData: async () => { - const status = await fetchIndexStatus(); + const dataHelper = await getUptimeDataHelper(); + const status = await dataHelper.indexStatus(); return status.docCount > 0; }, - fetchData: fetchUptimeOverviewData, + fetchData: async (params: FetchDataParams) => { + const dataHelper = await getUptimeDataHelper(); + return await dataHelper.overviewData(params); + }, }); core.application.register({ @@ -85,22 +93,15 @@ export class UptimePlugin category: DEFAULT_APP_CATEGORIES.observability, mount: async (params: AppMountParameters) => { const [coreStart, corePlugins] = await core.getStartServices(); - const { getKibanaFrameworkAdapter } = await import( - '../lib/adapters/framework/new_platform_adapter' - ); - const { element } = params; + const { renderApp } = await import('./render_app'); - const libs: UMFrontendLibs = { - framework: getKibanaFrameworkAdapter(coreStart, plugins, corePlugins), - }; - return libs.framework.render(element); + return renderApp(coreStart, plugins, corePlugins, params); }, }); } public start(start: CoreStart, plugins: ClientPluginsStart): void { - kibanaService.core = start; alertTypeInitializers.forEach((init) => { const alertInitializer = init({ core: start, diff --git a/x-pack/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx b/x-pack/plugins/uptime/public/apps/render_app.tsx similarity index 72% rename from x-pack/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx rename to x-pack/plugins/uptime/public/apps/render_app.tsx index d6185f2c2589a..f834f8b5cdd3c 100644 --- a/x-pack/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx +++ b/x-pack/plugins/uptime/public/apps/render_app.tsx @@ -4,26 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreStart } from 'src/core/public'; import React from 'react'; import ReactDOM from 'react-dom'; import { i18n as i18nFormatter } from '@kbn/i18n'; -import { UptimeApp, UptimeAppProps } from '../../../uptime_app'; -import { getIntegratedAppAvailability } from './capabilities_adapter'; +import { AppMountParameters, CoreStart } from 'kibana/public'; +import { getIntegratedAppAvailability } from '../lib/adapters/framework/capabilities_adapter'; import { - INTEGRATED_SOLUTIONS, - PLUGIN, DEFAULT_DARK_MODE, DEFAULT_TIMEPICKER_QUICK_RANGES, -} from '../../../../common/constants'; -import { UMFrameworkAdapter } from '../../lib'; -import { ClientPluginsStart, ClientPluginsSetup } from '../../../apps/plugin'; + INTEGRATED_SOLUTIONS, +} from '../../common/constants'; +import { UptimeApp, UptimeAppProps } from './uptime_app'; +import { ClientPluginsSetup, ClientPluginsStart } from './plugin'; +import { PLUGIN } from '../../common/constants/plugin'; -export const getKibanaFrameworkAdapter = ( +export function renderApp( core: CoreStart, plugins: ClientPluginsSetup, - startPlugins: ClientPluginsStart -): UMFrameworkAdapter => { + startPlugins: ClientPluginsStart, + { element }: AppMountParameters +) { const { application: { capabilities }, chrome: { setBadge, setHelpExtension }, @@ -40,17 +40,17 @@ export const getKibanaFrameworkAdapter = ( const canSave = (capabilities.uptime.save ?? false) as boolean; const props: UptimeAppProps = { - basePath: basePath.get(), + plugins, canSave, core, + i18n, + startPlugins, + basePath: basePath.get(), darkMode: core.uiSettings.get(DEFAULT_DARK_MODE), commonlyUsedRanges: core.uiSettings.get(DEFAULT_TIMEPICKER_QUICK_RANGES), - i18n, isApmAvailable: apm, isInfraAvailable: infrastructure, isLogsAvailable: logs, - plugins, - startPlugins, renderGlobalHelpControls: () => setHelpExtension({ appName: i18nFormatter.translate('xpack.uptime.header.appName', { @@ -72,15 +72,9 @@ export const getKibanaFrameworkAdapter = ( setBreadcrumbs: core.chrome.setBreadcrumbs, }; - return { - render: async (element: any) => { - if (element) { - ReactDOM.render(, element); - } + ReactDOM.render(, element); - return () => { - ReactDOM.unmountComponentAtNode(element); - }; - }, + return () => { + ReactDOM.unmountComponentAtNode(element); }; -}; +} diff --git a/x-pack/plugins/uptime/public/apps/template.html b/x-pack/plugins/uptime/public/apps/template.html deleted file mode 100644 index a6fb47048a9b1..0000000000000 --- a/x-pack/plugins/uptime/public/apps/template.html +++ /dev/null @@ -1 +0,0 @@ -
      \ No newline at end of file diff --git a/x-pack/plugins/uptime/public/uptime_app.tsx b/x-pack/plugins/uptime/public/apps/uptime_app.tsx similarity index 84% rename from x-pack/plugins/uptime/public/uptime_app.tsx rename to x-pack/plugins/uptime/public/apps/uptime_app.tsx index 4208d79e761ed..41370f9fff492 100644 --- a/x-pack/plugins/uptime/public/uptime_app.tsx +++ b/x-pack/plugins/uptime/public/apps/uptime_app.tsx @@ -9,24 +9,25 @@ import { i18n } from '@kbn/i18n'; import React, { useEffect } from 'react'; import { Provider as ReduxProvider } from 'react-redux'; import { BrowserRouter as Router } from 'react-router-dom'; -import { I18nStart, ChromeBreadcrumb, CoreStart } from 'src/core/public'; -import { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public'; -import { ClientPluginsSetup, ClientPluginsStart } from './apps/plugin'; -import { UMUpdateBadge } from './lib/lib'; +import { I18nStart, ChromeBreadcrumb, CoreStart } from 'kibana/public'; +import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; +import { ClientPluginsSetup, ClientPluginsStart } from './plugin'; +import { UMUpdateBadge } from '../lib/lib'; import { UptimeRefreshContextProvider, UptimeSettingsContextProvider, UptimeThemeContextProvider, UptimeStartupPluginsContextProvider, -} from './contexts'; -import { CommonlyUsedRange } from './components/common/uptime_date_picker'; -import { setBasePath } from './state/actions'; -import { PageRouter } from './routes'; +} from '../contexts'; +import { CommonlyUsedRange } from '../components/common/uptime_date_picker'; +import { setBasePath } from '../state/actions'; +import { PageRouter } from '../routes'; import { UptimeAlertsContextProvider, UptimeAlertsFlyoutWrapper, -} from './components/overview/alerts'; -import { store } from './state'; +} from '../components/overview/alerts'; +import { store } from '../state'; +import { kibanaService } from '../state/kibana_service'; export interface UptimeAppColors { danger: string; @@ -86,6 +87,8 @@ const Application = (props: UptimeAppProps) => { ); }, [canSave, renderGlobalHelpControls, setBadge]); + kibanaService.core = core; + store.dispatch(setBasePath(basePath)); return ( diff --git a/x-pack/plugins/uptime/public/apps/uptime_overview_fetcher.ts b/x-pack/plugins/uptime/public/apps/uptime_overview_fetcher.ts index d1e394dd4da6b..7e5c18f13b29e 100644 --- a/x-pack/plugins/uptime/public/apps/uptime_overview_fetcher.ts +++ b/x-pack/plugins/uptime/public/apps/uptime_overview_fetcher.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fetchPingHistogram, fetchSnapshotCount } from '../state/api'; +import { CoreStart } from 'kibana/public'; import { UptimeFetchDataResponse, FetchDataParams } from '../../../observability/public'; +import { fetchIndexStatus, fetchPingHistogram, fetchSnapshotCount } from '../state/api'; +import { kibanaService } from '../state/kibana_service'; -export async function fetchUptimeOverviewData({ +async function fetchUptimeOverviewData({ absoluteTime, relativeTime, bucketSize, @@ -52,3 +54,12 @@ export async function fetchUptimeOverviewData({ }; return response; } + +export function UptimeDataHelper(coreStart: CoreStart | null) { + kibanaService.core = coreStart!; + + return { + indexStatus: fetchIndexStatus, + overviewData: fetchUptimeOverviewData, + }; +} diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx index 87496e91c906c..7a2899558891d 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx @@ -8,7 +8,7 @@ import React, { useContext, useState } from 'react'; import { EuiButton, EuiContextMenu, EuiIcon, EuiPopover } from '@elastic/eui'; import { useSelector, useDispatch } from 'react-redux'; -import { CLIENT_ALERT_TYPES } from '../../../../common/constants'; +import { CLIENT_ALERT_TYPES } from '../../../../common/constants/alerts'; import { canDeleteMLJobSelector, hasMLJobSelector, diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx index 84634f328621f..e4fe1901729d3 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx @@ -22,13 +22,14 @@ import { import { MLJobLink } from './ml_job_link'; import * as labels from './translations'; import { MLFlyoutView } from './ml_flyout'; -import { CLIENT_ALERT_TYPES, ML_JOB_ID } from '../../../../common/constants'; +import { ML_JOB_ID } from '../../../../common/constants'; import { UptimeRefreshContext, UptimeSettingsContext } from '../../../contexts'; import { useGetUrlParams } from '../../../hooks'; import { getDynamicSettings } from '../../../state/actions/dynamic_settings'; import { useMonitorId } from '../../../hooks'; import { kibanaService } from '../../../state/kibana_service'; import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; +import { CLIENT_ALERT_TYPES } from '../../../../common/constants/alerts'; interface Props { onClose: () => void; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__tests__/map_config.test.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__tests__/map_config.test.ts index 09a41bd9eb4b9..18b43434da24b 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__tests__/map_config.test.ts +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__tests__/map_config.test.ts @@ -7,7 +7,7 @@ import { getLayerList } from '../map_config'; import { mockLayerList } from './__mocks__/mock'; import { LocationPoint } from '../embedded_map'; -import { UptimeAppColors } from '../../../../../../uptime_app'; +import { UptimeAppColors } from '../../../../../../apps/uptime_app'; jest.mock('uuid', () => { return { diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.ts index e766641102a24..6f9b7e4d39c16 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.ts +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.ts @@ -6,7 +6,7 @@ import lowPolyLayerFeatures from './low_poly_layer.json'; import { LocationPoint } from './embedded_map'; -import { UptimeAppColors } from '../../../../../uptime_app'; +import { UptimeAppColors } from '../../../../../apps/uptime_app'; /** * Returns `Source/Destination Point-to-point` Map LayerList configuration, with a source, diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_tls.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_tls.tsx index c7657c34220fc..70adcdb563bce 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_tls.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_tls.tsx @@ -24,6 +24,3 @@ export const AlertTls: React.FC<{}> = () => { /> ); }; - -// eslint-disable-next-line import/no-default-export -export { AlertTls as default }; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/anomaly_alert.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/anomaly_alert.tsx index 4b84012575ae9..1428a7f526fc2 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/anomaly_alert.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/anomaly_alert.tsx @@ -25,8 +25,7 @@ interface Props { setAlertParams: (key: string, value: any) => void; } -// eslint-disable-next-line import/no-default-export -export default function AnomalyAlertComponent({ setAlertParams, alertParams }: Props) { +export function AnomalyAlertComponent({ setAlertParams, alertParams }: Props) { const [severity, setSeverity] = useState(DEFAULT_SEVERITY); const monitorIdStore = useSelector(monitorIdSelector); diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx index 18514bd92d7a0..067972a452f27 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx @@ -15,7 +15,7 @@ import { import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { CLIENT_ALERT_TYPES } from '../../../../common/constants'; +import { CLIENT_ALERT_TYPES } from '../../../../common/constants/alerts'; import { ToggleFlyoutTranslations } from './translations'; import { ToggleAlertFlyoutButtonProps } from './alerts_containers'; diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx index 9c6a4e5d418a7..9e373949aea12 100644 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx @@ -5,8 +5,7 @@ */ import React, { useState, useEffect } from 'react'; -import { uniqueId, startsWith } from 'lodash'; -import { EuiCallOut } from '@elastic/eui'; +import { EuiCallOut, htmlIdGenerator } from '@elastic/eui'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; import { Typeahead } from './typeahead'; @@ -94,7 +93,7 @@ export function KueryBar({ setState({ ...state, suggestions: [] }); setSuggestionLimit(15); - const currentRequest = uniqueId(); + const currentRequest = htmlIdGenerator()(); currentRequestCheck = currentRequest; try { @@ -116,7 +115,7 @@ export function KueryBar({ }, ], })) || [] - ).filter((suggestion: QuerySuggestion) => !startsWith(suggestion.text, 'span.')); + ).filter((suggestion: QuerySuggestion) => !suggestion.text.startsWith('span.')); if (currentRequest !== currentRequestCheck) { return; } diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx index 2070a374e75d0..9e96f0ca76535 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { EuiPopover, EuiButton } from '@elastic/eui'; @@ -25,7 +24,8 @@ export const ActionsPopoverComponent = ({ }: ActionsPopoverProps) => { const popoverId = `${summary.monitor_id}_popover`; - const monitorUrl: string | undefined = get(summary, 'state.url.full', undefined); + const monitorUrl: string | undefined = summary?.state?.url?.full; + const isPopoverOpen: boolean = !!popoverState && popoverState.open && popoverState.id === popoverId; return ( diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx index 38aa9287b0c47..ff3b5d67375fe 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx @@ -27,9 +27,12 @@ interface IntegrationGroupProps { export const extractSummaryValues = (summary: Pick) => { const domain = summary.state.url?.domain ?? ''; - const podUid = summary.state.summaryPings?.[0]?.kubernetes?.pod?.uid ?? undefined; - const containerId = summary.state.summaryPings?.[0]?.container?.id ?? undefined; - const ip = summary.state.summaryPings?.[0]?.monitor.ip ?? undefined; + + const firstCheck = summary.state.summaryPings?.[0]; + + const podUid = firstCheck?.kubernetes?.pod?.uid ?? undefined; + const containerId = firstCheck?.container?.id ?? undefined; + const ip = firstCheck?.monitor.ip ?? undefined; return { domain, diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.tsx index 334de6e376074..96536a357a450 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.tsx @@ -5,7 +5,6 @@ */ import React from 'react'; -import { upperFirst } from 'lodash'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { LocationLink } from '../../../common/location_link'; @@ -29,9 +28,9 @@ export const MonitorStatusList = ({ summaryPings }: MonitorStatusListProps) => { const location = ping.observer?.geo?.name ?? UNNAMED_LOCATION; if (ping.monitor.status === STATUS.UP) { - upChecks.add(upperFirst(location)); + upChecks.add(location); } else if (ping.monitor.status === STATUS.DOWN) { - downChecks.add(upperFirst(location)); + downChecks.add(location); } }); diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_status_column.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_status_column.tsx index 68ddf512e4d3c..7140211d18807 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_status_column.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_status_column.tsx @@ -7,7 +7,6 @@ import React from 'react'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; -import { upperFirst } from 'lodash'; import styled from 'styled-components'; import { EuiHealth, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; import { parseTimestamp } from './parse_timestamp'; @@ -83,9 +82,9 @@ export const getLocationStatus = (summaryPings: Ping[], status: string) => { const location = summaryPing?.observer?.geo?.name ?? UNNAMED_LOCATION; if (summaryPing.monitor.status === STATUS.UP) { - upPings.add(upperFirst(location)); + upPings.add(location); } else if (summaryPing.monitor.status === STATUS.DOWN) { - downPings.add(upperFirst(location)); + downPings.add(location); } }); diff --git a/x-pack/plugins/uptime/public/contexts/uptime_settings_context.tsx b/x-pack/plugins/uptime/public/contexts/uptime_settings_context.tsx index 142c6e17c5fd9..4c08e76a11aae 100644 --- a/x-pack/plugins/uptime/public/contexts/uptime_settings_context.tsx +++ b/x-pack/plugins/uptime/public/contexts/uptime_settings_context.tsx @@ -5,7 +5,7 @@ */ import React, { createContext, useMemo } from 'react'; -import { UptimeAppProps } from '../uptime_app'; +import { UptimeAppProps } from '../apps/uptime_app'; import { CLIENT_DEFAULTS, CONTEXT_DEFAULTS } from '../../common/constants'; import { CommonlyUsedRange } from '../components/common/uptime_date_picker'; import { useGetUrlParams } from '../hooks'; diff --git a/x-pack/plugins/uptime/public/contexts/uptime_theme_context.tsx b/x-pack/plugins/uptime/public/contexts/uptime_theme_context.tsx index ca2fb50cdbc67..51e8bcaed986f 100644 --- a/x-pack/plugins/uptime/public/contexts/uptime_theme_context.tsx +++ b/x-pack/plugins/uptime/public/contexts/uptime_theme_context.tsx @@ -9,7 +9,7 @@ import React, { createContext, useMemo } from 'react'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; import { DARK_THEME, LIGHT_THEME, PartialTheme, Theme } from '@elastic/charts'; -import { UptimeAppColors } from '../uptime_app'; +import { UptimeAppColors } from '../apps/uptime_app'; export interface UptimeThemeContextValues { colors: UptimeAppColors; diff --git a/x-pack/plugins/uptime/public/index.ts b/x-pack/plugins/uptime/public/index.ts index 48cf2c90ad07b..cd6efa9016830 100644 --- a/x-pack/plugins/uptime/public/index.ts +++ b/x-pack/plugins/uptime/public/index.ts @@ -5,7 +5,7 @@ */ import { PluginInitializerContext } from 'kibana/public'; -import { UptimePlugin } from './apps'; +import { UptimePlugin } from './apps/plugin'; export const plugin = (initializerContext: PluginInitializerContext) => new UptimePlugin(initializerContext); diff --git a/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts b/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts index cfcb414f4815d..e999768d4e55d 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts +++ b/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { validate, initMonitorStatusAlertType } from '../monitor_status'; +import { initMonitorStatusAlertType } from '../monitor_status'; +import { validateMonitorStatusParams as validate } from '../lazy_wrapper/validate_monitor_status'; describe('monitor status alert type', () => { describe('validate', () => { @@ -206,19 +207,11 @@ describe('monitor status alert type', () => { ", "iconClass": "uptimeApp", "id": "xpack.uptime.alerts.monitorStatus", - "name": - - , + "name": , "requiresAppContext": false, "validate": [Function], } diff --git a/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx b/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx index f0eb305461582..c1f802c2d0c91 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx +++ b/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx @@ -5,30 +5,22 @@ */ import React from 'react'; -import { Provider as ReduxProvider } from 'react-redux'; import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; -import { CLIENT_ALERT_TYPES } from '../../../common/constants'; +import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts'; import { DurationAnomalyTranslations } from './translations'; import { AlertTypeInitializer } from '.'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; -import { store } from '../../state'; const { name, defaultActionMessage } = DurationAnomalyTranslations; -const AnomalyAlertExpression = React.lazy(() => - import('../../components/overview/alerts/anomaly_alert/anomaly_alert') -); +const DurationAnomalyAlert = React.lazy(() => import('./lazy_wrapper/duration_anomaly')); + export const initDurationAnomalyAlertType: AlertTypeInitializer = ({ core, plugins, }): AlertTypeModel => ({ id: CLIENT_ALERT_TYPES.DURATION_ANOMALY, iconClass: 'uptimeApp', - alertParamsExpression: (params: any) => ( - - - - - + alertParamsExpression: (params: unknown) => ( + ), name, validate: () => ({ errors: {} }), diff --git a/x-pack/plugins/uptime/public/lib/alert_types/lazy_wrapper/duration_anomaly.tsx b/x-pack/plugins/uptime/public/lib/alert_types/lazy_wrapper/duration_anomaly.tsx new file mode 100644 index 0000000000000..60f2d2e803b7b --- /dev/null +++ b/x-pack/plugins/uptime/public/lib/alert_types/lazy_wrapper/duration_anomaly.tsx @@ -0,0 +1,32 @@ +/* + * 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 { Provider as ReduxProvider } from 'react-redux'; +import { CoreStart } from 'kibana/public'; +import { store } from '../../../state'; +import { AnomalyAlertComponent } from '../../../components/overview/alerts/anomaly_alert/anomaly_alert'; +import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; +import { ClientPluginsStart } from '../../../apps/plugin'; +import { kibanaService } from '../../../state/kibana_service'; + +interface Props { + core: CoreStart; + plugins: ClientPluginsStart; + params: any; +} + +// eslint-disable-next-line import/no-default-export +export default function DurationAnomalyAlert({ core, plugins, params }: Props) { + kibanaService.core = core; + return ( + + + + + + ); +} diff --git a/x-pack/plugins/uptime/public/lib/alert_types/lazy_wrapper/monitor_status.tsx b/x-pack/plugins/uptime/public/lib/alert_types/lazy_wrapper/monitor_status.tsx new file mode 100644 index 0000000000000..f6b10d0fbf968 --- /dev/null +++ b/x-pack/plugins/uptime/public/lib/alert_types/lazy_wrapper/monitor_status.tsx @@ -0,0 +1,32 @@ +/* + * 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 { Provider as ReduxProvider } from 'react-redux'; +import { CoreStart } from 'kibana/public'; +import { store } from '../../../state'; +import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; +import { ClientPluginsStart } from '../../../apps/plugin'; +import { AlertMonitorStatus } from '../../../components/overview/alerts/alerts_containers'; +import { kibanaService } from '../../../state/kibana_service'; + +interface Props { + core: CoreStart; + plugins: ClientPluginsStart; + params: any; +} + +// eslint-disable-next-line import/no-default-export +export default function MonitorStatusAlert({ core, plugins, params }: Props) { + kibanaService.core = core; + return ( + + + + + + ); +} diff --git a/x-pack/plugins/uptime/public/lib/alert_types/lazy_wrapper/tls_alert.tsx b/x-pack/plugins/uptime/public/lib/alert_types/lazy_wrapper/tls_alert.tsx new file mode 100644 index 0000000000000..413734b63ced5 --- /dev/null +++ b/x-pack/plugins/uptime/public/lib/alert_types/lazy_wrapper/tls_alert.tsx @@ -0,0 +1,32 @@ +/* + * 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 { Provider as ReduxProvider } from 'react-redux'; +import { CoreStart } from 'kibana/public'; +import { store } from '../../../state'; +import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; +import { ClientPluginsStart } from '../../../apps/plugin'; +import { AlertTls } from '../../../components/overview/alerts/alerts_containers/alert_tls'; +import { kibanaService } from '../../../state/kibana_service'; + +interface Props { + core: CoreStart; + plugins: ClientPluginsStart; + params: any; +} + +// eslint-disable-next-line import/no-default-export +export default function TLSAlert({ core, plugins, params: _params }: Props) { + kibanaService.core = core; + return ( + + + + + + ); +} diff --git a/x-pack/plugins/uptime/public/lib/alert_types/lazy_wrapper/validate_monitor_status.ts b/x-pack/plugins/uptime/public/lib/alert_types/lazy_wrapper/validate_monitor_status.ts new file mode 100644 index 0000000000000..709669c24ed0a --- /dev/null +++ b/x-pack/plugins/uptime/public/lib/alert_types/lazy_wrapper/validate_monitor_status.ts @@ -0,0 +1,56 @@ +/* + * 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 { PathReporter } from 'io-ts/lib/PathReporter'; +import { isRight } from 'fp-ts/lib/Either'; +import { + AtomicStatusCheckParamsType, + MonitorAvailabilityType, + StatusCheckParamsType, +} from '../../../../common/runtime_types/alerts'; +import { ValidationResult } from '../../../../../triggers_actions_ui/public'; + +export function validateMonitorStatusParams(alertParams: any): ValidationResult { + const errors: Record = {}; + const decoded = AtomicStatusCheckParamsType.decode(alertParams); + const oldDecoded = StatusCheckParamsType.decode(alertParams); + const availabilityDecoded = MonitorAvailabilityType.decode(alertParams); + + if (!isRight(decoded) && !isRight(oldDecoded) && !isRight(availabilityDecoded)) { + return { + errors: { + typeCheckFailure: 'Provided parameters do not conform to the expected type.', + typeCheckParsingMessage: PathReporter.report(decoded), + }, + }; + } + + if ( + !(alertParams.shouldCheckAvailability ?? false) && + !(alertParams.shouldCheckStatus ?? false) + ) { + return { + errors: { + noAlertSelected: 'Alert must check for monitor status or monitor availability.', + }, + }; + } + + if (isRight(decoded) && decoded.right.shouldCheckStatus) { + const { numTimes, timerangeCount } = decoded.right; + if (numTimes < 1) { + errors.invalidNumTimes = 'Number of alert check down times must be an integer greater than 0'; + } + if (isNaN(timerangeCount)) { + errors.timeRangeStartValueNaN = 'Specified time range value must be a number'; + } + if (timerangeCount <= 0) { + errors.invalidTimeRangeValue = 'Time range value must be greater than 0'; + } + } + + return { errors }; +} diff --git a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx index cb24df2357d01..e4da3eb9ef7ae 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx +++ b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx @@ -4,70 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Provider as ReduxProvider } from 'react-redux'; import React from 'react'; -import { isRight } from 'fp-ts/lib/Either'; -import { PathReporter } from 'io-ts/lib/PathReporter'; -import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { AlertTypeModel, ValidationResult } from '../../../../triggers_actions_ui/public'; import { AlertTypeInitializer } from '.'; -import { - AtomicStatusCheckParamsType, - StatusCheckParamsType, - MonitorAvailabilityType, -} from '../../../common/runtime_types'; -import { MonitorStatusTitle } from './monitor_status_title'; -import { CLIENT_ALERT_TYPES } from '../../../common/constants'; -import { MonitorStatusTranslations } from './translations'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; -import { store } from '../../state'; - -export const validate = (alertParams: any) => { - const errors: Record = {}; - const decoded = AtomicStatusCheckParamsType.decode(alertParams); - const oldDecoded = StatusCheckParamsType.decode(alertParams); - const availabilityDecoded = MonitorAvailabilityType.decode(alertParams); - - if (!isRight(decoded) && !isRight(oldDecoded) && !isRight(availabilityDecoded)) { - return { - errors: { - typeCheckFailure: 'Provided parameters do not conform to the expected type.', - typeCheckParsingMessage: PathReporter.report(decoded), - }, - }; - } - - if ( - !(alertParams.shouldCheckAvailability ?? false) && - !(alertParams.shouldCheckStatus ?? false) - ) { - return { - errors: { - noAlertSelected: 'Alert must check for monitor status or monitor availability.', - }, - }; - } - - if (isRight(decoded) && decoded.right.shouldCheckStatus) { - const { numTimes, timerangeCount } = decoded.right; - if (numTimes < 1) { - errors.invalidNumTimes = 'Number of alert check down times must be an integer greater than 0'; - } - if (isNaN(timerangeCount)) { - errors.timeRangeStartValueNaN = 'Specified time range value must be a number'; - } - if (timerangeCount <= 0) { - errors.invalidTimeRangeValue = 'Time range value must be greater than 0'; - } - } - return { errors }; -}; +import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts'; +import { MonitorStatusTranslations } from './translations'; const { defaultActionMessage } = MonitorStatusTranslations; -const AlertMonitorStatus = React.lazy(() => - import('../../components/overview/alerts/alerts_containers/alert_monitor_status') -); +const MonitorStatusAlert = React.lazy(() => import('./lazy_wrapper/monitor_status')); + +let validateFunc: (alertParams: any) => ValidationResult; export const initMonitorStatusAlertType: AlertTypeInitializer = ({ core, @@ -75,21 +24,26 @@ export const initMonitorStatusAlertType: AlertTypeInitializer = ({ }): AlertTypeModel => ({ id: CLIENT_ALERT_TYPES.MONITOR_STATUS, name: ( - - - + ), iconClass: 'uptimeApp', - alertParamsExpression: (params: any) => { - return ( - - - - - - ); + alertParamsExpression: (params: any) => ( + + ), + validate: (alertParams: any) => { + if (!validateFunc) { + (async function loadValidate() { + const { validateMonitorStatusParams } = await import( + './lazy_wrapper/validate_monitor_status' + ); + validateFunc = validateMonitorStatusParams; + })(); + } + return validateFunc && validateFunc(alertParams); }, - validate, defaultActionMessage, requiresAppContext: false, }); diff --git a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status_title.tsx b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status_title.tsx deleted file mode 100644 index 1e2751a4ac388..0000000000000 --- a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status_title.tsx +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export const MonitorStatusTitle = () => { - return ( - - ); -}; diff --git a/x-pack/plugins/uptime/public/lib/alert_types/tls.tsx b/x-pack/plugins/uptime/public/lib/alert_types/tls.tsx index c541ea4ae1331..9019fc216192c 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/tls.tsx +++ b/x-pack/plugins/uptime/public/lib/alert_types/tls.tsx @@ -5,27 +5,18 @@ */ import React from 'react'; -import { Provider as ReduxProvider } from 'react-redux'; import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; -import { CLIENT_ALERT_TYPES } from '../../../common/constants'; +import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts'; import { TlsTranslations } from './translations'; import { AlertTypeInitializer } from '.'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; -import { store } from '../../state'; const { name, defaultActionMessage } = TlsTranslations; -const TlsAlertExpression = React.lazy(() => - import('../../components/overview/alerts/alerts_containers/alert_tls') -); +const TLSAlert = React.lazy(() => import('./lazy_wrapper/tls_alert')); export const initTlsAlertType: AlertTypeInitializer = ({ core, plugins }): AlertTypeModel => ({ id: CLIENT_ALERT_TYPES.TLS, iconClass: 'uptimeApp', - alertParamsExpression: (_params: any) => ( - - - - - + alertParamsExpression: (params: any) => ( + ), name, validate: () => ({ errors: {} }), diff --git a/x-pack/plugins/uptime/public/lib/helper/observability_integration/build_href.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/build_href.ts index 94383262b0acd..8c96a469da492 100644 --- a/x-pack/plugins/uptime/public/lib/helper/observability_integration/build_href.ts +++ b/x-pack/plugins/uptime/public/lib/helper/observability_integration/build_href.ts @@ -4,24 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; import { Ping } from '../../../../common/runtime_types'; /** * Builds URLs to the designated features by extracting values from the provided * monitor object on a given path. Then returns the result of a provided function * to place the value in its rightful place on the URI string. - * @param checks array of summary checks containing the data to extract - * @param path the location on the object of the desired data + * @param summaryPings array of summary checks containing the data to extract + * @param getData the location on the object of the desired data * @param getHref a function that returns the full URL */ export const buildHref = ( summaryPings: Ping[], - path: string, + getData: (ping: Ping) => string | undefined, getHref: (value: string | string[] | undefined) => string | undefined ): string | undefined => { const queryValue = summaryPings - .map((ping) => get(ping, path, undefined)) + .map((ping) => getData(ping)) .filter((value: string | undefined) => value !== undefined); if (queryValue.length === 0) { return getHref(undefined); diff --git a/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_apm_href.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_apm_href.ts index 0ff5a8acb3367..a1d69950cb61a 100644 --- a/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_apm_href.ts +++ b/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_apm_href.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; import { addBasePath } from './add_base_path'; import { MonitorSummary } from '../../../../common/runtime_types'; @@ -17,6 +16,6 @@ export const getApmHref = ( addBasePath( basePath, `/app/apm#/services?kuery=${encodeURI( - `url.domain: "${get(summary, 'state.url.domain')}"` + `url.domain: "${summary?.state?.url?.domain}"` )}&rangeFrom=${dateRangeStart}&rangeTo=${dateRangeEnd}` ); diff --git a/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_infra_href.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_infra_href.ts index 33d24a0f081b4..c225382350eac 100644 --- a/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_infra_href.ts +++ b/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_infra_href.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MonitorSummary } from '../../../../common/runtime_types'; +import { MonitorSummary, Ping } from '../../../../common/runtime_types'; import { addBasePath } from './add_base_path'; import { buildHref } from './build_href'; @@ -22,7 +22,7 @@ export const getInfraContainerHref = ( `/app/metrics/link-to/container-detail/${encodeURIComponent(ret)}` ); }; - return buildHref(summary.state.summaryPings || [], 'container.id', getHref); + return buildHref(summary.state.summaryPings || [], (ping: Ping) => ping?.container?.id, getHref); }; export const getInfraKubernetesHref = ( @@ -37,7 +37,11 @@ export const getInfraKubernetesHref = ( return addBasePath(basePath, `/app/metrics/link-to/pod-detail/${encodeURIComponent(ret)}`); }; - return buildHref(summary.state.summaryPings || [], 'kubernetes.pod.uid', getHref); + return buildHref( + summary.state.summaryPings || [], + (ping: Ping) => ping?.kubernetes?.pod?.uid, + getHref + ); }; export const getInfraIpHref = (summary: MonitorSummary, basePath: string) => { @@ -63,5 +67,5 @@ export const getInfraIpHref = (summary: MonitorSummary, basePath: string) => { `/app/metrics/inventory?waffleFilter=(expression:'${encodeURIComponent(ips)}',kind:kuery)` ); }; - return buildHref(summary.state.summaryPings || [], 'monitor.ip', getHref); + return buildHref(summary.state.summaryPings || [], (ping: Ping) => ping?.monitor?.ip, getHref); }; diff --git a/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_logging_href.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_logging_href.ts index c4fee330e9763..32709882d1d21 100644 --- a/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_logging_href.ts +++ b/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_logging_href.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MonitorSummary } from '../../../../common/runtime_types'; +import { MonitorSummary, Ping } from '../../../../common/runtime_types'; import { addBasePath } from './add_base_path'; import { buildHref } from './build_href'; @@ -22,7 +22,7 @@ export const getLoggingContainerHref = ( `/app/logs?logFilter=${encodeURI(`(expression:'container.id : ${ret}',kind:kuery)`)}` ); }; - return buildHref(summary.state.summaryPings || [], 'container.id', getHref); + return buildHref(summary.state.summaryPings || [], (ping: Ping) => ping?.container?.id, getHref); }; export const getLoggingKubernetesHref = (summary: MonitorSummary, basePath: string) => { @@ -36,7 +36,11 @@ export const getLoggingKubernetesHref = (summary: MonitorSummary, basePath: stri `/app/logs?logFilter=${encodeURI(`(expression:'pod.uid : ${ret}',kind:kuery)`)}` ); }; - return buildHref(summary.state.summaryPings || [], 'kubernetes.pod.uid', getHref); + return buildHref( + summary.state.summaryPings || [], + (ping: Ping) => ping?.kubernetes?.pod?.uid, + getHref + ); }; export const getLoggingIpHref = (summary: MonitorSummary, basePath: string) => { @@ -50,5 +54,5 @@ export const getLoggingIpHref = (summary: MonitorSummary, basePath: string) => { `/app/logs?logFilter=(expression:'${encodeURIComponent(`host.ip : ${ret}`)}',kind:kuery)` ); }; - return buildHref(summary.state.summaryPings || [], 'monitor.ip', getHref); + return buildHref(summary.state.summaryPings || [], (ping: Ping) => ping?.monitor?.ip, getHref); }; diff --git a/x-pack/plugins/uptime/public/lib/lib.ts b/x-pack/plugins/uptime/public/lib/lib.ts index 187dcee7adb1a..ac95f018a80a2 100644 --- a/x-pack/plugins/uptime/public/lib/lib.ts +++ b/x-pack/plugins/uptime/public/lib/lib.ts @@ -4,19 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReactElement } from 'react'; -import { AppUnmount } from 'kibana/public'; import { UMBadge } from '../badge'; -import { UptimeAppProps } from '../uptime_app'; - -export interface UMFrontendLibs { - framework: UMFrameworkAdapter; -} export type UMUpdateBadge = (badge: UMBadge) => void; - -export type BootstrapUptimeApp = (props: UptimeAppProps) => ReactElement; - -export interface UMFrameworkAdapter { - render(element: any): Promise; -} diff --git a/x-pack/plugins/uptime/public/pages/certificates.tsx b/x-pack/plugins/uptime/public/pages/certificates.tsx index 58a56a5555323..e46d228c6d21f 100644 --- a/x-pack/plugins/uptime/public/pages/certificates.tsx +++ b/x-pack/plugins/uptime/public/pages/certificates.tsx @@ -21,13 +21,14 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { useTrackPageview } from '../../../observability/public'; import { PageHeader } from './page_header'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; -import { OVERVIEW_ROUTE, SETTINGS_ROUTE, CLIENT_ALERT_TYPES } from '../../common/constants'; +import { OVERVIEW_ROUTE, SETTINGS_ROUTE } from '../../common/constants'; import { getDynamicSettings } from '../state/actions/dynamic_settings'; import { UptimeRefreshContext } from '../contexts'; import * as labels from './translations'; import { certificatesSelector, getCertificatesAction } from '../state/certificates/certificates'; import { CertificateList, CertificateSearch, CertSort } from '../components/certificates'; import { ToggleAlertFlyoutButton } from '../components/overview/alerts/alerts_containers'; +import { CLIENT_ALERT_TYPES } from '../../common/constants/alerts'; const DEFAULT_PAGE_SIZE = 10; const LOCAL_STORAGE_KEY = 'xpack.uptime.certList.pageSize'; diff --git a/x-pack/plugins/uptime/public/pages/settings.tsx b/x-pack/plugins/uptime/public/pages/settings.tsx index 602911cd41aab..89c12d0efdac1 100644 --- a/x-pack/plugins/uptime/public/pages/settings.tsx +++ b/x-pack/plugins/uptime/public/pages/settings.tsx @@ -17,7 +17,6 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { useDispatch, useSelector } from 'react-redux'; -import { isEqual } from 'lodash'; import { useHistory } from 'react-router-dom'; import { selectDynamicSettings } from '../state/selectors'; import { getDynamicSettings, setDynamicSettings } from '../state/actions/dynamic_settings'; @@ -80,6 +79,14 @@ const getFieldErrors = (formFields: DynamicSettings | null): SettingsPageFieldEr return null; }; +const isDirtyForm = (formFields: DynamicSettings | null, settings?: DynamicSettings) => { + return ( + settings?.certAgeThreshold !== formFields?.certAgeThreshold || + settings?.certExpirationThreshold !== formFields?.certExpirationThreshold || + settings?.heartbeatIndices !== formFields?.heartbeatIndices + ); +}; + export const SettingsPage: React.FC = () => { const dss = useSelector(selectDynamicSettings); @@ -121,7 +128,8 @@ export const SettingsPage: React.FC = () => { const resetForm = () => setFormFields(dss.settings ? { ...dss.settings } : null); - const isFormDirty = !isEqual(dss.settings, formFields); + const isFormDirty = isDirtyForm(formFields, dss.settings); + const canEdit: boolean = !!useKibana().services?.application?.capabilities.uptime.configureSettings || false; const isFormDisabled = dss.loading || !canEdit; diff --git a/x-pack/plugins/uptime/public/state/api/utils.ts b/x-pack/plugins/uptime/public/state/api/utils.ts index 4f3765275c49a..e0cec56dd52cd 100644 --- a/x-pack/plugins/uptime/public/state/api/utils.ts +++ b/x-pack/plugins/uptime/public/state/api/utils.ts @@ -8,7 +8,11 @@ import { PathReporter } from 'io-ts/lib/PathReporter'; import { isRight } from 'fp-ts/lib/Either'; import { HttpFetchQuery, HttpSetup } from 'src/core/public'; import * as t from 'io-ts'; -import { isObject } from 'lodash'; + +function isObject(value: unknown) { + const type = typeof value; + return value != null && (type === 'object' || type === 'function'); +} // TODO: Copied from https://github.com/elastic/kibana/blob/master/x-pack/plugins/security_solution/common/format_errors.ts // We should figure out a better way to share this diff --git a/x-pack/plugins/uptime/public/state/effects/index_status.ts b/x-pack/plugins/uptime/public/state/effects/index_status.ts index a4b85312849a2..3917159381eb5 100644 --- a/x-pack/plugins/uptime/public/state/effects/index_status.ts +++ b/x-pack/plugins/uptime/public/state/effects/index_status.ts @@ -6,8 +6,8 @@ import { takeLeading } from 'redux-saga/effects'; import { indexStatusAction } from '../actions'; -import { fetchIndexStatus } from '../api'; import { fetchEffectFactory } from './fetch_effect'; +import { fetchIndexStatus } from '../api/index_status'; export function* fetchIndexStatusEffect() { yield takeLeading( diff --git a/x-pack/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts index 2bf0d84a49de1..ab8d7a068b19d 100644 --- a/x-pack/plugins/uptime/server/kibana.index.ts +++ b/x-pack/plugins/uptime/server/kibana.index.ts @@ -5,7 +5,7 @@ */ import { Request, Server } from 'hapi'; -import { PLUGIN } from '../common/constants'; +import { PLUGIN } from '../common/constants/plugin'; import { compose } from './lib/compose/kibana'; import { initUptimeServer } from './uptime_server'; import { UptimeCorePlugins, UptimeCoreSetup } from './lib/adapters/framework'; diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts index 7dd357e99b83d..a71913d0eea9a 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts @@ -8,7 +8,7 @@ import moment from 'moment'; import { schema } from '@kbn/config-schema'; import { ILegacyScopedClusterClient } from 'kibana/server'; import { updateState } from './common'; -import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants'; +import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants/alerts'; import { commonStateTranslations, durationAnomalyTranslations } from './translations'; import { AnomaliesTableRecord } from '../../../../ml/common/types/anomalies'; import { getSeverityType } from '../../../../ml/common/util/anomaly_utils'; diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index 2117ac4b7ed4e..a34d7eb292eef 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -21,7 +21,7 @@ import { MonitorAvailabilityType, DynamicSettings, } from '../../../common/runtime_types'; -import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants'; +import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants/alerts'; import { savedObjectsAdapter } from '../saved_objects'; import { updateState } from './common'; import { commonStateTranslations } from './translations'; diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls.ts b/x-pack/plugins/uptime/server/lib/alerts/tls.ts index 61e738b088d50..d4853ad7a9cb0 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/tls.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/tls.ts @@ -9,7 +9,8 @@ import { schema } from '@kbn/config-schema'; import { UptimeAlertTypeFactory } from './types'; import { savedObjectsAdapter } from '../saved_objects'; import { updateState } from './common'; -import { ACTION_GROUP_DEFINITIONS, DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants'; +import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants/alerts'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants'; import { Cert, CertResult } from '../../../common/runtime_types'; import { commonStateTranslations, tlsTranslations } from './translations'; import { DEFAULT_FROM, DEFAULT_TO } from '../../rest_api/certs/certs'; From 0dbfde4f4d945a92adab66a8895fc0a129af2bd9 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 28 Jul 2020 17:34:40 +0300 Subject: [PATCH 88/96] [Functional Tests] Increase the timeout on getting the legend value on timeseries (#73279) --- test/functional/page_objects/visual_builder_page.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index 8488eb8cd2749..2771982fecdea 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -315,9 +315,9 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro public async getRhythmChartLegendValue(nth = 0) { await PageObjects.visChart.waitForVisualizationRenderingStabilized(); - const metricValue = (await find.allByCssSelector(`.echLegendItem .echLegendItem__extra`))[ - nth - ]; + const metricValue = ( + await find.allByCssSelector(`.echLegendItem .echLegendItem__extra`, 20000) + )[nth]; await metricValue.moveMouseTo(); return await metricValue.getVisibleText(); } From f87d97b629d2497fd6d63dfe5ce5fd6d3a90537e Mon Sep 17 00:00:00 2001 From: Toby Sutor <55087308+toby-sutor@users.noreply.github.com> Date: Tue, 28 Jul 2020 16:35:00 +0200 Subject: [PATCH 89/96] 32 characters requirement for xpack.reporting.encryptionKey (#72593) Similar to https://www.elastic.co/guide/en/kibana/current/alert-action-settings-kb.html#general-alert-action-settings is a 32 character minimum length required for xpack.reporting.encryptionKey --- docs/settings/reporting-settings.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index c83cd068eff5b..b31ae76d28052 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -21,7 +21,7 @@ You can configure `xpack.reporting` settings in your `kibana.yml` to: | Set to `false` to disable the {report-features}. | `xpack.reporting.encryptionKey` - | Set to any text string. By default, {kib} will generate a random key when it + | Set to an alphanumeric, at least 32 characters long text string. By default, {kib} will generate a random key when it starts, which will cause pending reports to fail after restart. Configure this setting to preserve the same key across multiple restarts and multiple instances of {kib}. From 56609049cb51d5a6edddc9929d0cb3bd5bc4cdee Mon Sep 17 00:00:00 2001 From: Toby Sutor <55087308+toby-sutor@users.noreply.github.com> Date: Tue, 28 Jul 2020 16:35:08 +0200 Subject: [PATCH 90/96] 32 characters requirement for xpack.reporting.encryptionKey (#72594) Similar to https://github.com/elastic/kibana/pull/72593 document that the string needs to be at least 32 characters long. --- docs/user/reporting/configuring-reporting.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/reporting/configuring-reporting.asciidoc b/docs/user/reporting/configuring-reporting.asciidoc index ca2d79bb2dec0..6a0c44cf4c2a4 100644 --- a/docs/user/reporting/configuring-reporting.asciidoc +++ b/docs/user/reporting/configuring-reporting.asciidoc @@ -23,7 +23,7 @@ reporting job metadata. To set a static encryption key for reporting, set the `xpack.reporting.encryptionKey` property in the `kibana.yml` -configuration file. You can use any text string as the encryption key. +configuration file. You can use any alphanumeric, at least 32 characters long text string as the encryption key. [source,yaml] -------------------------------------------------------------------------------- From cdb1c0d9a4c86cb1342005767ca697263cfafe39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Tue, 28 Jul 2020 16:44:27 +0200 Subject: [PATCH 91/96] [Logs UI] Check for presence of data instead of presence of indices in overview page fetchers (#73101) This causes the "has data" check for the observability overview page to not only check for the presence of log indices but also of log entries. --- .../log_sources/get_log_source_status.ts | 10 +++- .../public/pages/logs/stream/page_content.tsx | 2 +- .../pages/logs/stream/page_providers.tsx | 2 +- .../public/utils/logs_overview_fetchers.ts | 2 +- .../utils/logs_overview_fetches.test.ts | 48 ++++++++++++------- .../server/graphql/source_status/resolvers.ts | 2 +- .../lib/adapters/framework/adapter_types.ts | 1 + .../elasticsearch_source_status_adapter.ts | 22 +++++++-- .../plugins/infra/server/lib/source_status.ts | 19 +++++--- .../infra/server/routes/log_sources/status.ts | 11 +++-- .../infra/server/routes/source/index.ts | 8 ++-- 11 files changed, 84 insertions(+), 43 deletions(-) diff --git a/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_status.ts b/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_status.ts index b522d86987283..dc4e4f1ea6217 100644 --- a/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_status.ts +++ b/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_status.ts @@ -40,9 +40,17 @@ const logIndexFieldRT = rt.strict({ export type LogIndexField = rt.TypeOf; +const logIndexStatusRT = rt.keyof({ + missing: null, + empty: null, + available: null, +}); + +export type LogIndexStatus = rt.TypeOf; + const logSourceStatusRT = rt.strict({ logIndexFields: rt.array(logIndexFieldRT), - logIndicesExist: rt.boolean, + logIndexStatus: logIndexStatusRT, }); export type LogSourceStatus = rt.TypeOf; diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx index b2a4ce65ab2b6..fe362dcf8da8c 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx @@ -25,7 +25,7 @@ export const StreamPageContent: React.FunctionComponent = () => { return ; } else if (hasFailedLoadingSource) { return ; - } else if (sourceStatus?.logIndicesExist) { + } else if (sourceStatus?.logIndexStatus !== 'missing') { return ; } else { return ; diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx index 82c21f663bc96..1a1cc13576556 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx @@ -107,7 +107,7 @@ export const LogsPageProviders: React.FunctionComponent = ({ children }) => { const { sourceStatus } = useLogSourceContext(); // The providers assume the source is loaded, so short-circuit them otherwise - if (!sourceStatus?.logIndicesExist) { + if (sourceStatus?.logIndexStatus === 'missing') { return <>{children}; } diff --git a/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts b/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts index 53f7e00a3354c..3716e618068a3 100644 --- a/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts +++ b/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts @@ -43,7 +43,7 @@ export function getLogsHasDataFetcher( return async () => { const [core] = await getStartServices(); const sourceStatus = await callFetchLogSourceStatusAPI(DEFAULT_SOURCE_ID, core.http.fetch); - return sourceStatus.data.logIndicesExist; + return sourceStatus.data.logIndexStatus === 'available'; }; } diff --git a/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts b/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts index 6f9e41fbd08f3..a2b4e162756e3 100644 --- a/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts +++ b/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts @@ -3,17 +3,18 @@ * 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 } from 'kibana/public'; import { coreMock } from 'src/core/public/mocks'; import { dataPluginMock } from 'src/plugins/data/public/mocks'; -import { CoreStart } from 'kibana/public'; -import { getLogsHasDataFetcher } from './logs_overview_fetchers'; -import { InfraClientStartDeps, InfraClientStartExports } from '../types'; import { callFetchLogSourceStatusAPI } from '../containers/logs/log_source/api/fetch_log_source_status'; +import { InfraClientStartDeps, InfraClientStartExports } from '../types'; +import { getLogsHasDataFetcher } from './logs_overview_fetchers'; -// Note -// Calls to `.mock*` functions will fail the typecheck because how jest does the mocking. -// The calls will be preluded with a `@ts-expect-error` jest.mock('../containers/logs/log_source/api/fetch_log_source_status'); +const mockedCallFetchLogSourceStatusAPI = callFetchLogSourceStatusAPI as jest.MockedFunction< + typeof callFetchLogSourceStatusAPI +>; function setup() { const core = coreMock.createStart(); @@ -33,36 +34,47 @@ function setup() { describe('Logs UI Observability Homepage Functions', () => { describe('getLogsHasDataFetcher()', () => { beforeEach(() => { - // @ts-expect-error - callFetchLogSourceStatusAPI.mockReset(); + mockedCallFetchLogSourceStatusAPI.mockReset(); }); - it('should return true when some index is present', async () => { + it('should return true when non-empty indices exist', async () => { const { mockedGetStartServices } = setup(); - // @ts-expect-error - callFetchLogSourceStatusAPI.mockResolvedValue({ - data: { logIndexFields: [], logIndicesExist: true }, + mockedCallFetchLogSourceStatusAPI.mockResolvedValue({ + data: { logIndexFields: [], logIndexStatus: 'available' }, }); const hasData = getLogsHasDataFetcher(mockedGetStartServices); const response = await hasData(); - expect(callFetchLogSourceStatusAPI).toHaveBeenCalledTimes(1); + expect(mockedCallFetchLogSourceStatusAPI).toHaveBeenCalledTimes(1); expect(response).toBe(true); }); - it('should return false when no index is present', async () => { + it('should return false when only empty indices exist', async () => { + const { mockedGetStartServices } = setup(); + + mockedCallFetchLogSourceStatusAPI.mockResolvedValue({ + data: { logIndexFields: [], logIndexStatus: 'empty' }, + }); + + const hasData = getLogsHasDataFetcher(mockedGetStartServices); + const response = await hasData(); + + expect(mockedCallFetchLogSourceStatusAPI).toHaveBeenCalledTimes(1); + expect(response).toBe(false); + }); + + it('should return false when no index exists', async () => { const { mockedGetStartServices } = setup(); - // @ts-expect-error - callFetchLogSourceStatusAPI.mockResolvedValue({ - data: { logIndexFields: [], logIndicesExist: false }, + mockedCallFetchLogSourceStatusAPI.mockResolvedValue({ + data: { logIndexFields: [], logIndexStatus: 'missing' }, }); const hasData = getLogsHasDataFetcher(mockedGetStartServices); const response = await hasData(); - expect(callFetchLogSourceStatusAPI).toHaveBeenCalledTimes(1); + expect(mockedCallFetchLogSourceStatusAPI).toHaveBeenCalledTimes(1); expect(response).toBe(false); }); }); diff --git a/x-pack/plugins/infra/server/graphql/source_status/resolvers.ts b/x-pack/plugins/infra/server/graphql/source_status/resolvers.ts index 848d66058e64c..bd92dd0f7da8b 100644 --- a/x-pack/plugins/infra/server/graphql/source_status/resolvers.ts +++ b/x-pack/plugins/infra/server/graphql/source_status/resolvers.ts @@ -73,7 +73,7 @@ export const createSourceStatusResolvers = (libs: { return await libs.sourceStatus.hasLogAlias(req, source.id); }, async logIndicesExist(source, args, { req }) { - return await libs.sourceStatus.hasLogIndices(req, source.id); + return (await libs.sourceStatus.getLogIndexStatus(req, source.id)) !== 'missing'; }, async logIndices(source, args, { req }) { return await libs.sourceStatus.getLogIndexNames(req, source.id); diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 018e5098a4291..117749ae87bbe 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -38,6 +38,7 @@ export interface CallWithRequestParams extends GenericParams { fields?: string | string[]; path?: string; query?: string | object; + track_total_hits?: boolean | number; } export type InfraResponse = Lifecycle.ReturnValue; diff --git a/x-pack/plugins/infra/server/lib/adapters/source_status/elasticsearch_source_status_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/source_status/elasticsearch_source_status_adapter.ts index 9bc58604f12a5..2a61e64c94fcd 100644 --- a/x-pack/plugins/infra/server/lib/adapters/source_status/elasticsearch_source_status_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/source_status/elasticsearch_source_status_adapter.ts @@ -5,7 +5,7 @@ */ import { RequestHandlerContext } from 'src/core/server'; -import { InfraSourceStatusAdapter } from '../../source_status'; +import { InfraSourceStatusAdapter, SourceIndexStatus } from '../../source_status'; import { InfraDatabaseGetIndicesResponse } from '../framework'; import { KibanaFramework } from '../framework/kibana_framework_adapter'; @@ -40,7 +40,10 @@ export class InfraElasticsearchSourceStatusAdapter implements InfraSourceStatusA }); } - public async hasIndices(requestContext: RequestHandlerContext, indexNames: string) { + public async getIndexStatus( + requestContext: RequestHandlerContext, + indexNames: string + ): Promise { return await this.framework .callWithRequest(requestContext, 'search', { ignore_unavailable: true, @@ -48,12 +51,23 @@ export class InfraElasticsearchSourceStatusAdapter implements InfraSourceStatusA index: indexNames, size: 0, terminate_after: 1, + track_total_hits: 1, }) .then( - (response) => response._shards.total > 0, + (response) => { + if (response._shards.total <= 0) { + return 'missing'; + } + + if (response.hits.total.value > 0) { + return 'available'; + } + + return 'empty'; + }, (err) => { if (err.status === 404) { - return false; + return 'missing'; } throw err; } diff --git a/x-pack/plugins/infra/server/lib/source_status.ts b/x-pack/plugins/infra/server/lib/source_status.ts index 9bb953845e5a1..c383d01933562 100644 --- a/x-pack/plugins/infra/server/lib/source_status.ts +++ b/x-pack/plugins/infra/server/lib/source_status.ts @@ -69,19 +69,19 @@ export class InfraSourceStatus { ); return hasAlias; } - public async hasLogIndices( + public async getLogIndexStatus( requestContext: RequestHandlerContext, sourceId: string - ): Promise { + ): Promise { const sourceConfiguration = await this.libs.sources.getSourceConfiguration( requestContext.core.savedObjects.client, sourceId ); - const hasIndices = await this.adapter.hasIndices( + const indexStatus = await this.adapter.getIndexStatus( requestContext, sourceConfiguration.configuration.logAlias ); - return hasIndices; + return indexStatus; } public async hasMetricIndices( requestContext: RequestHandlerContext, @@ -91,16 +91,21 @@ export class InfraSourceStatus { requestContext.core.savedObjects.client, sourceId ); - const hasIndices = await this.adapter.hasIndices( + const indexStatus = await this.adapter.getIndexStatus( requestContext, sourceConfiguration.configuration.metricAlias ); - return hasIndices; + return indexStatus !== 'missing'; } } +export type SourceIndexStatus = 'missing' | 'empty' | 'available'; + export interface InfraSourceStatusAdapter { getIndexNames(requestContext: RequestHandlerContext, aliasName: string): Promise; hasAlias(requestContext: RequestHandlerContext, aliasName: string): Promise; - hasIndices(requestContext: RequestHandlerContext, indexNames: string): Promise; + getIndexStatus( + requestContext: RequestHandlerContext, + indexNames: string + ): Promise; } diff --git a/x-pack/plugins/infra/server/routes/log_sources/status.ts b/x-pack/plugins/infra/server/routes/log_sources/status.ts index 4cd85ecfe23c1..193c3541d740b 100644 --- a/x-pack/plugins/infra/server/routes/log_sources/status.ts +++ b/x-pack/plugins/infra/server/routes/log_sources/status.ts @@ -31,16 +31,17 @@ export const initLogSourceStatusRoutes = ({ const { sourceId } = request.params; try { - const logIndicesExist = await sourceStatus.hasLogIndices(requestContext, sourceId); - const logIndexFields = logIndicesExist - ? await fields.getFields(requestContext, sourceId, InfraIndexType.LOGS) - : []; + const logIndexStatus = await sourceStatus.getLogIndexStatus(requestContext, sourceId); + const logIndexFields = + logIndexStatus !== 'missing' + ? await fields.getFields(requestContext, sourceId, InfraIndexType.LOGS) + : []; return response.ok({ body: getLogSourceStatusSuccessResponsePayloadRT.encode({ data: { - logIndicesExist, logIndexFields, + logIndexStatus, }, }), }); diff --git a/x-pack/plugins/infra/server/routes/source/index.ts b/x-pack/plugins/infra/server/routes/source/index.ts index 2843897071e19..6069d3a35e54c 100644 --- a/x-pack/plugins/infra/server/routes/source/index.ts +++ b/x-pack/plugins/infra/server/routes/source/index.ts @@ -37,9 +37,9 @@ export const initSourceRoute = (libs: InfraBackendLibs) => { try { const { type, sourceId } = request.params; - const [source, logIndicesExist, metricIndicesExist, indexFields] = await Promise.all([ + const [source, logIndexStatus, metricIndicesExist, indexFields] = await Promise.all([ libs.sources.getSourceConfiguration(requestContext.core.savedObjects.client, sourceId), - libs.sourceStatus.hasLogIndices(requestContext, sourceId), + libs.sourceStatus.getLogIndexStatus(requestContext, sourceId), libs.sourceStatus.hasMetricIndices(requestContext, sourceId), libs.fields.getFields(requestContext, sourceId, typeToInfraIndexType(type)), ]); @@ -49,7 +49,7 @@ export const initSourceRoute = (libs: InfraBackendLibs) => { } const status = { - logIndicesExist, + logIndicesExist: logIndexStatus !== 'missing', metricIndicesExist, indexFields, }; @@ -83,7 +83,7 @@ export const initSourceRoute = (libs: InfraBackendLibs) => { const hasData = type === 'metrics' ? await libs.sourceStatus.hasMetricIndices(requestContext, sourceId) - : await libs.sourceStatus.hasLogIndices(requestContext, sourceId); + : (await libs.sourceStatus.getLogIndexStatus(requestContext, sourceId)) !== 'missing'; return response.ok({ body: { hasData }, From 4ede075681fae19701b3d0b004a74b1193493f7a Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 28 Jul 2020 17:51:23 +0300 Subject: [PATCH 92/96] [KP] fix doc generation for platform code (#73407) * fix doc generation for platform code * terminate process if type build failed * update types --- ...lugin-core-server.countresponse._shards.md | 11 ++ ...-plugin-core-server.countresponse.count.md | 11 ++ ...kibana-plugin-core-server.countresponse.md | 20 +++ ...-core-server.deletedocumentresponse._id.md | 11 ++ ...re-server.deletedocumentresponse._index.md | 11 ++ ...e-server.deletedocumentresponse._shards.md | 11 ++ ...ore-server.deletedocumentresponse._type.md | 11 ++ ...-server.deletedocumentresponse._version.md | 11 ++ ...ore-server.deletedocumentresponse.error.md | 13 ++ ...ore-server.deletedocumentresponse.found.md | 11 ++ ...ugin-core-server.deletedocumentresponse.md | 26 ++++ ...re-server.deletedocumentresponse.result.md | 11 ++ ...-plugin-core-server.elasticsearchclient.md | 17 +++ ...gin-core-server.explanation.description.md | 11 ++ ...-plugin-core-server.explanation.details.md | 11 ++ .../kibana-plugin-core-server.explanation.md | 21 +++ ...na-plugin-core-server.explanation.value.md | 11 ++ ...bana-plugin-core-server.getresponse._id.md | 11 ++ ...a-plugin-core-server.getresponse._index.md | 11 ++ ...n-core-server.getresponse._primary_term.md | 11 ++ ...plugin-core-server.getresponse._routing.md | 11 ++ ...-plugin-core-server.getresponse._seq_no.md | 11 ++ ...-plugin-core-server.getresponse._source.md | 11 ++ ...na-plugin-core-server.getresponse._type.md | 11 ++ ...plugin-core-server.getresponse._version.md | 11 ++ ...na-plugin-core-server.getresponse.found.md | 11 ++ .../kibana-plugin-core-server.getresponse.md | 27 ++++ .../core/server/kibana-plugin-core-server.md | 8 + ...er.savedobjectsdeletebynamespaceoptions.md | 2 +- ...objectsdeletebynamespaceoptions.refresh.md | 4 +- ...n-core-server.searchresponse._scroll_id.md | 11 ++ ...ugin-core-server.searchresponse._shards.md | 11 ++ ...core-server.searchresponse.aggregations.md | 11 ++ ...-plugin-core-server.searchresponse.hits.md | 28 ++++ ...ibana-plugin-core-server.searchresponse.md | 24 +++ ...in-core-server.searchresponse.timed_out.md | 11 ++ ...-plugin-core-server.searchresponse.took.md | 11 ++ ...na-plugin-core-server.shardsinfo.failed.md | 11 ++ .../kibana-plugin-core-server.shardsinfo.md | 22 +++ ...a-plugin-core-server.shardsinfo.skipped.md | 11 ++ ...lugin-core-server.shardsinfo.successful.md | 11 ++ ...ana-plugin-core-server.shardsinfo.total.md | 11 ++ ...lugin-core-server.shardsresponse.failed.md | 11 ++ ...ibana-plugin-core-server.shardsresponse.md | 22 +++ ...ugin-core-server.shardsresponse.skipped.md | 11 ++ ...n-core-server.shardsresponse.successful.md | 11 ++ ...plugin-core-server.shardsresponse.total.md | 11 ++ ...plugin-plugins-data-server.isearchsetup.md | 2 +- src/core/public/public.api.md | 122 +-------------- src/core/server/elasticsearch/client/types.ts | 32 +++- src/core/server/elasticsearch/index.ts | 6 +- src/core/server/index.ts | 7 + src/core/server/server.api.md | 142 +++++++++++++++++- src/dev/run_check_published_api_changes.ts | 47 +++--- src/plugins/data/public/public.api.md | 120 +-------------- src/plugins/data/server/server.api.md | 5 + 56 files changed, 811 insertions(+), 280 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.countresponse._shards.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.countresponse.count.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.countresponse.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse._id.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse._index.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse._shards.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse._type.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse._version.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse.error.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse.found.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse.result.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.elasticsearchclient.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.explanation.description.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.explanation.details.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.explanation.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.explanation.value.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.getresponse._id.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.getresponse._index.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.getresponse._primary_term.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.getresponse._routing.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.getresponse._seq_no.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.getresponse._source.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.getresponse._type.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.getresponse._version.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.getresponse.found.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.getresponse.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.searchresponse._scroll_id.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.searchresponse._shards.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.searchresponse.aggregations.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.searchresponse.hits.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.searchresponse.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.searchresponse.timed_out.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.searchresponse.took.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.shardsinfo.failed.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.shardsinfo.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.shardsinfo.skipped.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.shardsinfo.successful.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.shardsinfo.total.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.shardsresponse.failed.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.shardsresponse.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.shardsresponse.skipped.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.shardsresponse.successful.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.shardsresponse.total.md diff --git a/docs/development/core/server/kibana-plugin-core-server.countresponse._shards.md b/docs/development/core/server/kibana-plugin-core-server.countresponse._shards.md new file mode 100644 index 0000000000000..0f31a554e2208 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.countresponse._shards.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CountResponse](./kibana-plugin-core-server.countresponse.md) > [\_shards](./kibana-plugin-core-server.countresponse._shards.md) + +## CountResponse.\_shards property + +Signature: + +```typescript +_shards: ShardsInfo; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.countresponse.count.md b/docs/development/core/server/kibana-plugin-core-server.countresponse.count.md new file mode 100644 index 0000000000000..3cd1a6aaf6644 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.countresponse.count.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CountResponse](./kibana-plugin-core-server.countresponse.md) > [count](./kibana-plugin-core-server.countresponse.count.md) + +## CountResponse.count property + +Signature: + +```typescript +count: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.countresponse.md b/docs/development/core/server/kibana-plugin-core-server.countresponse.md new file mode 100644 index 0000000000000..f8664f4878f46 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.countresponse.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CountResponse](./kibana-plugin-core-server.countresponse.md) + +## CountResponse interface + + +Signature: + +```typescript +export interface CountResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [\_shards](./kibana-plugin-core-server.countresponse._shards.md) | ShardsInfo | | +| [count](./kibana-plugin-core-server.countresponse.count.md) | number | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse._id.md b/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse._id.md new file mode 100644 index 0000000000000..ccc6a76361f26 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse._id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeleteDocumentResponse](./kibana-plugin-core-server.deletedocumentresponse.md) > [\_id](./kibana-plugin-core-server.deletedocumentresponse._id.md) + +## DeleteDocumentResponse.\_id property + +Signature: + +```typescript +_id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse._index.md b/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse._index.md new file mode 100644 index 0000000000000..a9a04bb2b2ed7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse._index.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeleteDocumentResponse](./kibana-plugin-core-server.deletedocumentresponse.md) > [\_index](./kibana-plugin-core-server.deletedocumentresponse._index.md) + +## DeleteDocumentResponse.\_index property + +Signature: + +```typescript +_index: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse._shards.md b/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse._shards.md new file mode 100644 index 0000000000000..e3d5e9208db0a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse._shards.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeleteDocumentResponse](./kibana-plugin-core-server.deletedocumentresponse.md) > [\_shards](./kibana-plugin-core-server.deletedocumentresponse._shards.md) + +## DeleteDocumentResponse.\_shards property + +Signature: + +```typescript +_shards: ShardsResponse; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse._type.md b/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse._type.md new file mode 100644 index 0000000000000..690852e20a76e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse._type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeleteDocumentResponse](./kibana-plugin-core-server.deletedocumentresponse.md) > [\_type](./kibana-plugin-core-server.deletedocumentresponse._type.md) + +## DeleteDocumentResponse.\_type property + +Signature: + +```typescript +_type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse._version.md b/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse._version.md new file mode 100644 index 0000000000000..acfe8ef55ae71 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse._version.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeleteDocumentResponse](./kibana-plugin-core-server.deletedocumentresponse.md) > [\_version](./kibana-plugin-core-server.deletedocumentresponse._version.md) + +## DeleteDocumentResponse.\_version property + +Signature: + +```typescript +_version: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse.error.md b/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse.error.md new file mode 100644 index 0000000000000..aafe850188998 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse.error.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeleteDocumentResponse](./kibana-plugin-core-server.deletedocumentresponse.md) > [error](./kibana-plugin-core-server.deletedocumentresponse.error.md) + +## DeleteDocumentResponse.error property + +Signature: + +```typescript +error?: { + type: string; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse.found.md b/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse.found.md new file mode 100644 index 0000000000000..00bc89bda66ed --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse.found.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeleteDocumentResponse](./kibana-plugin-core-server.deletedocumentresponse.md) > [found](./kibana-plugin-core-server.deletedocumentresponse.found.md) + +## DeleteDocumentResponse.found property + +Signature: + +```typescript +found: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse.md b/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse.md new file mode 100644 index 0000000000000..e8ac7d2fd8ec1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeleteDocumentResponse](./kibana-plugin-core-server.deletedocumentresponse.md) + +## DeleteDocumentResponse interface + + +Signature: + +```typescript +export interface DeleteDocumentResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [\_id](./kibana-plugin-core-server.deletedocumentresponse._id.md) | string | | +| [\_index](./kibana-plugin-core-server.deletedocumentresponse._index.md) | string | | +| [\_shards](./kibana-plugin-core-server.deletedocumentresponse._shards.md) | ShardsResponse | | +| [\_type](./kibana-plugin-core-server.deletedocumentresponse._type.md) | string | | +| [\_version](./kibana-plugin-core-server.deletedocumentresponse._version.md) | number | | +| [error](./kibana-plugin-core-server.deletedocumentresponse.error.md) | {
      type: string;
      } | | +| [found](./kibana-plugin-core-server.deletedocumentresponse.found.md) | boolean | | +| [result](./kibana-plugin-core-server.deletedocumentresponse.result.md) | string | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse.result.md b/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse.result.md new file mode 100644 index 0000000000000..88f7568d3d9bc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse.result.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeleteDocumentResponse](./kibana-plugin-core-server.deletedocumentresponse.md) > [result](./kibana-plugin-core-server.deletedocumentresponse.result.md) + +## DeleteDocumentResponse.result property + +Signature: + +```typescript +result: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchclient.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchclient.md new file mode 100644 index 0000000000000..279262aa6a5ec --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchclient.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchClient](./kibana-plugin-core-server.elasticsearchclient.md) + +## ElasticsearchClient type + +Client used to query the elasticsearch cluster. + +Signature: + +```typescript +export declare type ElasticsearchClient = Omit & { + transport: { + request(params: TransportRequestParams, options?: TransportRequestOptions): TransportRequestPromise; + }; +}; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.explanation.description.md b/docs/development/core/server/kibana-plugin-core-server.explanation.description.md new file mode 100644 index 0000000000000..37fc90f5ac5d8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.explanation.description.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [Explanation](./kibana-plugin-core-server.explanation.md) > [description](./kibana-plugin-core-server.explanation.description.md) + +## Explanation.description property + +Signature: + +```typescript +description: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.explanation.details.md b/docs/development/core/server/kibana-plugin-core-server.explanation.details.md new file mode 100644 index 0000000000000..afba9175d86cf --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.explanation.details.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [Explanation](./kibana-plugin-core-server.explanation.md) > [details](./kibana-plugin-core-server.explanation.details.md) + +## Explanation.details property + +Signature: + +```typescript +details: Explanation[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.explanation.md b/docs/development/core/server/kibana-plugin-core-server.explanation.md new file mode 100644 index 0000000000000..eb18910c4795b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.explanation.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [Explanation](./kibana-plugin-core-server.explanation.md) + +## Explanation interface + + +Signature: + +```typescript +export interface Explanation +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [description](./kibana-plugin-core-server.explanation.description.md) | string | | +| [details](./kibana-plugin-core-server.explanation.details.md) | Explanation[] | | +| [value](./kibana-plugin-core-server.explanation.value.md) | number | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.explanation.value.md b/docs/development/core/server/kibana-plugin-core-server.explanation.value.md new file mode 100644 index 0000000000000..b10f60176b0c8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.explanation.value.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [Explanation](./kibana-plugin-core-server.explanation.md) > [value](./kibana-plugin-core-server.explanation.value.md) + +## Explanation.value property + +Signature: + +```typescript +value: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.getresponse._id.md b/docs/development/core/server/kibana-plugin-core-server.getresponse._id.md new file mode 100644 index 0000000000000..d31b61f3962c8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.getresponse._id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [GetResponse](./kibana-plugin-core-server.getresponse.md) > [\_id](./kibana-plugin-core-server.getresponse._id.md) + +## GetResponse.\_id property + +Signature: + +```typescript +_id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.getresponse._index.md b/docs/development/core/server/kibana-plugin-core-server.getresponse._index.md new file mode 100644 index 0000000000000..0353ec1a17b2c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.getresponse._index.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [GetResponse](./kibana-plugin-core-server.getresponse.md) > [\_index](./kibana-plugin-core-server.getresponse._index.md) + +## GetResponse.\_index property + +Signature: + +```typescript +_index: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.getresponse._primary_term.md b/docs/development/core/server/kibana-plugin-core-server.getresponse._primary_term.md new file mode 100644 index 0000000000000..8412302ab727d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.getresponse._primary_term.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [GetResponse](./kibana-plugin-core-server.getresponse.md) > [\_primary\_term](./kibana-plugin-core-server.getresponse._primary_term.md) + +## GetResponse.\_primary\_term property + +Signature: + +```typescript +_primary_term: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.getresponse._routing.md b/docs/development/core/server/kibana-plugin-core-server.getresponse._routing.md new file mode 100644 index 0000000000000..1af3ed31ee112 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.getresponse._routing.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [GetResponse](./kibana-plugin-core-server.getresponse.md) > [\_routing](./kibana-plugin-core-server.getresponse._routing.md) + +## GetResponse.\_routing property + +Signature: + +```typescript +_routing?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.getresponse._seq_no.md b/docs/development/core/server/kibana-plugin-core-server.getresponse._seq_no.md new file mode 100644 index 0000000000000..e8d72563f8149 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.getresponse._seq_no.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [GetResponse](./kibana-plugin-core-server.getresponse.md) > [\_seq\_no](./kibana-plugin-core-server.getresponse._seq_no.md) + +## GetResponse.\_seq\_no property + +Signature: + +```typescript +_seq_no: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.getresponse._source.md b/docs/development/core/server/kibana-plugin-core-server.getresponse._source.md new file mode 100644 index 0000000000000..97aacb42992a3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.getresponse._source.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [GetResponse](./kibana-plugin-core-server.getresponse.md) > [\_source](./kibana-plugin-core-server.getresponse._source.md) + +## GetResponse.\_source property + +Signature: + +```typescript +_source: T; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.getresponse._type.md b/docs/development/core/server/kibana-plugin-core-server.getresponse._type.md new file mode 100644 index 0000000000000..b3205e2fe91d7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.getresponse._type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [GetResponse](./kibana-plugin-core-server.getresponse.md) > [\_type](./kibana-plugin-core-server.getresponse._type.md) + +## GetResponse.\_type property + +Signature: + +```typescript +_type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.getresponse._version.md b/docs/development/core/server/kibana-plugin-core-server.getresponse._version.md new file mode 100644 index 0000000000000..23d3a8c91f4a2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.getresponse._version.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [GetResponse](./kibana-plugin-core-server.getresponse.md) > [\_version](./kibana-plugin-core-server.getresponse._version.md) + +## GetResponse.\_version property + +Signature: + +```typescript +_version: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.getresponse.found.md b/docs/development/core/server/kibana-plugin-core-server.getresponse.found.md new file mode 100644 index 0000000000000..8d34a3e743cca --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.getresponse.found.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [GetResponse](./kibana-plugin-core-server.getresponse.md) > [found](./kibana-plugin-core-server.getresponse.found.md) + +## GetResponse.found property + +Signature: + +```typescript +found: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.getresponse.md b/docs/development/core/server/kibana-plugin-core-server.getresponse.md new file mode 100644 index 0000000000000..bab3092c6b1fa --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.getresponse.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [GetResponse](./kibana-plugin-core-server.getresponse.md) + +## GetResponse interface + + +Signature: + +```typescript +export interface GetResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [\_id](./kibana-plugin-core-server.getresponse._id.md) | string | | +| [\_index](./kibana-plugin-core-server.getresponse._index.md) | string | | +| [\_primary\_term](./kibana-plugin-core-server.getresponse._primary_term.md) | number | | +| [\_routing](./kibana-plugin-core-server.getresponse._routing.md) | string | | +| [\_seq\_no](./kibana-plugin-core-server.getresponse._seq_no.md) | number | | +| [\_source](./kibana-plugin-core-server.getresponse._source.md) | T | | +| [\_type](./kibana-plugin-core-server.getresponse._type.md) | string | | +| [\_version](./kibana-plugin-core-server.getresponse._version.md) | number | | +| [found](./kibana-plugin-core-server.getresponse.found.md) | boolean | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 61ffc532f0de5..95b7627398b45 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -74,7 +74,9 @@ The plugin integrates with the core system via lifecycle events: `setup` | [CoreSetup](./kibana-plugin-core-server.coresetup.md) | Context passed to the plugins setup method. | | [CoreStart](./kibana-plugin-core-server.corestart.md) | Context passed to the plugins start method. | | [CoreStatus](./kibana-plugin-core-server.corestatus.md) | Status of core services. | +| [CountResponse](./kibana-plugin-core-server.countresponse.md) | | | [CustomHttpResponseOptions](./kibana-plugin-core-server.customhttpresponseoptions.md) | HTTP response parameters for a response with adjustable status code. | +| [DeleteDocumentResponse](./kibana-plugin-core-server.deletedocumentresponse.md) | | | [DeprecationAPIClientParams](./kibana-plugin-core-server.deprecationapiclientparams.md) | | | [DeprecationAPIResponse](./kibana-plugin-core-server.deprecationapiresponse.md) | | | [DeprecationInfo](./kibana-plugin-core-server.deprecationinfo.md) | | @@ -85,7 +87,9 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ElasticsearchStatusMeta](./kibana-plugin-core-server.elasticsearchstatusmeta.md) | | | [EnvironmentMode](./kibana-plugin-core-server.environmentmode.md) | | | [ErrorHttpResponseOptions](./kibana-plugin-core-server.errorhttpresponseoptions.md) | HTTP response parameters | +| [Explanation](./kibana-plugin-core-server.explanation.md) | | | [FakeRequest](./kibana-plugin-core-server.fakerequest.md) | Fake request object created manually by Kibana plugins. | +| [GetResponse](./kibana-plugin-core-server.getresponse.md) | | | [HttpAuth](./kibana-plugin-core-server.httpauth.md) | | | [HttpResources](./kibana-plugin-core-server.httpresources.md) | HttpResources service is responsible for serving static & dynamic assets for Kibana application via HTTP. Provides API allowing plug-ins to respond with: - a pre-configured HTML page bootstrapping Kibana client app - custom HTML page - custom JS script file. | | [HttpResourcesRenderOptions](./kibana-plugin-core-server.httpresourcesrenderoptions.md) | Allows to configure HTTP response parameters | @@ -189,11 +193,14 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsTypeMappingDefinition](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) | Describe a saved object type mapping. | | [SavedObjectsUpdateOptions](./kibana-plugin-core-server.savedobjectsupdateoptions.md) | | | [SavedObjectsUpdateResponse](./kibana-plugin-core-server.savedobjectsupdateresponse.md) | | +| [SearchResponse](./kibana-plugin-core-server.searchresponse.md) | | | [ServiceStatus](./kibana-plugin-core-server.servicestatus.md) | The current status of a service at a point in time. | | [SessionCookieValidationResult](./kibana-plugin-core-server.sessioncookievalidationresult.md) | Return type from a function to validate cookie contents. | | [SessionStorage](./kibana-plugin-core-server.sessionstorage.md) | Provides an interface to store and retrieve data across requests. | | [SessionStorageCookieOptions](./kibana-plugin-core-server.sessionstoragecookieoptions.md) | Configuration used to create HTTP session storage based on top of cookie mechanism. | | [SessionStorageFactory](./kibana-plugin-core-server.sessionstoragefactory.md) | SessionStorage factory to bind one to an incoming request | +| [ShardsInfo](./kibana-plugin-core-server.shardsinfo.md) | | +| [ShardsResponse](./kibana-plugin-core-server.shardsresponse.md) | | | [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) | API for accessing status of Core and this plugin's dependencies as well as for customizing this plugin's status. | | [StringValidationRegex](./kibana-plugin-core-server.stringvalidationregex.md) | StringValidation with regex object | | [StringValidationRegexString](./kibana-plugin-core-server.stringvalidationregexstring.md) | StringValidation as regex string | @@ -228,6 +235,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ConfigDeprecationProvider](./kibana-plugin-core-server.configdeprecationprovider.md) | A provider that should returns a list of [ConfigDeprecation](./kibana-plugin-core-server.configdeprecation.md).See [ConfigDeprecationFactory](./kibana-plugin-core-server.configdeprecationfactory.md) for more usage examples. | | [ConfigPath](./kibana-plugin-core-server.configpath.md) | | | [DestructiveRouteMethod](./kibana-plugin-core-server.destructiveroutemethod.md) | Set of HTTP methods changing the state of the server. | +| [ElasticsearchClient](./kibana-plugin-core-server.elasticsearchclient.md) | Client used to query the elasticsearch cluster. | | [Freezable](./kibana-plugin-core-server.freezable.md) | | | [GetAuthHeaders](./kibana-plugin-core-server.getauthheaders.md) | Get headers to authenticate a user against Elasticsearch. | | [GetAuthState](./kibana-plugin-core-server.getauthstate.md) | Gets authentication state for a request. Returned by auth interceptor. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.md index 3fac0d889c6ce..ba81a3e8c32d0 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.md @@ -15,5 +15,5 @@ export interface SavedObjectsDeleteByNamespaceOptions extends SavedObjectsBaseOp | Property | Type | Description | | --- | --- | --- | -| [refresh](./kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | +| [refresh](./kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.refresh.md) | boolean | The Elasticsearch supports only boolean flag for this operation | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.refresh.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.refresh.md index c67866a5553a0..52b562e8e22b7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.refresh.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.refresh.md @@ -4,10 +4,10 @@ ## SavedObjectsDeleteByNamespaceOptions.refresh property -The Elasticsearch Refresh setting for this operation +The Elasticsearch supports only boolean flag for this operation Signature: ```typescript -refresh?: MutatingOperationRefreshSetting; +refresh?: boolean; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.searchresponse._scroll_id.md b/docs/development/core/server/kibana-plugin-core-server.searchresponse._scroll_id.md new file mode 100644 index 0000000000000..a9dd0e76475fd --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.searchresponse._scroll_id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SearchResponse](./kibana-plugin-core-server.searchresponse.md) > [\_scroll\_id](./kibana-plugin-core-server.searchresponse._scroll_id.md) + +## SearchResponse.\_scroll\_id property + +Signature: + +```typescript +_scroll_id?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.searchresponse._shards.md b/docs/development/core/server/kibana-plugin-core-server.searchresponse._shards.md new file mode 100644 index 0000000000000..e090ad20e8bc8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.searchresponse._shards.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SearchResponse](./kibana-plugin-core-server.searchresponse.md) > [\_shards](./kibana-plugin-core-server.searchresponse._shards.md) + +## SearchResponse.\_shards property + +Signature: + +```typescript +_shards: ShardsResponse; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.searchresponse.aggregations.md b/docs/development/core/server/kibana-plugin-core-server.searchresponse.aggregations.md new file mode 100644 index 0000000000000..686e6f2aa05e9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.searchresponse.aggregations.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SearchResponse](./kibana-plugin-core-server.searchresponse.md) > [aggregations](./kibana-plugin-core-server.searchresponse.aggregations.md) + +## SearchResponse.aggregations property + +Signature: + +```typescript +aggregations?: any; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.searchresponse.hits.md b/docs/development/core/server/kibana-plugin-core-server.searchresponse.hits.md new file mode 100644 index 0000000000000..1629e77425525 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.searchresponse.hits.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SearchResponse](./kibana-plugin-core-server.searchresponse.md) > [hits](./kibana-plugin-core-server.searchresponse.hits.md) + +## SearchResponse.hits property + +Signature: + +```typescript +hits: { + total: number; + max_score: number; + hits: Array<{ + _index: string; + _type: string; + _id: string; + _score: number; + _source: T; + _version?: number; + _explanation?: Explanation; + fields?: any; + highlight?: any; + inner_hits?: any; + matched_queries?: string[]; + sort?: string[]; + }>; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.searchresponse.md b/docs/development/core/server/kibana-plugin-core-server.searchresponse.md new file mode 100644 index 0000000000000..b53cbf0d87f24 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.searchresponse.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SearchResponse](./kibana-plugin-core-server.searchresponse.md) + +## SearchResponse interface + + +Signature: + +```typescript +export interface SearchResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [\_scroll\_id](./kibana-plugin-core-server.searchresponse._scroll_id.md) | string | | +| [\_shards](./kibana-plugin-core-server.searchresponse._shards.md) | ShardsResponse | | +| [aggregations](./kibana-plugin-core-server.searchresponse.aggregations.md) | any | | +| [hits](./kibana-plugin-core-server.searchresponse.hits.md) | {
      total: number;
      max_score: number;
      hits: Array<{
      _index: string;
      _type: string;
      _id: string;
      _score: number;
      _source: T;
      _version?: number;
      _explanation?: Explanation;
      fields?: any;
      highlight?: any;
      inner_hits?: any;
      matched_queries?: string[];
      sort?: string[];
      }>;
      } | | +| [timed\_out](./kibana-plugin-core-server.searchresponse.timed_out.md) | boolean | | +| [took](./kibana-plugin-core-server.searchresponse.took.md) | number | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.searchresponse.timed_out.md b/docs/development/core/server/kibana-plugin-core-server.searchresponse.timed_out.md new file mode 100644 index 0000000000000..a3488117cd874 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.searchresponse.timed_out.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SearchResponse](./kibana-plugin-core-server.searchresponse.md) > [timed\_out](./kibana-plugin-core-server.searchresponse.timed_out.md) + +## SearchResponse.timed\_out property + +Signature: + +```typescript +timed_out: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.searchresponse.took.md b/docs/development/core/server/kibana-plugin-core-server.searchresponse.took.md new file mode 100644 index 0000000000000..8c9c0b0f7c420 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.searchresponse.took.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SearchResponse](./kibana-plugin-core-server.searchresponse.md) > [took](./kibana-plugin-core-server.searchresponse.took.md) + +## SearchResponse.took property + +Signature: + +```typescript +took: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.shardsinfo.failed.md b/docs/development/core/server/kibana-plugin-core-server.shardsinfo.failed.md new file mode 100644 index 0000000000000..a47fc1263be41 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.shardsinfo.failed.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ShardsInfo](./kibana-plugin-core-server.shardsinfo.md) > [failed](./kibana-plugin-core-server.shardsinfo.failed.md) + +## ShardsInfo.failed property + +Signature: + +```typescript +failed: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.shardsinfo.md b/docs/development/core/server/kibana-plugin-core-server.shardsinfo.md new file mode 100644 index 0000000000000..9eafe3792c14a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.shardsinfo.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ShardsInfo](./kibana-plugin-core-server.shardsinfo.md) + +## ShardsInfo interface + + +Signature: + +```typescript +export interface ShardsInfo +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [failed](./kibana-plugin-core-server.shardsinfo.failed.md) | number | | +| [skipped](./kibana-plugin-core-server.shardsinfo.skipped.md) | number | | +| [successful](./kibana-plugin-core-server.shardsinfo.successful.md) | number | | +| [total](./kibana-plugin-core-server.shardsinfo.total.md) | number | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.shardsinfo.skipped.md b/docs/development/core/server/kibana-plugin-core-server.shardsinfo.skipped.md new file mode 100644 index 0000000000000..0c87831edd6ca --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.shardsinfo.skipped.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ShardsInfo](./kibana-plugin-core-server.shardsinfo.md) > [skipped](./kibana-plugin-core-server.shardsinfo.skipped.md) + +## ShardsInfo.skipped property + +Signature: + +```typescript +skipped: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.shardsinfo.successful.md b/docs/development/core/server/kibana-plugin-core-server.shardsinfo.successful.md new file mode 100644 index 0000000000000..c927adb39932a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.shardsinfo.successful.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ShardsInfo](./kibana-plugin-core-server.shardsinfo.md) > [successful](./kibana-plugin-core-server.shardsinfo.successful.md) + +## ShardsInfo.successful property + +Signature: + +```typescript +successful: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.shardsinfo.total.md b/docs/development/core/server/kibana-plugin-core-server.shardsinfo.total.md new file mode 100644 index 0000000000000..820c8a70fd222 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.shardsinfo.total.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ShardsInfo](./kibana-plugin-core-server.shardsinfo.md) > [total](./kibana-plugin-core-server.shardsinfo.total.md) + +## ShardsInfo.total property + +Signature: + +```typescript +total: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.shardsresponse.failed.md b/docs/development/core/server/kibana-plugin-core-server.shardsresponse.failed.md new file mode 100644 index 0000000000000..7f7a173af2e58 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.shardsresponse.failed.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ShardsResponse](./kibana-plugin-core-server.shardsresponse.md) > [failed](./kibana-plugin-core-server.shardsresponse.failed.md) + +## ShardsResponse.failed property + +Signature: + +```typescript +failed: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.shardsresponse.md b/docs/development/core/server/kibana-plugin-core-server.shardsresponse.md new file mode 100644 index 0000000000000..722ffd8efdb57 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.shardsresponse.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ShardsResponse](./kibana-plugin-core-server.shardsresponse.md) + +## ShardsResponse interface + + +Signature: + +```typescript +export interface ShardsResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [failed](./kibana-plugin-core-server.shardsresponse.failed.md) | number | | +| [skipped](./kibana-plugin-core-server.shardsresponse.skipped.md) | number | | +| [successful](./kibana-plugin-core-server.shardsresponse.successful.md) | number | | +| [total](./kibana-plugin-core-server.shardsresponse.total.md) | number | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.shardsresponse.skipped.md b/docs/development/core/server/kibana-plugin-core-server.shardsresponse.skipped.md new file mode 100644 index 0000000000000..b01c3501fe022 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.shardsresponse.skipped.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ShardsResponse](./kibana-plugin-core-server.shardsresponse.md) > [skipped](./kibana-plugin-core-server.shardsresponse.skipped.md) + +## ShardsResponse.skipped property + +Signature: + +```typescript +skipped: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.shardsresponse.successful.md b/docs/development/core/server/kibana-plugin-core-server.shardsresponse.successful.md new file mode 100644 index 0000000000000..23c6ff0519ed7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.shardsresponse.successful.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ShardsResponse](./kibana-plugin-core-server.shardsresponse.md) > [successful](./kibana-plugin-core-server.shardsresponse.successful.md) + +## ShardsResponse.successful property + +Signature: + +```typescript +successful: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.shardsresponse.total.md b/docs/development/core/server/kibana-plugin-core-server.shardsresponse.total.md new file mode 100644 index 0000000000000..e669f6216a10f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.shardsresponse.total.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ShardsResponse](./kibana-plugin-core-server.shardsresponse.md) > [total](./kibana-plugin-core-server.shardsresponse.total.md) + +## ShardsResponse.total property + +Signature: + +```typescript +total: number; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.md index 3afba80064f08..d9749bc44f45a 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.md @@ -14,6 +14,6 @@ export interface ISearchSetup | Property | Type | Description | | --- | --- | --- | -| [registerSearchStrategy](./kibana-plugin-plugins-data-server.isearchsetup.registersearchstrategy.md) | TRegisterSearchStrategy | Extension point exposed for other plugins to register their own search strategies. | +| [registerSearchStrategy](./kibana-plugin-plugins-data-server.isearchsetup.registersearchstrategy.md) | (name: string, strategy: ISearchStrategy) => void | Extension point exposed for other plugins to register their own search strategies. | | [usage](./kibana-plugin-plugins-data-server.isearchsetup.usage.md) | SearchUsage | Used internally for telemetry | diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index c811209dfa80f..9b421e0172df0 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -5,147 +5,35 @@ ```ts import { Action } from 'history'; +import { ApiResponse } from '@elastic/elasticsearch/lib/Transport'; import Boom from 'boom'; -import { BulkIndexDocumentsParams } from 'elasticsearch'; -import { CatAliasesParams } from 'elasticsearch'; -import { CatAllocationParams } from 'elasticsearch'; -import { CatCommonParams } from 'elasticsearch'; -import { CatFielddataParams } from 'elasticsearch'; -import { CatHealthParams } from 'elasticsearch'; -import { CatHelpParams } from 'elasticsearch'; -import { CatIndicesParams } from 'elasticsearch'; -import { CatRecoveryParams } from 'elasticsearch'; -import { CatSegmentsParams } from 'elasticsearch'; -import { CatShardsParams } from 'elasticsearch'; -import { CatSnapshotsParams } from 'elasticsearch'; -import { CatTasksParams } from 'elasticsearch'; -import { CatThreadPoolParams } from 'elasticsearch'; -import { ClearScrollParams } from 'elasticsearch'; -import { Client } from 'elasticsearch'; -import { ClusterAllocationExplainParams } from 'elasticsearch'; -import { ClusterGetSettingsParams } from 'elasticsearch'; -import { ClusterHealthParams } from 'elasticsearch'; -import { ClusterPendingTasksParams } from 'elasticsearch'; -import { ClusterPutSettingsParams } from 'elasticsearch'; -import { ClusterRerouteParams } from 'elasticsearch'; -import { ClusterStateParams } from 'elasticsearch'; -import { ClusterStatsParams } from 'elasticsearch'; -import { CountParams } from 'elasticsearch'; -import { CreateDocumentParams } from 'elasticsearch'; -import { DeleteDocumentByQueryParams } from 'elasticsearch'; -import { DeleteDocumentParams } from 'elasticsearch'; -import { DeleteScriptParams } from 'elasticsearch'; -import { DeleteTemplateParams } from 'elasticsearch'; import { EuiBreadcrumb } from '@elastic/eui'; import { EuiButtonEmptyProps } from '@elastic/eui'; import { EuiConfirmModalProps } from '@elastic/eui'; import { EuiGlobalToastListToast } from '@elastic/eui'; import { ExclusiveUnion } from '@elastic/eui'; -import { ExistsParams } from 'elasticsearch'; -import { ExplainParams } from 'elasticsearch'; -import { FieldStatsParams } from 'elasticsearch'; -import { GenericParams } from 'elasticsearch'; -import { GetParams } from 'elasticsearch'; -import { GetResponse } from 'elasticsearch'; -import { GetScriptParams } from 'elasticsearch'; -import { GetSourceParams } from 'elasticsearch'; -import { GetTemplateParams } from 'elasticsearch'; import { History } from 'history'; import { Href } from 'history'; import { IconType } from '@elastic/eui'; -import { IndexDocumentParams } from 'elasticsearch'; -import { IndicesAnalyzeParams } from 'elasticsearch'; -import { IndicesClearCacheParams } from 'elasticsearch'; -import { IndicesCloseParams } from 'elasticsearch'; -import { IndicesCreateParams } from 'elasticsearch'; -import { IndicesDeleteAliasParams } from 'elasticsearch'; -import { IndicesDeleteParams } from 'elasticsearch'; -import { IndicesDeleteTemplateParams } from 'elasticsearch'; -import { IndicesExistsAliasParams } from 'elasticsearch'; -import { IndicesExistsParams } from 'elasticsearch'; -import { IndicesExistsTemplateParams } from 'elasticsearch'; -import { IndicesExistsTypeParams } from 'elasticsearch'; -import { IndicesFlushParams } from 'elasticsearch'; -import { IndicesFlushSyncedParams } from 'elasticsearch'; -import { IndicesForcemergeParams } from 'elasticsearch'; -import { IndicesGetAliasParams } from 'elasticsearch'; -import { IndicesGetFieldMappingParams } from 'elasticsearch'; -import { IndicesGetMappingParams } from 'elasticsearch'; -import { IndicesGetParams } from 'elasticsearch'; -import { IndicesGetSettingsParams } from 'elasticsearch'; -import { IndicesGetTemplateParams } from 'elasticsearch'; -import { IndicesGetUpgradeParams } from 'elasticsearch'; -import { IndicesOpenParams } from 'elasticsearch'; -import { IndicesPutAliasParams } from 'elasticsearch'; -import { IndicesPutMappingParams } from 'elasticsearch'; -import { IndicesPutSettingsParams } from 'elasticsearch'; -import { IndicesPutTemplateParams } from 'elasticsearch'; -import { IndicesRecoveryParams } from 'elasticsearch'; -import { IndicesRefreshParams } from 'elasticsearch'; -import { IndicesRolloverParams } from 'elasticsearch'; -import { IndicesSegmentsParams } from 'elasticsearch'; -import { IndicesShardStoresParams } from 'elasticsearch'; -import { IndicesShrinkParams } from 'elasticsearch'; -import { IndicesStatsParams } from 'elasticsearch'; -import { IndicesUpdateAliasesParams } from 'elasticsearch'; -import { IndicesUpgradeParams } from 'elasticsearch'; -import { IndicesValidateQueryParams } from 'elasticsearch'; -import { InfoParams } from 'elasticsearch'; -import { IngestDeletePipelineParams } from 'elasticsearch'; -import { IngestGetPipelineParams } from 'elasticsearch'; -import { IngestPutPipelineParams } from 'elasticsearch'; -import { IngestSimulateParams } from 'elasticsearch'; +import { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { KibanaConfigType } from 'src/core/server/kibana_config'; import { Location } from 'history'; import { LocationDescriptorObject } from 'history'; import { MaybePromise } from '@kbn/utility-types'; -import { MGetParams } from 'elasticsearch'; -import { MGetResponse } from 'elasticsearch'; -import { MSearchParams } from 'elasticsearch'; -import { MSearchResponse } from 'elasticsearch'; -import { MSearchTemplateParams } from 'elasticsearch'; -import { MTermVectorsParams } from 'elasticsearch'; -import { NodesHotThreadsParams } from 'elasticsearch'; -import { NodesInfoParams } from 'elasticsearch'; -import { NodesStatsParams } from 'elasticsearch'; import { Observable } from 'rxjs'; import { ParsedQuery } from 'query-string'; import { Path } from 'history'; -import { PingParams } from 'elasticsearch'; import { PublicUiSettingsParams as PublicUiSettingsParams_2 } from 'src/core/server/types'; -import { PutScriptParams } from 'elasticsearch'; -import { PutTemplateParams } from 'elasticsearch'; import React from 'react'; import { RecursiveReadonly } from '@kbn/utility-types'; -import { ReindexParams } from 'elasticsearch'; -import { ReindexRethrottleParams } from 'elasticsearch'; -import { RenderSearchTemplateParams } from 'elasticsearch'; import * as Rx from 'rxjs'; -import { ScrollParams } from 'elasticsearch'; -import { SearchParams } from 'elasticsearch'; -import { SearchResponse } from 'elasticsearch'; -import { SearchShardsParams } from 'elasticsearch'; -import { SearchTemplateParams } from 'elasticsearch'; import { ShallowPromise } from '@kbn/utility-types'; -import { SnapshotCreateParams } from 'elasticsearch'; -import { SnapshotCreateRepositoryParams } from 'elasticsearch'; -import { SnapshotDeleteParams } from 'elasticsearch'; -import { SnapshotDeleteRepositoryParams } from 'elasticsearch'; -import { SnapshotGetParams } from 'elasticsearch'; -import { SnapshotGetRepositoryParams } from 'elasticsearch'; -import { SnapshotRestoreParams } from 'elasticsearch'; -import { SnapshotStatusParams } from 'elasticsearch'; -import { SnapshotVerifyRepositoryParams } from 'elasticsearch'; -import { SuggestParams } from 'elasticsearch'; -import { TasksCancelParams } from 'elasticsearch'; -import { TasksGetParams } from 'elasticsearch'; -import { TasksListParams } from 'elasticsearch'; -import { TermvectorsParams } from 'elasticsearch'; +import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; +import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; +import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; import { UnregisterCallback } from 'history'; -import { UpdateDocumentByQueryParams } from 'elasticsearch'; -import { UpdateDocumentParams } from 'elasticsearch'; import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/types'; // @internal (undocumented) diff --git a/src/core/server/elasticsearch/client/types.ts b/src/core/server/elasticsearch/client/types.ts index 285f52e89a591..827b185672c7c 100644 --- a/src/core/server/elasticsearch/client/types.ts +++ b/src/core/server/elasticsearch/client/types.ts @@ -42,34 +42,50 @@ export type ElasticsearchClient = Omit< }; }; -interface ShardsResponse { +/** + * All response typings are maintained until elasticsearch-js provides them out of the box + * https://github.com/elastic/elasticsearch-js/pull/970 + */ + +/** + * @public + */ +export interface ShardsResponse { total: number; successful: number; failed: number; skipped: number; } -interface Explanation { +/** + * @public + */ +export interface Explanation { value: number; description: string; details: Explanation[]; } -interface ShardsInfo { +/** + * @public + */ +export interface ShardsInfo { total: number; successful: number; skipped: number; failed: number; } +/** + * @public + */ export interface CountResponse { _shards: ShardsInfo; count: number; } /** - * Maintained until elasticsearch provides response typings out of the box - * https://github.com/elastic/elasticsearch-js/pull/970 + * @public */ export interface SearchResponse { took: number; @@ -97,6 +113,9 @@ export interface SearchResponse { aggregations?: any; } +/** + * @public + */ export interface GetResponse { _index: string; _type: string; @@ -109,6 +128,9 @@ export interface GetResponse { _primary_term: number; } +/** + * @public + */ export interface DeleteDocumentResponse { _shards: ShardsResponse; found: boolean; diff --git a/src/core/server/elasticsearch/index.ts b/src/core/server/elasticsearch/index.ts index 32be6e6bf34dd..9359b88434396 100644 --- a/src/core/server/elasticsearch/index.ts +++ b/src/core/server/elasticsearch/index.ts @@ -36,8 +36,12 @@ export { ElasticsearchClientConfig, ElasticsearchClient, IScopedClusterClient, + // responses SearchResponse, + CountResponse, + ShardsInfo, + ShardsResponse, + Explanation, GetResponse, DeleteDocumentResponse, - CountResponse, } from './client'; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index c846e81573acb..f46b41d6b8793 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -110,6 +110,13 @@ export { FakeRequest, ScopeableRequest, ElasticsearchClient, + SearchResponse, + CountResponse, + ShardsInfo, + ShardsResponse, + Explanation, + GetResponse, + DeleteDocumentResponse, } from './elasticsearch'; export * from './elasticsearch/legacy/api_types'; export { diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 4b6bcbc8ad7a0..bb4f2f30ac18f 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -45,7 +45,7 @@ import { ExplainParams } from 'elasticsearch'; import { FieldStatsParams } from 'elasticsearch'; import { GenericParams } from 'elasticsearch'; import { GetParams } from 'elasticsearch'; -import { GetResponse } from 'elasticsearch'; +import { GetResponse as GetResponse_2 } from 'elasticsearch'; import { GetScriptParams } from 'elasticsearch'; import { GetSourceParams } from 'elasticsearch'; import { GetTemplateParams } from 'elasticsearch'; @@ -121,7 +121,7 @@ import { ResponseToolkit } from 'hapi'; import { SchemaTypeError } from '@kbn/config-schema'; import { ScrollParams } from 'elasticsearch'; import { SearchParams } from 'elasticsearch'; -import { SearchResponse } from 'elasticsearch'; +import { SearchResponse as SearchResponse_2 } from 'elasticsearch'; import { SearchShardsParams } from 'elasticsearch'; import { SearchTemplateParams } from 'elasticsearch'; import { Server } from 'hapi'; @@ -532,6 +532,14 @@ export interface CoreStatus { savedObjects: ServiceStatus; } +// @public (undocumented) +export interface CountResponse { + // (undocumented) + count: number; + // (undocumented) + _shards: ShardsInfo; +} + // @public export class CspConfig implements ICspConfig { // @internal @@ -592,6 +600,28 @@ export const DEFAULT_APP_CATEGORIES: Readonly<{ }; }>; +// @public (undocumented) +export interface DeleteDocumentResponse { + // (undocumented) + error?: { + type: string; + }; + // (undocumented) + found: boolean; + // (undocumented) + _id: string; + // (undocumented) + _index: string; + // (undocumented) + result: string; + // (undocumented) + _shards: ShardsResponse; + // (undocumented) + _type: string; + // (undocumented) + _version: number; +} + // @public (undocumented) export interface DeprecationAPIClientParams extends GenericParams { // (undocumented) @@ -642,6 +672,13 @@ export interface DiscoveredPlugin { readonly requiredPlugins: readonly PluginName[]; } +// @public +export type ElasticsearchClient = Omit & { + transport: { + request(params: TransportRequestParams, options?: TransportRequestOptions): TransportRequestPromise; + }; +}; + // @public export class ElasticsearchConfig { constructor(rawConfig: ElasticsearchConfigType); @@ -709,6 +746,16 @@ export interface ErrorHttpResponseOptions { headers?: ResponseHeaders; } +// @public (undocumented) +export interface Explanation { + // (undocumented) + description: string; + // (undocumented) + details: Explanation[]; + // (undocumented) + value: number; +} + // @public export function exportSavedObjectsToStream({ types, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, }: SavedObjectsExportOptions): Promise; @@ -736,6 +783,28 @@ export function getFlattenedObject(rootValue: Record): { [key: string]: any; }; +// @public (undocumented) +export interface GetResponse { + // (undocumented) + found: boolean; + // (undocumented) + _id: string; + // (undocumented) + _index: string; + // (undocumented) + _primary_term: number; + // (undocumented) + _routing?: string; + // (undocumented) + _seq_no: number; + // (undocumented) + _source: T; + // (undocumented) + _type: string; + // (undocumented) + _version: number; +} + // @public export type HandlerContextType> = T extends HandlerFunction ? U : never; @@ -1042,7 +1111,7 @@ export interface LegacyAPICaller { // (undocumented) (endpoint: 'fieldStats', params: FieldStatsParams, options?: LegacyCallAPIOptions): ReturnType; // (undocumented) - (endpoint: 'get', params: GetParams, options?: LegacyCallAPIOptions): Promise>; + (endpoint: 'get', params: GetParams, options?: LegacyCallAPIOptions): Promise>; // (undocumented) (endpoint: 'getScript', params: GetScriptParams, options?: LegacyCallAPIOptions): ReturnType; // (undocumented) @@ -1074,9 +1143,9 @@ export interface LegacyAPICaller { // (undocumented) (endpoint: 'renderSearchTemplate', params: RenderSearchTemplateParams, options?: LegacyCallAPIOptions): ReturnType; // (undocumented) - (endpoint: 'scroll', params: ScrollParams, options?: LegacyCallAPIOptions): Promise>; + (endpoint: 'scroll', params: ScrollParams, options?: LegacyCallAPIOptions): Promise>; // (undocumented) - (endpoint: 'search', params: SearchParams, options?: LegacyCallAPIOptions): Promise>; + (endpoint: 'search', params: SearchParams, options?: LegacyCallAPIOptions): Promise>; // (undocumented) (endpoint: 'searchShards', params: SearchShardsParams, options?: LegacyCallAPIOptions): ReturnType; // (undocumented) @@ -2082,7 +2151,7 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions { // @public (undocumented) export interface SavedObjectsDeleteByNamespaceOptions extends SavedObjectsBaseOptions { - refresh?: MutatingOperationRefreshSetting; + refresh?: boolean; } // @public (undocumented) @@ -2396,7 +2465,7 @@ export class SavedObjectsRepository { // Warning: (ae-forgotten-export) The symbol "KibanaMigrator" needs to be exported by the entry point index.d.ts // // @internal - static createRepository(migrator: KibanaMigrator, typeRegistry: SavedObjectTypeRegistry, indexName: string, callCluster: LegacyAPICaller, includedHiddenTypes?: string[], injectedConstructor?: any): ISavedObjectsRepository; + static createRepository(migrator: KibanaMigrator, typeRegistry: SavedObjectTypeRegistry, indexName: string, client: ElasticsearchClient, includedHiddenTypes?: string[], injectedConstructor?: any): ISavedObjectsRepository; delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>; deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise; deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>; @@ -2412,7 +2481,7 @@ export class SavedObjectsRepository { attributes: any; }>; update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; - } +} // @public export interface SavedObjectsRepositoryFactory { @@ -2552,6 +2621,39 @@ export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial // @public export type ScopeableRequest = KibanaRequest | LegacyRequest | FakeRequest; +// @public (undocumented) +export interface SearchResponse { + // (undocumented) + aggregations?: any; + // (undocumented) + hits: { + total: number; + max_score: number; + hits: Array<{ + _index: string; + _type: string; + _id: string; + _score: number; + _source: T; + _version?: number; + _explanation?: Explanation; + fields?: any; + highlight?: any; + inner_hits?: any; + matched_queries?: string[]; + sort?: string[]; + }>; + }; + // (undocumented) + _scroll_id?: string; + // (undocumented) + _shards: ShardsResponse; + // (undocumented) + timed_out: boolean; + // (undocumented) + took: number; +} + // @public export interface ServiceStatus | unknown = unknown> { detail?: string; @@ -2612,6 +2714,30 @@ export interface SessionStorageFactory { asScoped: (request: KibanaRequest) => SessionStorage; } +// @public (undocumented) +export interface ShardsInfo { + // (undocumented) + failed: number; + // (undocumented) + skipped: number; + // (undocumented) + successful: number; + // (undocumented) + total: number; +} + +// @public (undocumented) +export interface ShardsResponse { + // (undocumented) + failed: number; + // (undocumented) + skipped: number; + // (undocumented) + successful: number; + // (undocumented) + total: number; +} + // @public (undocumented) export type SharedGlobalConfig = RecursiveReadonly<{ kibana: Pick; diff --git a/src/dev/run_check_published_api_changes.ts b/src/dev/run_check_published_api_changes.ts index 0aa450c8b002a..28e8570812915 100644 --- a/src/dev/run_check_published_api_changes.ts +++ b/src/dev/run_check_published_api_changes.ts @@ -17,8 +17,6 @@ * under the License. */ -/* eslint-disable no-console */ - import { ToolingLog } from '@kbn/dev-utils'; import { Extractor, @@ -35,6 +33,11 @@ import fs from 'fs'; import path from 'path'; import getopts from 'getopts'; +const log = new ToolingLog({ + level: 'info', + writeTo: process.stdout, +}); + /* * Step 1: execute build:types * This users tsconfig.types.json to generate types in `target/types` @@ -92,13 +95,13 @@ const apiExtractorConfig = (folder: string): ExtractorConfig => { }, }, }; - const con = ExtractorConfig.prepare({ + const cfg = ExtractorConfig.prepare({ configObject: config, configObjectFullPath: undefined, packageJsonFullPath: path.resolve('package.json'), }); - return con; + return cfg; }; const runBuildTypes = async () => { @@ -108,7 +111,7 @@ const runBuildTypes = async () => { const runApiDocumenter = async (folder: string) => { const sourceFolder = `./build/${folder}`; const targetFolder = `./docs/development/${folder}`; - console.log(`Generating docs from ${sourceFolder} into ${targetFolder}...`); + log.info(`Generating docs from ${sourceFolder} into ${targetFolder}...`); await execa('api-documenter', ['generate', '-i', sourceFolder, '-o', targetFolder], { preferLocal: true, }); @@ -117,7 +120,7 @@ const runApiDocumenter = async (folder: string) => { const renameExtractedApiPackageName = async (folder: string) => { const fname = getReportFileName(folder); const jsonApiFile = `build/${folder}/${fname}.api.json`; - console.log(`Updating ${jsonApiFile}...`); + log.info(`Updating ${jsonApiFile}...`); const json = JSON.parse(fs.readFileSync(jsonApiFile).toString()); json.name = json.canonicalReference = `kibana-plugin-${folder.replace(/\//g, '-')}`; fs.writeFileSync(jsonApiFile, JSON.stringify(json, null, 2)); @@ -127,11 +130,7 @@ const renameExtractedApiPackageName = async (folder: string) => { * Runs api-extractor with a custom logger in order to extract results from the process * */ -const runApiExtractor = ( - log: ToolingLog, - folder: string, - acceptChanges: boolean = false -): ExtractorResult => { +const runApiExtractor = (folder: string, acceptChanges: boolean = false): ExtractorResult => { const config = apiExtractorConfig(folder); const options = { // Indicates that API Extractor is running as part of a local build, @@ -177,13 +176,10 @@ interface Options { filter: string; } -async function run( - folder: string, - { log, opts }: { log: ToolingLog; opts: Options } -): Promise { +async function run(folder: string, { opts }: { opts: Options }): Promise { log.info(`${folder} API: checking for changes in API signature...`); - const { apiReportChanged, succeeded } = runApiExtractor(log, folder, opts.accept); + const { apiReportChanged, succeeded } = runApiExtractor(folder, opts.accept); // If we're not accepting changes and there's a failure, exit. if (!opts.accept && !succeeded) { @@ -209,11 +205,6 @@ async function run( } (async () => { - const log = new ToolingLog({ - level: 'info', - writeTo: process.stdout, - }); - const extraFlags: string[] = []; const opts = (getopts(process.argv.slice(2), { boolean: ['accept', 'docs', 'help'], @@ -276,26 +267,22 @@ async function run( return !(extraFlags.length > 0); } - try { - log.info(`Building types for api extractor...`); - await runBuildTypes(); - } catch (e) { - log.error(e); - return false; - } + log.info('Building types for api extractor...'); + await runBuildTypes(); + log.info('Types for api extractor has been built'); const filteredFolders = folders.filter((folder) => opts.filter.length ? folder.match(opts.filter) : true ); const results = []; for (const folder of filteredFolders) { - results.push(await run(folder, { log, opts })); + results.push(await run(folder, { opts })); } if (results.includes(false)) { process.exitCode = 1; } })().catch((e) => { - console.log(e); + log.error(e); process.exitCode = 1; }); diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 65670bc1cf83e..2b904ed9536e0 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -7,44 +7,15 @@ import { $Values } from '@kbn/utility-types'; import _ from 'lodash'; import { Action } from 'history'; +import { ApiResponse } from '@elastic/elasticsearch/lib/Transport'; import { ApplicationStart } from 'kibana/public'; import { Assign } from '@kbn/utility-types'; import { BehaviorSubject } from 'rxjs'; import Boom from 'boom'; -import { BulkIndexDocumentsParams } from 'elasticsearch'; -import { CatAliasesParams } from 'elasticsearch'; -import { CatAllocationParams } from 'elasticsearch'; -import { CatCommonParams } from 'elasticsearch'; -import { CatFielddataParams } from 'elasticsearch'; -import { CatHealthParams } from 'elasticsearch'; -import { CatHelpParams } from 'elasticsearch'; -import { CatIndicesParams } from 'elasticsearch'; -import { CatRecoveryParams } from 'elasticsearch'; -import { CatSegmentsParams } from 'elasticsearch'; -import { CatShardsParams } from 'elasticsearch'; -import { CatSnapshotsParams } from 'elasticsearch'; -import { CatTasksParams } from 'elasticsearch'; -import { CatThreadPoolParams } from 'elasticsearch'; -import { ClearScrollParams } from 'elasticsearch'; -import { Client } from 'elasticsearch'; -import { ClusterAllocationExplainParams } from 'elasticsearch'; -import { ClusterGetSettingsParams } from 'elasticsearch'; -import { ClusterHealthParams } from 'elasticsearch'; -import { ClusterPendingTasksParams } from 'elasticsearch'; -import { ClusterPutSettingsParams } from 'elasticsearch'; -import { ClusterRerouteParams } from 'elasticsearch'; -import { ClusterStateParams } from 'elasticsearch'; -import { ClusterStatsParams } from 'elasticsearch'; import { Component } from 'react'; import { CoreSetup } from 'src/core/public'; import { CoreStart } from 'kibana/public'; import { CoreStart as CoreStart_2 } from 'src/core/public'; -import { CountParams } from 'elasticsearch'; -import { CreateDocumentParams } from 'elasticsearch'; -import { DeleteDocumentByQueryParams } from 'elasticsearch'; -import { DeleteDocumentParams } from 'elasticsearch'; -import { DeleteScriptParams } from 'elasticsearch'; -import { DeleteTemplateParams } from 'elasticsearch'; import { Ensure } from '@kbn/utility-types'; import { ErrorToastOptions } from 'src/core/public/notifications'; import { EuiBreadcrumb } from '@elastic/eui'; @@ -53,98 +24,33 @@ import { EuiComboBoxProps } from '@elastic/eui'; import { EuiConfirmModalProps } from '@elastic/eui'; import { EuiGlobalToastListToast } from '@elastic/eui'; import { ExclusiveUnion } from '@elastic/eui'; -import { ExistsParams } from 'elasticsearch'; -import { ExplainParams } from 'elasticsearch'; import { ExpressionAstFunction } from 'src/plugins/expressions/common'; import { ExpressionsSetup } from 'src/plugins/expressions/public'; -import { FieldStatsParams } from 'elasticsearch'; -import { GenericParams } from 'elasticsearch'; -import { GetParams } from 'elasticsearch'; -import { GetResponse } from 'elasticsearch'; -import { GetScriptParams } from 'elasticsearch'; -import { GetSourceParams } from 'elasticsearch'; -import { GetTemplateParams } from 'elasticsearch'; import { History } from 'history'; import { Href } from 'history'; import { IconType } from '@elastic/eui'; -import { IndexDocumentParams } from 'elasticsearch'; -import { IndicesAnalyzeParams } from 'elasticsearch'; -import { IndicesClearCacheParams } from 'elasticsearch'; -import { IndicesCloseParams } from 'elasticsearch'; -import { IndicesCreateParams } from 'elasticsearch'; -import { IndicesDeleteAliasParams } from 'elasticsearch'; -import { IndicesDeleteParams } from 'elasticsearch'; -import { IndicesDeleteTemplateParams } from 'elasticsearch'; -import { IndicesExistsAliasParams } from 'elasticsearch'; -import { IndicesExistsParams } from 'elasticsearch'; -import { IndicesExistsTemplateParams } from 'elasticsearch'; -import { IndicesExistsTypeParams } from 'elasticsearch'; -import { IndicesFlushParams } from 'elasticsearch'; -import { IndicesFlushSyncedParams } from 'elasticsearch'; -import { IndicesForcemergeParams } from 'elasticsearch'; -import { IndicesGetAliasParams } from 'elasticsearch'; -import { IndicesGetFieldMappingParams } from 'elasticsearch'; -import { IndicesGetMappingParams } from 'elasticsearch'; -import { IndicesGetParams } from 'elasticsearch'; -import { IndicesGetSettingsParams } from 'elasticsearch'; -import { IndicesGetTemplateParams } from 'elasticsearch'; -import { IndicesGetUpgradeParams } from 'elasticsearch'; -import { IndicesOpenParams } from 'elasticsearch'; -import { IndicesPutAliasParams } from 'elasticsearch'; -import { IndicesPutMappingParams } from 'elasticsearch'; -import { IndicesPutSettingsParams } from 'elasticsearch'; -import { IndicesPutTemplateParams } from 'elasticsearch'; -import { IndicesRecoveryParams } from 'elasticsearch'; -import { IndicesRefreshParams } from 'elasticsearch'; -import { IndicesRolloverParams } from 'elasticsearch'; -import { IndicesSegmentsParams } from 'elasticsearch'; -import { IndicesShardStoresParams } from 'elasticsearch'; -import { IndicesShrinkParams } from 'elasticsearch'; -import { IndicesStatsParams } from 'elasticsearch'; -import { IndicesUpdateAliasesParams } from 'elasticsearch'; -import { IndicesUpgradeParams } from 'elasticsearch'; -import { IndicesValidateQueryParams } from 'elasticsearch'; -import { InfoParams } from 'elasticsearch'; -import { IngestDeletePipelineParams } from 'elasticsearch'; -import { IngestGetPipelineParams } from 'elasticsearch'; -import { IngestPutPipelineParams } from 'elasticsearch'; -import { IngestSimulateParams } from 'elasticsearch'; import { InjectedIntl } from '@kbn/i18n/react'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { IUiSettingsClient } from 'src/core/public'; import { IUiSettingsClient as IUiSettingsClient_3 } from 'kibana/public'; +import { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { KibanaConfigType } from 'src/core/server/kibana_config'; import { Location } from 'history'; import { LocationDescriptorObject } from 'history'; import { MaybePromise } from '@kbn/utility-types'; import { METRIC_TYPE } from '@kbn/analytics'; -import { MGetParams } from 'elasticsearch'; -import { MGetResponse } from 'elasticsearch'; import { Moment } from 'moment'; import moment from 'moment'; -import { MSearchParams } from 'elasticsearch'; -import { MSearchResponse } from 'elasticsearch'; -import { MSearchTemplateParams } from 'elasticsearch'; -import { MTermVectorsParams } from 'elasticsearch'; import { NameList } from 'elasticsearch'; -import { NodesHotThreadsParams } from 'elasticsearch'; -import { NodesInfoParams } from 'elasticsearch'; -import { NodesStatsParams } from 'elasticsearch'; import { Observable } from 'rxjs'; import { Path } from 'history'; -import { PingParams } from 'elasticsearch'; import { Plugin as Plugin_2 } from 'src/core/public'; import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/public'; import { PopoverAnchorPosition } from '@elastic/eui'; import { PublicUiSettingsParams } from 'src/core/server/types'; -import { PutScriptParams } from 'elasticsearch'; -import { PutTemplateParams } from 'elasticsearch'; import React from 'react'; import * as React_2 from 'react'; import { RecursiveReadonly } from '@kbn/utility-types'; -import { ReindexParams } from 'elasticsearch'; -import { ReindexRethrottleParams } from 'elasticsearch'; -import { RenderSearchTemplateParams } from 'elasticsearch'; import { Reporter } from '@kbn/analytics'; import { RequestAdapter } from 'src/plugins/inspector/common'; import { RequestStatistics } from 'src/plugins/inspector/common'; @@ -153,38 +59,22 @@ import * as Rx from 'rxjs'; import { SavedObject } from 'src/core/server'; import { SavedObject as SavedObject_3 } from 'src/core/public'; import { SavedObjectsClientContract } from 'src/core/public'; -import { ScrollParams } from 'elasticsearch'; import { SearchParams } from 'elasticsearch'; import { SearchResponse as SearchResponse_2 } from 'elasticsearch'; -import { SearchShardsParams } from 'elasticsearch'; -import { SearchTemplateParams } from 'elasticsearch'; import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/common'; -import { SnapshotCreateParams } from 'elasticsearch'; -import { SnapshotCreateRepositoryParams } from 'elasticsearch'; -import { SnapshotDeleteParams } from 'elasticsearch'; -import { SnapshotDeleteRepositoryParams } from 'elasticsearch'; -import { SnapshotGetParams } from 'elasticsearch'; -import { SnapshotGetRepositoryParams } from 'elasticsearch'; -import { SnapshotRestoreParams } from 'elasticsearch'; -import { SnapshotStatusParams } from 'elasticsearch'; -import { SnapshotVerifyRepositoryParams } from 'elasticsearch'; import { Subscription } from 'rxjs'; -import { SuggestParams } from 'elasticsearch'; -import { TasksCancelParams } from 'elasticsearch'; -import { TasksGetParams } from 'elasticsearch'; -import { TasksListParams } from 'elasticsearch'; -import { TermvectorsParams } from 'elasticsearch'; import { Toast } from 'kibana/public'; import { ToastInputFields } from 'src/core/public/notifications'; import { ToastsStart } from 'kibana/public'; +import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; +import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; +import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; import { TypeOf } from '@kbn/config-schema'; import { UiActionsSetup } from 'src/plugins/ui_actions/public'; import { UiActionsStart } from 'src/plugins/ui_actions/public'; import { Unit } from '@elastic/datemath'; import { UnregisterCallback } from 'history'; import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; -import { UpdateDocumentByQueryParams } from 'elasticsearch'; -import { UpdateDocumentParams } from 'elasticsearch'; import { UserProvidedValues } from 'src/core/server/types'; // Warning: (ae-forgotten-export) The symbol "AggConfigSerialized" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 99a77ff9aeb10..7ad2f9edd3325 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -4,6 +4,7 @@ ```ts +import { ApiResponse } from '@elastic/elasticsearch/lib/Transport'; import Boom from 'boom'; import { BulkIndexDocumentsParams } from 'elasticsearch'; import { CatAliasesParams } from 'elasticsearch'; @@ -91,6 +92,7 @@ import { IngestDeletePipelineParams } from 'elasticsearch'; import { IngestGetPipelineParams } from 'elasticsearch'; import { IngestPutPipelineParams } from 'elasticsearch'; import { IngestSimulateParams } from 'elasticsearch'; +import { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { KibanaConfigType as KibanaConfigType_2 } from 'src/core/server/kibana_config'; import { KibanaRequest as KibanaRequest_2 } from 'kibana/server'; import { LegacyAPICaller as LegacyAPICaller_2 } from 'kibana/server'; @@ -143,6 +145,9 @@ import { TasksGetParams } from 'elasticsearch'; import { TasksListParams } from 'elasticsearch'; import { TermvectorsParams } from 'elasticsearch'; import { ToastInputFields } from 'src/core/public/notifications'; +import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; +import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; +import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; import { Unit } from '@elastic/datemath'; From 8648063b1073cc04d308dd83ca2b81d8fe1e2120 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Tue, 28 Jul 2020 10:52:28 -0400 Subject: [PATCH 93/96] [CI] Harden Slack notifications (#73361) --- .../src/test/KibanaBasePipelineTest.groovy | 4 ++ .../src/test/slackNotifications.groovy | 63 +++++++++++++++++++ vars/slackNotifications.groovy | 50 +++++++++++++-- 3 files changed, 112 insertions(+), 5 deletions(-) diff --git a/.ci/pipeline-library/src/test/KibanaBasePipelineTest.groovy b/.ci/pipeline-library/src/test/KibanaBasePipelineTest.groovy index 086484f2385b0..4112cb3c0e14b 100644 --- a/.ci/pipeline-library/src/test/KibanaBasePipelineTest.groovy +++ b/.ci/pipeline-library/src/test/KibanaBasePipelineTest.groovy @@ -78,6 +78,10 @@ class KibanaBasePipelineTest extends BasePipelineTest { return helper.callStack.find { it.methodName == name } } + def fnMocks(String name) { + helper.callStack.findAll { it.methodName == name } + } + void mockFailureBuild() { props([ buildUtils: [ diff --git a/.ci/pipeline-library/src/test/slackNotifications.groovy b/.ci/pipeline-library/src/test/slackNotifications.groovy index 467e4a0e5f520..f7e39f5fad903 100644 --- a/.ci/pipeline-library/src/test/slackNotifications.groovy +++ b/.ci/pipeline-library/src/test/slackNotifications.groovy @@ -59,4 +59,67 @@ class SlackNotificationsTest extends KibanaBasePipelineTest { args.blocks[2].text.text.toString() ) } + + @Test + void 'sendFailedBuild() should call slackSend() with a backup message when first attempt fails'() { + mockFailureBuild() + def counter = 0 + helper.registerAllowedMethod('slackSend', [Map.class], { ++counter > 1 }) + slackNotifications.sendFailedBuild() + + def args = fnMocks('slackSend')[1].args[0] + + def expected = [ + channel: '#kibana-operations-alerts', + username: 'Kibana Operations', + iconEmoji: ':jenkins:', + color: 'danger', + message: ':broken_heart: elastic / kibana # master #1', + ] + + expected.each { + assertEquals(it.value.toString(), args[it.key].toString()) + } + + assertEquals( + ":broken_heart: **" + + "\n\nFirst attempt at sending this notification failed. Please check the build.", + args.blocks[0].text.text.toString() + ) + } + + @Test + void 'getTestFailures() should truncate list of failures to 10'() { + prop('testUtils', [ + getFailures: { + return (1..12).collect { + return [ + url: Mocks.TEST_FAILURE_URL, + fullDisplayName: "Failure #${it}", + ] + } + }, + ]) + + def message = (String) slackNotifications.getTestFailures() + + assertTrue("Message ends with truncated indicator", message.endsWith("...and 2 more")) + assertTrue("Message contains Failure #10", message.contains("Failure #10")) + assertTrue("Message does not contain Failure #11", !message.contains("Failure #11")) + } + + @Test + void 'shortenMessage() should truncate a long message, but leave parts that fit'() { + assertEquals('Hello\nHello\n[...truncated...]', slackNotifications.shortenMessage('Hello\nHello\nthis is a long string', 29)) + } + + @Test + void 'shortenMessage() should not modify a short message'() { + assertEquals('Hello world', slackNotifications.shortenMessage('Hello world', 11)) + } + + @Test + void 'shortenMessage() should truncate an entire message with only one part'() { + assertEquals('[...truncated...]', slackNotifications.shortenMessage('Hello world this is a really long message', 40)) + } } diff --git a/vars/slackNotifications.groovy b/vars/slackNotifications.groovy index 2ffb420ecf3f4..30f86e6d6f0ad 100644 --- a/vars/slackNotifications.groovy +++ b/vars/slackNotifications.groovy @@ -13,12 +13,35 @@ def dividerBlock() { return [ type: "divider" ] } +// If a message is longer than the limit, split it up by '\n' into parts, and return as many parts as will fit within the limit +def shortenMessage(message, sizeLimit = 3000) { + if (message.size() <= sizeLimit) { + return message + } + + def truncatedMessage = "[...truncated...]" + + def parts = message.split("\n") + message = "" + + for(def part in parts) { + if ((message.size() + part.size() + truncatedMessage.size() + 1) > sizeLimit) { + break; + } + message += part+"\n" + } + + message += truncatedMessage + + return message.size() <= sizeLimit ? message : truncatedMessage +} + def markdownBlock(message) { return [ type: "section", text: [ type: "mrkdwn", - text: message, + text: shortenMessage(message, 3000), // 3000 is max text length for `section`s only ], ] } @@ -29,7 +52,7 @@ def contextBlock(message) { elements: [ [ type: 'mrkdwn', - text: message, + text: message, // Not sure what the size limit is here, I tried 10000s of characters and it still worked ] ] ] @@ -62,7 +85,7 @@ def getTestFailures() { def messages = [] messages << "*Test Failures*" - def list = failures.collect { + def list = failures.take(10).collect { def name = it .fullDisplayName .split(/\./, 2)[-1] @@ -73,7 +96,9 @@ def getTestFailures() { return "• <${it.url}|${name}>" }.join("\n") - return "*Test Failures*\n${list}" + + def moreText = failures.size() > 10 ? "\n• ...and ${failures.size()-10} more" : "" + return "*Test Failures*\n${list}${moreText}" } def getDefaultDisplayName() { @@ -98,6 +123,10 @@ def getStatusIcon() { return ':broken_heart:' } +def getBackupMessage(config) { + return "${getStatusIcon()} ${config.title}\n\nFirst attempt at sending this notification failed. Please check the build." +} + def sendFailedBuild(Map params = [:]) { def config = [ channel: '#kibana-operations-alerts', @@ -117,7 +146,7 @@ def sendFailedBuild(Map params = [:]) { blocks << dividerBlock() blocks << config.context - slackSend( + def resp = slackSend( channel: config.channel, username: config.username, iconEmoji: config.icon, @@ -125,6 +154,17 @@ def sendFailedBuild(Map params = [:]) { message: message, blocks: blocks ) + + if (!resp) { + slackSend( + channel: config.channel, + username: config.username, + iconEmoji: config.icon, + color: config.color, + message: message, + blocks: [markdownBlock(getBackupMessage(config))] + ) + } } def onFailure(Map options = [:]) { From 0149c65221aac101043eaa03b63b4e223801ec1d Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 28 Jul 2020 10:56:29 -0400 Subject: [PATCH 94/96] lodash `4.17.15` -> `4.17.19` (#73122) --- yarn.lock | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index c1328731db150..ee4188440e0ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20737,7 +20737,12 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.11.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: +lodash@4.17.11, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.11.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.16, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== + +lodash@4.17.15: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== From 3d5d4de63ce456e734fa1c5d6fdf28e779c46a24 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 28 Jul 2020 08:01:34 -0700 Subject: [PATCH 95/96] [kbn/optimizer] log info about the metrics being reported even when reporter is disabled (#73389) Co-authored-by: spalger --- .../ci_stats_reporter/ci_stats_reporter.ts | 4 +++- packages/kbn-optimizer/src/cli.ts | 2 +- .../kbn-optimizer/src/log_optimizer_state.ts | 6 ++--- .../src/report_optimizer_stats.ts | 22 ++++++++++++++++--- .../tasks/build_kibana_platform_plugins.ts | 2 +- 5 files changed, 27 insertions(+), 9 deletions(-) diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts index b38a27fdc1b48..b0378ab6c5cd5 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts @@ -29,6 +29,8 @@ interface Config { buildId: string; } +export type CiStatsMetrics = Array<{ group: string; id: string; value: number }>; + function parseConfig(log: ToolingLog) { const configJson = process.env.KIBANA_CI_STATS_CONFIG; if (!configJson) { @@ -84,7 +86,7 @@ export class CiStatsReporter { return !!this.config; } - async metrics(metrics: Array<{ group: string; id: string; value: number }>) { + async metrics(metrics: CiStatsMetrics) { if (!this.config) { return; } diff --git a/packages/kbn-optimizer/src/cli.ts b/packages/kbn-optimizer/src/cli.ts index 9d3f4b88a258f..542dc7255f22f 100644 --- a/packages/kbn-optimizer/src/cli.ts +++ b/packages/kbn-optimizer/src/cli.ts @@ -116,7 +116,7 @@ run( log.warning('Unable to initialize CiStatsReporter from env'); } - update$ = update$.pipe(reportOptimizerStats(reporter, config)); + update$ = update$.pipe(reportOptimizerStats(reporter, config, log)); } await update$.pipe(logOptimizerState(log, config)).toPromise(); diff --git a/packages/kbn-optimizer/src/log_optimizer_state.ts b/packages/kbn-optimizer/src/log_optimizer_state.ts index 20d98f74dbe86..e8bc6debf971e 100644 --- a/packages/kbn-optimizer/src/log_optimizer_state.ts +++ b/packages/kbn-optimizer/src/log_optimizer_state.ts @@ -104,7 +104,7 @@ export function logOptimizerState(log: ToolingLog, config: OptimizerConfig) { } if (state.phase === 'running' || state.phase === 'initializing') { - return true; + return; } if (state.phase === 'issue') { @@ -119,7 +119,7 @@ export function logOptimizerState(log: ToolingLog, config: OptimizerConfig) { } } log.indent(-4); - return true; + return; } if (state.phase === 'success') { @@ -135,7 +135,7 @@ export function logOptimizerState(log: ToolingLog, config: OptimizerConfig) { ); } - return true; + return; } throw new Error(`unhandled optimizer message: ${inspect(update)}`); diff --git a/packages/kbn-optimizer/src/report_optimizer_stats.ts b/packages/kbn-optimizer/src/report_optimizer_stats.ts index 5057c717efcc3..eff2bce0b827e 100644 --- a/packages/kbn-optimizer/src/report_optimizer_stats.ts +++ b/packages/kbn-optimizer/src/report_optimizer_stats.ts @@ -21,7 +21,7 @@ import Fs from 'fs'; import Path from 'path'; import { materialize, mergeMap, dematerialize } from 'rxjs/operators'; -import { CiStatsReporter } from '@kbn/dev-utils'; +import { CiStatsReporter, CiStatsMetrics, ToolingLog } from '@kbn/dev-utils'; import { OptimizerUpdate$ } from './run_optimizer'; import { OptimizerState, OptimizerConfig } from './optimizer'; @@ -67,7 +67,11 @@ const getFiles = (dir: string, parent?: string) => return true; }); -export function reportOptimizerStats(reporter: CiStatsReporter, config: OptimizerConfig) { +export function reportOptimizerStats( + reporter: CiStatsReporter, + config: OptimizerConfig, + log: ToolingLog +) { return pipeClosure((update$: OptimizerUpdate$) => { let lastState: OptimizerState | undefined; return update$.pipe( @@ -98,10 +102,18 @@ export function reportOptimizerStats(reporter: CiStatsReporter, config: Optimize const miscFiles = outputFiles.filter( (f) => f !== entry && !asyncChunks.includes(f) ); + + if (asyncChunks.length) { + log.verbose(bundle.id, 'async chunks', asyncChunks); + } + if (miscFiles.length) { + log.verbose(bundle.id, 'misc files', asyncChunks); + } + const sumSize = (files: Entry[]) => files.reduce((acc: number, f) => acc + f.stats!.size, 0); - return [ + const metrics: CiStatsMetrics = [ { group: `@kbn/optimizer bundle module count`, id: bundle.id, @@ -123,6 +135,10 @@ export function reportOptimizerStats(reporter: CiStatsReporter, config: Optimize value: sumSize(miscFiles), }, ]; + + log.info(bundle.id, 'metrics', metrics); + + return metrics; }) ) ); diff --git a/src/dev/build/tasks/build_kibana_platform_plugins.ts b/src/dev/build/tasks/build_kibana_platform_plugins.ts index 08637677fcfbe..beb5ad40229df 100644 --- a/src/dev/build/tasks/build_kibana_platform_plugins.ts +++ b/src/dev/build/tasks/build_kibana_platform_plugins.ts @@ -44,7 +44,7 @@ export const BuildKibanaPlatformPlugins: Task = { await runOptimizer(optimizerConfig) .pipe( - reportOptimizerStats(reporter, optimizerConfig), + reportOptimizerStats(reporter, optimizerConfig, log), logOptimizerState(log, optimizerConfig) ) .toPromise(); From f6a53f680552172dced497eb34a20b0f87d0b795 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Tue, 28 Jul 2020 11:45:18 -0400 Subject: [PATCH 96/96] Upgrade jimp to v0.14.0 (#73429) --- package.json | 2 +- yarn.lock | 416 +++++++++++++++++++++++++++------------------------ 2 files changed, 223 insertions(+), 195 deletions(-) diff --git a/package.json b/package.json index 0c49ec26be194..51a41cbbab9ff 100644 --- a/package.json +++ b/package.json @@ -460,7 +460,7 @@ "jest-cli": "^25.5.4", "jest-environment-jsdom-thirteen": "^1.0.1", "jest-raw-loader": "^1.0.1", - "jimp": "^0.9.6", + "jimp": "^0.14.0", "json5": "^1.0.1", "license-checker": "^16.0.0", "listr": "^0.14.1", diff --git a/yarn.lock b/yarn.lock index ee4188440e0ca..c31f58c3320e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2767,26 +2767,24 @@ "@types/yargs" "^15.0.0" chalk "^3.0.0" -"@jimp/bmp@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/bmp/-/bmp-0.9.6.tgz#379e261615d7c1f3b52af4d5a0f324666de53d7d" - integrity sha512-T2Fh/k/eN6cDyOx0KQ4y56FMLo8+mKNhBh7GXMQXLK2NNZ0ckpFo3VHDBZ3HnaFeVTZXF/atLiR9CfnXH+rLxA== +"@jimp/bmp@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/bmp/-/bmp-0.14.0.tgz#6df246026554f276f7b354047c6fff9f5b2b5182" + integrity sha512-5RkX6tSS7K3K3xNEb2ygPuvyL9whjanhoaB/WmmXlJS6ub4DjTqrapu8j4qnIWmO4YYtFeTbDTXV6v9P1yMA5A== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.9.6" + "@jimp/utils" "^0.14.0" bmp-js "^0.1.0" - core-js "^3.4.1" -"@jimp/core@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/core/-/core-0.9.6.tgz#a553f801bd436526d36e8982b99e58e8afc3d17a" - integrity sha512-sQO04S+HZNid68a9ehb4BC2lmW6iZ5JgU9tC+thC2Lhix+N/XKDJcBJ6HevbLgeTzuIAw24C5EKuUeO3C+rE5w== +"@jimp/core@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/core/-/core-0.14.0.tgz#870c9ca25b40be353ebda1d2abb48723d9010055" + integrity sha512-S62FcKdtLtj3yWsGfJRdFXSutjvHg7aQNiFogMbwq19RP4XJWqS2nOphu7ScB8KrSlyy5nPF2hkWNhLRLyD82w== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.9.6" + "@jimp/utils" "^0.14.0" any-base "^1.1.0" buffer "^5.2.0" - core-js "^3.4.1" exif-parser "^0.1.12" file-type "^9.0.0" load-bmfont "^1.3.1" @@ -2795,256 +2793,269 @@ pixelmatch "^4.0.2" tinycolor2 "^1.4.1" -"@jimp/custom@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/custom/-/custom-0.9.6.tgz#3d8a19d6ed717f0f1aa3f1b8f42fa374f43bc715" - integrity sha512-ZYKgrBZVoQwvIGlQSO7MFmn7Jn8a9X5g1g+KOTDO9Q0s4vnxdPTtr/qUjG9QYX6zW/6AK4LaIsDinDrrKDnOag== +"@jimp/custom@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/custom/-/custom-0.14.0.tgz#1dbbf0094df7403f4e03bc984ed92e7458842f74" + integrity sha512-kQJMeH87+kWJdVw8F9GQhtsageqqxrvzg7yyOw3Tx/s7v5RToe8RnKyMM+kVtBJtNAG+Xyv/z01uYQ2jiZ3GwA== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/core" "^0.9.6" - core-js "^3.4.1" + "@jimp/core" "^0.14.0" -"@jimp/gif@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/gif/-/gif-0.9.6.tgz#0a7b1e521daca635b02259f941bdb3600569d8e6" - integrity sha512-Z2muC2On8KHEVrWKCCM0L2eua9kw4bQETzT7gmVsizc8MXAKdS8AyVV9T3ZrImiI0o5UkAN/u0cPi1U2pSiD8Q== +"@jimp/gif@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/gif/-/gif-0.14.0.tgz#db159f57c3cfd1566bbe8b124958791998614960" + integrity sha512-DHjoOSfCaCz72+oGGEh8qH0zE6pUBaBxPxxmpYJjkNyDZP7RkbBkZJScIYeQ7BmJxmGN4/dZn+MxamoQlr+UYg== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.9.6" - core-js "^3.4.1" + "@jimp/utils" "^0.14.0" + gifwrap "^0.9.2" omggif "^1.0.9" -"@jimp/jpeg@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/jpeg/-/jpeg-0.9.6.tgz#fb90bdc0111966987c5ba59cdca7040be86ead41" - integrity sha512-igSe0pIX3le/CKdvqW4vLXMxoFjTLjEaW6ZHt/h63OegaEa61TzJ2OM7j7DxrEHcMCMlkhUc9Bapk57MAefCTQ== +"@jimp/jpeg@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/jpeg/-/jpeg-0.14.0.tgz#8a687a6a653bbbae38c522edef8f84bb418d9461" + integrity sha512-561neGbr+87S/YVQYnZSTyjWTHBm9F6F1obYHiyU3wVmF+1CLbxY3FQzt4YolwyQHIBv36Bo0PY2KkkU8BEeeQ== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.9.6" - core-js "^3.4.1" - jpeg-js "^0.3.4" + "@jimp/utils" "^0.14.0" + jpeg-js "^0.4.0" -"@jimp/plugin-blit@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/plugin-blit/-/plugin-blit-0.9.6.tgz#7937e02e3514b95dbe4c70d444054847f6e9ce3c" - integrity sha512-zp7X6uDU1lCu44RaSY88aAvsSKbgqUrfDyWRX1wsamJvvZpRnp1WekWlGyydRtnlUBAGIpiHCHmyh/TJ2I4RWA== +"@jimp/plugin-blit@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/plugin-blit/-/plugin-blit-0.14.0.tgz#5eb374be1201313b2113899fb842232d8fcfd345" + integrity sha512-YoYOrnVHeX3InfgbJawAU601iTZMwEBZkyqcP1V/S33Qnz9uzH1Uj1NtC6fNgWzvX6I4XbCWwtr4RrGFb5CFrw== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.9.6" - core-js "^3.4.1" + "@jimp/utils" "^0.14.0" -"@jimp/plugin-blur@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/plugin-blur/-/plugin-blur-0.9.6.tgz#3d74b18c27e9eae11b956ffe26290ece6d250813" - integrity sha512-xEi63hvzewUp7kzw+PI3f9CIrgZbphLI4TDDHWNYuS70RvhTuplbR6RMHD/zFhosrANCkJGr5OZJlrJnsCg6ug== +"@jimp/plugin-blur@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/plugin-blur/-/plugin-blur-0.14.0.tgz#fe07e4932d5a2f5d8c9831e245561553224bfc60" + integrity sha512-9WhZcofLrT0hgI7t0chf7iBQZib//0gJh9WcQMUt5+Q1Bk04dWs8vTgLNj61GBqZXgHSPzE4OpCrrLDBG8zlhQ== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.9.6" - core-js "^3.4.1" + "@jimp/utils" "^0.14.0" -"@jimp/plugin-color@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/plugin-color/-/plugin-color-0.9.6.tgz#d0fca0ed4c2c48fd6f929ef4a03cebf9c1342e14" - integrity sha512-o1HSoqBVUUAsWbqSXnpiHU0atKWy/Q1GUbZ3F5GWt/0OSDyl9RWM82V9axT2vePZHInKjIaimhnx1gGj8bfxkQ== +"@jimp/plugin-circle@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/plugin-circle/-/plugin-circle-0.14.0.tgz#82c0e904a34e90fa672fb9c286bc892e92088ddf" + integrity sha512-o5L+wf6QA44tvTum5HeLyLSc5eVfIUd5ZDVi5iRfO4o6GT/zux9AxuTSkKwnjhsG8bn1dDmywAOQGAx7BjrQVA== + dependencies: + "@babel/runtime" "^7.7.2" + "@jimp/utils" "^0.14.0" + +"@jimp/plugin-color@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/plugin-color/-/plugin-color-0.14.0.tgz#772bd2d80a88bc66ea1331d010207870f169a74b" + integrity sha512-JJz512SAILYV0M5LzBb9sbOm/XEj2fGElMiHAxb7aLI6jx+n0agxtHpfpV/AePTLm1vzzDxx6AJxXbKv355hBQ== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.9.6" - core-js "^3.4.1" + "@jimp/utils" "^0.14.0" tinycolor2 "^1.4.1" -"@jimp/plugin-contain@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/plugin-contain/-/plugin-contain-0.9.6.tgz#7d7bbd5e9c2fa4391a3d63620e13a28f51e1e7e8" - integrity sha512-Xz467EN1I104yranET4ff1ViVKMtwKLg1uRe8j3b5VOrjtiXpDbjirNZjP3HTlv8IEUreWNz4BK7ZtfHSptufA== +"@jimp/plugin-contain@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/plugin-contain/-/plugin-contain-0.14.0.tgz#c68115420d182e696f81bbe76fb5e704909b2b6a" + integrity sha512-RX2q233lGyaxiMY6kAgnm9ScmEkNSof0hdlaJAVDS1OgXphGAYAeSIAwzESZN4x3ORaWvkFefeVH9O9/698Evg== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.9.6" - core-js "^3.4.1" + "@jimp/utils" "^0.14.0" -"@jimp/plugin-cover@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/plugin-cover/-/plugin-cover-0.9.6.tgz#2853de7f8302f655ae8e95f51ab25a0ed77e3756" - integrity sha512-Ocr27AvtvH4ZT/9EWZgT3+HQV9fG5njwh2CYMHbdpx09O62Asj6pZ4QI0kKzOcux1oLgv59l7a93pEfMOfkfwQ== +"@jimp/plugin-cover@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/plugin-cover/-/plugin-cover-0.14.0.tgz#4755322589c5885e44e14e31b86b542e907297ce" + integrity sha512-0P/5XhzWES4uMdvbi3beUgfvhn4YuQ/ny8ijs5kkYIw6K8mHcl820HahuGpwWMx56DJLHRl1hFhJwo9CeTRJtQ== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.9.6" - core-js "^3.4.1" + "@jimp/utils" "^0.14.0" -"@jimp/plugin-crop@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/plugin-crop/-/plugin-crop-0.9.6.tgz#82e539af2a2417783abbd143124a57672ff4cc31" - integrity sha512-d9rNdmz3+eYLbSKcTyyp+b8Nmhf6HySnimDXlTej4UP6LDtkq2VAyVaJ12fz9x6dfd8qcXOBXMozSfNCcgpXYA== +"@jimp/plugin-crop@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/plugin-crop/-/plugin-crop-0.14.0.tgz#4cbd856ca84ffc37230fad2534906f2f75aa3057" + integrity sha512-Ojtih+XIe6/XSGtpWtbAXBozhCdsDMmy+THUJAGu2x7ZgKrMS0JotN+vN2YC3nwDpYkM+yOJImQeptSfZb2Sug== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.9.6" - core-js "^3.4.1" + "@jimp/utils" "^0.14.0" -"@jimp/plugin-displace@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/plugin-displace/-/plugin-displace-0.9.6.tgz#67564d081dc6b19007248ca222d025fd6f90c03b" - integrity sha512-SWpbrxiHmUYBVWtDDMjaG3eRDBASrTPaad7l07t73/+kmU6owAKWQW6KtVs05MYSJgXz7Ggdr0fhEn9AYLH1Rg== +"@jimp/plugin-displace@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/plugin-displace/-/plugin-displace-0.14.0.tgz#b0e6a57d00cb1f893f541413fe9d737d23c3b70c" + integrity sha512-c75uQUzMgrHa8vegkgUvgRL/PRvD7paFbFJvzW0Ugs8Wl+CDMGIPYQ3j7IVaQkIS+cAxv+NJ3TIRBQyBrfVEOg== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.9.6" - core-js "^3.4.1" + "@jimp/utils" "^0.14.0" -"@jimp/plugin-dither@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/plugin-dither/-/plugin-dither-0.9.6.tgz#dc48669cf51f3933761aa9137e99ebfa000b8cce" - integrity sha512-abm1GjfYK7ru/PoxH9fAUmhl+meHhGEClbVvjjMMe5g2S0BSTvMJl3SrkQD/FMkRLniaS/Qci6aQhIi+8rZmSw== +"@jimp/plugin-dither@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/plugin-dither/-/plugin-dither-0.14.0.tgz#9185ec4c38e02edc9e5831f5d709f6ba891e1b93" + integrity sha512-g8SJqFLyYexXQQsoh4dc1VP87TwyOgeTElBcxSXX2LaaMZezypmxQfLTzOFzZoK8m39NuaoH21Ou1Ftsq7LzVQ== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.9.6" - core-js "^3.4.1" + "@jimp/utils" "^0.14.0" -"@jimp/plugin-flip@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/plugin-flip/-/plugin-flip-0.9.6.tgz#f81f9b886da8cd56e23dd04d5aa359f2b94f939e" - integrity sha512-KFZTzAzQQ5bct3ii7gysOhWrTKVdUOghkkoSzLi+14nO3uS/dxiu8fPeH1m683ligbdnuM/b22OuLwEwrboTHA== +"@jimp/plugin-fisheye@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/plugin-fisheye/-/plugin-fisheye-0.14.0.tgz#9f26346cf2fbc660cc2008cd7fd30a83b5029e78" + integrity sha512-BFfUZ64EikCaABhCA6mR3bsltWhPpS321jpeIQfJyrILdpFsZ/OccNwCgpW1XlbldDHIoNtXTDGn3E+vCE7vDg== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.9.6" - core-js "^3.4.1" + "@jimp/utils" "^0.14.0" -"@jimp/plugin-gaussian@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/plugin-gaussian/-/plugin-gaussian-0.9.6.tgz#6c93897ee0ff979466184d7d0ec0fbc95c679be4" - integrity sha512-WXKLtJKWchXfWHT5HIOq1HkPKpbH7xBLWPgVRxw00NV/6I8v4xT63A7/Nag78m00JgjwwiE7eK2tLGDbbrPYig== +"@jimp/plugin-flip@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/plugin-flip/-/plugin-flip-0.14.0.tgz#7966d6aa3b5fe1aa4d2d561ff12b8ef5ccb9b071" + integrity sha512-WtL1hj6ryqHhApih+9qZQYA6Ye8a4HAmdTzLbYdTMrrrSUgIzFdiZsD0WeDHpgS/+QMsWwF+NFmTZmxNWqKfXw== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.9.6" - core-js "^3.4.1" + "@jimp/utils" "^0.14.0" -"@jimp/plugin-invert@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/plugin-invert/-/plugin-invert-0.9.6.tgz#4b3fa7b81ea976b09b82b3db59ee00ac3093d2fd" - integrity sha512-Pab/cupZrYxeRp07N4L5a4C/3ksTN9k6Knm/o2G5C789OF0rYsGGLcnBR/6h69nPizRZHBYdXCEyXYgujlIFiw== +"@jimp/plugin-gaussian@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/plugin-gaussian/-/plugin-gaussian-0.14.0.tgz#452bc1971a4467ad9b984aa67f4c200bf941bb65" + integrity sha512-uaLwQ0XAQoydDlF9tlfc7iD9drYPriFe+jgYnWm8fbw5cN+eOIcnneEX9XCOOzwgLPkNCxGox6Kxjn8zY6GxtQ== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.9.6" - core-js "^3.4.1" + "@jimp/utils" "^0.14.0" -"@jimp/plugin-mask@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/plugin-mask/-/plugin-mask-0.9.6.tgz#d70be0030ab3294b191f5b487fb655d776820b19" - integrity sha512-ikypRoDJkbxXlo6gW+EZOcTiLDIt0DrPwOFMt1bvL8UV2QPgX+GJ685IYwhIfEhBf/GSNFgB/NYsVvuSufTGeg== +"@jimp/plugin-invert@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/plugin-invert/-/plugin-invert-0.14.0.tgz#cd31a555860e9f821394936d15af161c09c42921" + integrity sha512-UaQW9X9vx8orQXYSjT5VcITkJPwDaHwrBbxxPoDG+F/Zgv4oV9fP+udDD6qmkgI9taU+44Fy+zm/J/gGcMWrdg== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.9.6" - core-js "^3.4.1" + "@jimp/utils" "^0.14.0" -"@jimp/plugin-normalize@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/plugin-normalize/-/plugin-normalize-0.9.6.tgz#c9128412a53485d91236a1da241f3166e572be4a" - integrity sha512-V3GeuAJ1NeL7qsLoDjnypJq24RWDCwbXpKhtxB+Yg9zzgOCkmb041p7ysxbcpkuJsRpKLNABZeNCCqd83bRawA== +"@jimp/plugin-mask@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/plugin-mask/-/plugin-mask-0.14.0.tgz#52619643ac6222f85e6b27dee33c771ca3a6a4c9" + integrity sha512-tdiGM69OBaKtSPfYSQeflzFhEpoRZ+BvKfDEoivyTjauynbjpRiwB1CaiS8En1INTDwzLXTT0Be9SpI3LkJoEA== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.9.6" - core-js "^3.4.1" + "@jimp/utils" "^0.14.0" -"@jimp/plugin-print@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/plugin-print/-/plugin-print-0.9.6.tgz#fea31ffeafee18ae7b5cfd6fa131bb205abfee51" - integrity sha512-gKkqZZPQtMSufHOL0mtJm5d/KI2O6+0kUpOBVSYdGedtPXA61kmVnsOd3wwajIMlXA3E0bDxLXLdAguWqjjGgw== +"@jimp/plugin-normalize@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/plugin-normalize/-/plugin-normalize-0.14.0.tgz#bf39e356b6d473f582ce95633ad49c9cdb82492b" + integrity sha512-AfY8sqlsbbdVwFGcyIPy5JH/7fnBzlmuweb+Qtx2vn29okq6+HelLjw2b+VT2btgGUmWWHGEHd86oRGSoWGyEQ== + dependencies: + "@babel/runtime" "^7.7.2" + "@jimp/utils" "^0.14.0" + +"@jimp/plugin-print@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/plugin-print/-/plugin-print-0.14.0.tgz#1c43c2a92a7adc05b464863882cb89ce486d63e6" + integrity sha512-MwP3sH+VS5AhhSTXk7pui+tEJFsxnTKFY3TraFJb8WFbA2Vo2qsRCZseEGwpTLhENB7p/JSsLvWoSSbpmxhFAQ== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.9.6" - core-js "^3.4.1" + "@jimp/utils" "^0.14.0" load-bmfont "^1.4.0" -"@jimp/plugin-resize@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/plugin-resize/-/plugin-resize-0.9.6.tgz#7fb939c8a42e2a3639d661cc7ab24057598693bd" - integrity sha512-r5wJcVII7ZWMuY2l6WSbHPG6gKMFemtCHmJRXGUu+/ZhPGBz3IFluycBpHkWW3OB+jfvuyv1EGQWHU50N1l8Og== +"@jimp/plugin-resize@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/plugin-resize/-/plugin-resize-0.14.0.tgz#ef7fc6c2e45f8bcab62456baf8fd3bc415b02b64" + integrity sha512-qFeMOyXE/Bk6QXN0GQo89+CB2dQcXqoxUcDb2Ah8wdYlKqpi53skABkgVy5pW3EpiprDnzNDboMltdvDslNgLQ== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.9.6" - core-js "^3.4.1" + "@jimp/utils" "^0.14.0" -"@jimp/plugin-rotate@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/plugin-rotate/-/plugin-rotate-0.9.6.tgz#06d725155e5cdb615133f57a52f5a860a9d03f3e" - integrity sha512-B2nm/eO2nbvn1DgmnzMd79yt3V6kffhRNrKoo2VKcKFiVze1vGP3MD3fVyw5U1PeqwAFu7oTICFnCf9wKDWSqg== +"@jimp/plugin-rotate@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/plugin-rotate/-/plugin-rotate-0.14.0.tgz#3632bc159bf1c3b9ec9f459d9c05d02a11781ee7" + integrity sha512-aGaicts44bvpTcq5Dtf93/8TZFu5pMo/61lWWnYmwJJU1RqtQlxbCLEQpMyRhKDNSfPbuP8nyGmaqXlM/82J0Q== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.9.6" - core-js "^3.4.1" + "@jimp/utils" "^0.14.0" -"@jimp/plugin-scale@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/plugin-scale/-/plugin-scale-0.9.6.tgz#3fa939c1a4f44383e12afeb7c434eb41f20e4a1c" - integrity sha512-DLsLB5S3mh9+TZY5ycwfLgOJvUcoS7bP0Mi3I8vE1J91qmA+TXoWFFgrIVgnEPw5jSKzNTt8WhykQ0x2lKXncw== +"@jimp/plugin-scale@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/plugin-scale/-/plugin-scale-0.14.0.tgz#d30f0cd1365b8e68f43fa423300ae7f124e9bf10" + integrity sha512-ZcJk0hxY5ZKZDDwflqQNHEGRblgaR+piePZm7dPwPUOSeYEH31P0AwZ1ziceR74zd8N80M0TMft+e3Td6KGBHw== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.9.6" - core-js "^3.4.1" + "@jimp/utils" "^0.14.0" -"@jimp/plugins@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/plugins/-/plugins-0.9.6.tgz#a1cfdf9f3e1adf5b124686486343888a16c8fd27" - integrity sha512-eQI29e+K+3L/fb5GbPgsBdoftvaYetSOO2RL5z+Gjk6R4EF4QFRo63YcFl+f72Kc1b0JTOoDxClvn/s5GMV0tg== +"@jimp/plugin-shadow@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/plugin-shadow/-/plugin-shadow-0.14.0.tgz#471fdb9f109ff2d9e20d533d45e1e18e0b48c749" + integrity sha512-p2igcEr/iGrLiTu0YePNHyby0WYAXM14c5cECZIVnq/UTOOIQ7xIcWZJ1lRbAEPxVVXPN1UibhZAbr3HAb5BjQ== + dependencies: + "@babel/runtime" "^7.7.2" + "@jimp/utils" "^0.14.0" + +"@jimp/plugin-threshold@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/plugin-threshold/-/plugin-threshold-0.14.0.tgz#ebd72721c7d1d518c5bb6e494e55d97ac3351d3b" + integrity sha512-N4BlDgm/FoOMV/DQM2rSpzsgqAzkP0DXkWZoqaQrlRxQBo4zizQLzhEL00T/YCCMKnddzgEhnByaocgaaa0fKw== + dependencies: + "@babel/runtime" "^7.7.2" + "@jimp/utils" "^0.14.0" + +"@jimp/plugins@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/plugins/-/plugins-0.14.0.tgz#41dba85f15ab8dadb4162100eb54e5f27b93ee2c" + integrity sha512-vDO3XT/YQlFlFLq5TqNjQkISqjBHT8VMhpWhAfJVwuXIpilxz5Glu4IDLK6jp4IjPR6Yg2WO8TmRY/HI8vLrOw== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/plugin-blit" "^0.9.6" - "@jimp/plugin-blur" "^0.9.6" - "@jimp/plugin-color" "^0.9.6" - "@jimp/plugin-contain" "^0.9.6" - "@jimp/plugin-cover" "^0.9.6" - "@jimp/plugin-crop" "^0.9.6" - "@jimp/plugin-displace" "^0.9.6" - "@jimp/plugin-dither" "^0.9.6" - "@jimp/plugin-flip" "^0.9.6" - "@jimp/plugin-gaussian" "^0.9.6" - "@jimp/plugin-invert" "^0.9.6" - "@jimp/plugin-mask" "^0.9.6" - "@jimp/plugin-normalize" "^0.9.6" - "@jimp/plugin-print" "^0.9.6" - "@jimp/plugin-resize" "^0.9.6" - "@jimp/plugin-rotate" "^0.9.6" - "@jimp/plugin-scale" "^0.9.6" - core-js "^3.4.1" + "@jimp/plugin-blit" "^0.14.0" + "@jimp/plugin-blur" "^0.14.0" + "@jimp/plugin-circle" "^0.14.0" + "@jimp/plugin-color" "^0.14.0" + "@jimp/plugin-contain" "^0.14.0" + "@jimp/plugin-cover" "^0.14.0" + "@jimp/plugin-crop" "^0.14.0" + "@jimp/plugin-displace" "^0.14.0" + "@jimp/plugin-dither" "^0.14.0" + "@jimp/plugin-fisheye" "^0.14.0" + "@jimp/plugin-flip" "^0.14.0" + "@jimp/plugin-gaussian" "^0.14.0" + "@jimp/plugin-invert" "^0.14.0" + "@jimp/plugin-mask" "^0.14.0" + "@jimp/plugin-normalize" "^0.14.0" + "@jimp/plugin-print" "^0.14.0" + "@jimp/plugin-resize" "^0.14.0" + "@jimp/plugin-rotate" "^0.14.0" + "@jimp/plugin-scale" "^0.14.0" + "@jimp/plugin-shadow" "^0.14.0" + "@jimp/plugin-threshold" "^0.14.0" timm "^1.6.1" -"@jimp/png@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/png/-/png-0.9.6.tgz#00ed7e6fb783b94f2f1a9fadf9a42bd75f70cc7f" - integrity sha512-9vhOG2xylcDqPbBf4lzpa2Sa1WNJrEZNGvPvWcM+XVhqYa8+DJBLYkoBlpI/qWIYA+eVWDnLF3ygtGj8CElICw== +"@jimp/png@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/png/-/png-0.14.0.tgz#0f2dddb5125c0795ca7e67c771204c5437fcda4b" + integrity sha512-0RV/mEIDOrPCcNfXSPmPBqqSZYwGADNRVUTyMt47RuZh7sugbYdv/uvKmQSiqRdR0L1sfbCBMWUEa5G/8MSbdA== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.9.6" - core-js "^3.4.1" + "@jimp/utils" "^0.14.0" pngjs "^3.3.3" -"@jimp/tiff@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/tiff/-/tiff-0.9.6.tgz#9ff12122e727ee15f27f40a710516102a636f66b" - integrity sha512-pKKEMqPzX9ak8mek2iVVoW34+h/TSWUyI4NjbYWJMQ2WExfuvEJvLocy9Q9xi6HqRuJmUxgNIiC5iZM1PDEEfg== +"@jimp/tiff@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/tiff/-/tiff-0.14.0.tgz#a5b25bbe7c43fc3b07bad4e2ab90e0e164c1967f" + integrity sha512-zBYDTlutc7j88G/7FBCn3kmQwWr0rmm1e0FKB4C3uJ5oYfT8645lftUsvosKVUEfkdmOaMAnhrf4ekaHcb5gQw== dependencies: "@babel/runtime" "^7.7.2" - core-js "^3.4.1" utif "^2.0.1" -"@jimp/types@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/types/-/types-0.9.6.tgz#7be7f415ad93be733387c03b8a228c587a868a18" - integrity sha512-PSjdbLZ8d50En+Wf1XkWFfrXaf/GqyrxxgIwFWPbL+wrW4pmbYovfxSLCY61s8INsOFOft9dzzllhLBtg1aQ6A== +"@jimp/types@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/types/-/types-0.14.0.tgz#ef681ff702883c5f105b5e4e30d49abf39ee9e34" + integrity sha512-hx3cXAW1KZm+b+XCrY3LXtdWy2U+hNtq0rPyJ7NuXCjU7lZR3vIkpz1DLJ3yDdS70hTi5QDXY3Cd9kd6DtloHQ== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/bmp" "^0.9.6" - "@jimp/gif" "^0.9.6" - "@jimp/jpeg" "^0.9.6" - "@jimp/png" "^0.9.6" - "@jimp/tiff" "^0.9.6" - core-js "^3.4.1" + "@jimp/bmp" "^0.14.0" + "@jimp/gif" "^0.14.0" + "@jimp/jpeg" "^0.14.0" + "@jimp/png" "^0.14.0" + "@jimp/tiff" "^0.14.0" timm "^1.6.1" -"@jimp/utils@^0.9.6": - version "0.9.6" - resolved "https://registry.yarnpkg.com/@jimp/utils/-/utils-0.9.6.tgz#a3e6c29e835e2b9ea9f3899c9d3d230dc63bd518" - integrity sha512-kzxcp0i4ecSdMXFEmtH+NYdBQysINEUTsrjm7v0zH8t/uwaEMOG46I16wo/iPBXJkUeNdL2rbXoGoxxoeSfrrA== +"@jimp/utils@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@jimp/utils/-/utils-0.14.0.tgz#296254e63118554c62c31c19ac6b8c4bfe6490e5" + integrity sha512-MY5KFYUru0y74IsgM/9asDwb3ERxWxXEu3CRCZEvE7DtT86y1bR1XgtlSliMrptjz4qbivNGMQSvUBpEFJDp1A== dependencies: "@babel/runtime" "^7.7.2" - core-js "^3.4.1" + regenerator-runtime "^0.13.3" "@mapbox/extent@0.4.0": version "0.4.0" @@ -10924,7 +10935,7 @@ core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.1, core-js@^2.5.3, core-js@^2.6.5, resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== -core-js@^3.0.1, core-js@^3.0.4, core-js@^3.4.1, core-js@^3.6.4: +core-js@^3.0.1, core-js@^3.0.4, core-js@^3.6.4: version "3.6.4" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.4.tgz#440a83536b458114b9cb2ac1580ba377dc470647" integrity sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw== @@ -15487,6 +15498,14 @@ gh-got@^5.0.0: got "^6.2.0" is-plain-obj "^1.1.0" +gifwrap@^0.9.2: + version "0.9.2" + resolved "https://registry.yarnpkg.com/gifwrap/-/gifwrap-0.9.2.tgz#348e286e67d7cf57942172e1e6f05a71cee78489" + integrity sha512-fcIswrPaiCDAyO8xnWvHSZdWChjKXUanKKpAiWWJ/UTkEi/aYKn5+90e7DE820zbEaVR9CE2y4z9bzhQijZ0BA== + dependencies: + image-q "^1.1.1" + omggif "^1.0.10" + git-clone@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/git-clone/-/git-clone-0.1.0.tgz#0d76163778093aef7f1c30238f2a9ef3f07a2eb9" @@ -17368,6 +17387,11 @@ ignore@^5.1.1, ignore@^5.1.4: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== +image-q@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/image-q/-/image-q-1.1.1.tgz#fc84099664460b90ca862d9300b6bfbbbfbf8056" + integrity sha1-/IQJlmRGC5DKhi2TALa/u7+/gFY= + image-size@^0.8.2: version "0.8.3" resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.8.3.tgz#f0b568857e034f29baffd37013587f2c0cad8b46" @@ -19398,16 +19422,15 @@ jest@^25.5.4: import-local "^3.0.2" jest-cli "^25.5.4" -jimp@^0.9.6: - version "0.9.6" - resolved "https://registry.yarnpkg.com/jimp/-/jimp-0.9.6.tgz#abf381daf193a4fa335cb4ee0e22948049251eb9" - integrity sha512-DBDHYeNVqVpoPkcvo0PKTNGvD+i7NYvkKTsl0I3k7ql36uN8wGTptRg0HtgQyYE/bhGSLI6Lq5qLwewaOPXNfg== +jimp@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/jimp/-/jimp-0.14.0.tgz#fde55f69bdb918c1b01ac633d89a25853af85625" + integrity sha512-8BXU+J8+SPmwwyq9ELihpSV4dWPTiOKBWCEgtkbnxxAVMjXdf3yGmyaLSshBfXc8sP/JQ9OZj5R8nZzz2wPXgA== dependencies: "@babel/runtime" "^7.7.2" - "@jimp/custom" "^0.9.6" - "@jimp/plugins" "^0.9.6" - "@jimp/types" "^0.9.6" - core-js "^3.4.1" + "@jimp/custom" "^0.14.0" + "@jimp/plugins" "^0.14.0" + "@jimp/types" "^0.14.0" regenerator-runtime "^0.13.3" jit-grunt@0.10.0: @@ -19429,10 +19452,10 @@ joi@13.x.x, joi@^13.5.2: isemail "3.x.x" topo "3.x.x" -jpeg-js@^0.3.4: - version "0.3.4" - resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.3.4.tgz#dc2ba501ee3d58b7bb893c5d1fab47294917e7e7" - integrity sha512-6IzjQxvnlT8UlklNmDXIJMWxijULjqGrzgqc0OG7YadZdvm7KPQ1j0ehmQQHckgEWOfgpptzcnWgESovxudpTA== +jpeg-js@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.1.tgz#937a3ae911eb6427f151760f8123f04c8bfe6ef7" + integrity sha512-jA55yJiB5tCXEddos8JBbvW+IMrqY0y1tjjx9KNVtA+QPmu7ND5j0zkKopClpUTsaETL135uOM2XfcYG4XRjmw== jquery@^3.5.0: version "3.5.0" @@ -23066,6 +23089,11 @@ octokit-pagination-methods@^1.1.0: resolved "https://registry.yarnpkg.com/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4" integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ== +omggif@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/omggif/-/omggif-1.0.10.tgz#ddaaf90d4a42f532e9e7cb3a95ecdd47f17c7b19" + integrity sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw== + omggif@^1.0.9: version "1.0.9" resolved "https://registry.yarnpkg.com/omggif/-/omggif-1.0.9.tgz#dcb7024dacd50c52b4d303f04802c91c057c765f"
{children}; -}; +} diff --git a/x-pack/plugins/observability/public/components/app/empty_section/index.tsx b/x-pack/plugins/observability/public/components/app/empty_section/index.tsx index 4c830b2b2f094..5a2e64459358d 100644 --- a/x-pack/plugins/observability/public/components/app/empty_section/index.tsx +++ b/x-pack/plugins/observability/public/components/app/empty_section/index.tsx @@ -11,7 +11,7 @@ interface Props { section: ISection; } -export const EmptySection = ({ section }: Props) => { +export function EmptySection({ section }: Props) { return ( { } /> ); -}; +} diff --git a/x-pack/plugins/observability/public/components/app/header/index.tsx b/x-pack/plugins/observability/public/components/app/header/index.tsx index 531e6abf3d236..0e35fbb008bee 100644 --- a/x-pack/plugins/observability/public/components/app/header/index.tsx +++ b/x-pack/plugins/observability/public/components/app/header/index.tsx @@ -38,12 +38,12 @@ interface Props { showGiveFeedback?: boolean; } -export const Header = ({ +export function Header({ color, restrictWidth, showAddData = false, showGiveFeedback = false, -}: Props) => { +}: Props) { const { core } = usePluginContext(); return ( @@ -91,4 +91,4 @@ export const Header = ({ ); -}; +} diff --git a/x-pack/plugins/observability/public/components/app/ingest_manager_panel/index.tsx b/x-pack/plugins/observability/public/components/app/ingest_manager_panel/index.tsx index 41bcfa1da7fa1..1ab9f75632d9d 100644 --- a/x-pack/plugins/observability/public/components/app/ingest_manager_panel/index.tsx +++ b/x-pack/plugins/observability/public/components/app/ingest_manager_panel/index.tsx @@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n'; import { EuiText } from '@elastic/eui'; import { EuiLink } from '@elastic/eui'; -export const IngestManagerPanel = () => { +export function IngestManagerPanel() { return ( { ); -}; +} diff --git a/x-pack/plugins/observability/public/components/app/layout/with_header.tsx b/x-pack/plugins/observability/public/components/app/layout/with_header.tsx index 27b25f0056055..a77487e1244e6 100644 --- a/x-pack/plugins/observability/public/components/app/layout/with_header.tsx +++ b/x-pack/plugins/observability/public/components/app/layout/with_header.tsx @@ -32,23 +32,25 @@ interface Props { showGiveFeedback?: boolean; } -export const WithHeaderLayout = ({ +export function WithHeaderLayout({ headerColor, bodyColor, children, restrictWidth, showAddData, showGiveFeedback, -}: Props) => ( - -
- - {children} - - -); +}: Props) { + return ( + +
+ + {children} + + + ); +} diff --git a/x-pack/plugins/observability/public/components/app/news_feed/index.tsx b/x-pack/plugins/observability/public/components/app/news_feed/index.tsx index 2fbd6659bcb5a..625ae94c90aa2 100644 --- a/x-pack/plugins/observability/public/components/app/news_feed/index.tsx +++ b/x-pack/plugins/observability/public/components/app/news_feed/index.tsx @@ -23,7 +23,7 @@ interface Props { items: INewsItem[]; } -export const NewsFeed = ({ items }: Props) => { +export function NewsFeed({ items }: Props) { return ( // The news feed is manually added/edited, to prevent any errors caused by typos or missing fields, // wraps the component with EuiErrorBoundary to avoid breaking the entire page. @@ -46,11 +46,11 @@ export const NewsFeed = ({ items }: Props) => { ); -}; +} const limitString = (string: string, limit: number) => truncate(string, { length: limit }); -const NewsItem = ({ item }: { item: INewsItem }) => { +function NewsItem({ item }: { item: INewsItem }) { const theme = useContext(ThemeContext); return ( @@ -98,4 +98,4 @@ const NewsItem = ({ item }: { item: INewsItem }) => { ); -}; +} diff --git a/x-pack/plugins/observability/public/components/app/resources/index.tsx b/x-pack/plugins/observability/public/components/app/resources/index.tsx index 929802df3329b..47ac5f0f6d301 100644 --- a/x-pack/plugins/observability/public/components/app/resources/index.tsx +++ b/x-pack/plugins/observability/public/components/app/resources/index.tsx @@ -31,7 +31,7 @@ const resources = [ }, ]; -export const Resources = () => { +export function Resources() { return ( @@ -46,4 +46,4 @@ export const Resources = () => { ); -}; +} diff --git a/x-pack/plugins/observability/public/components/app/section/alerts/index.tsx b/x-pack/plugins/observability/public/components/app/section/alerts/index.tsx index c0dc67b3373b1..02e841ec50ee2 100644 --- a/x-pack/plugins/observability/public/components/app/section/alerts/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/alerts/index.tsx @@ -33,7 +33,7 @@ interface Props { alerts: Alert[]; } -export const AlertsSection = ({ alerts }: Props) => { +export function AlertsSection({ alerts }: Props) { const { core } = usePluginContext(); const [filter, setFilter] = useState(ALL_TYPES); @@ -130,4 +130,4 @@ export const AlertsSection = ({ alerts }: Props) => { ); -}; +} diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx index dce80ed324456..a1d51ffda6afd 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx @@ -30,7 +30,7 @@ function formatTpm(value?: number) { return numeral(value).format('0.00a'); } -export const APMSection = ({ absoluteTime, relativeTime, bucketSize }: Props) => { +export function APMSection({ absoluteTime, relativeTime, bucketSize }: Props) { const theme = useContext(ThemeContext); const history = useHistory(); @@ -43,7 +43,7 @@ export const APMSection = ({ absoluteTime, relativeTime, bucketSize }: Props) => bucketSize, }); } - }, [start, end, bucketSize]); + }, [start, end, bucketSize, relativeTime]); const { appLink, stats, series } = data || {}; @@ -121,4 +121,4 @@ export const APMSection = ({ absoluteTime, relativeTime, bucketSize }: Props) => ); -}; +} diff --git a/x-pack/plugins/observability/public/components/app/section/error_panel/index.tsx b/x-pack/plugins/observability/public/components/app/section/error_panel/index.tsx index 8f0781b8f0269..2413580e90a07 100644 --- a/x-pack/plugins/observability/public/components/app/section/error_panel/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/error_panel/index.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -export const ErrorPanel = () => { +export function ErrorPanel() { return ( @@ -19,4 +19,4 @@ export const ErrorPanel = () => { ); -}; +} diff --git a/x-pack/plugins/observability/public/components/app/section/index.tsx b/x-pack/plugins/observability/public/components/app/section/index.tsx index 9ba524259ea1c..6c6d107b714be 100644 --- a/x-pack/plugins/observability/public/components/app/section/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/index.tsx @@ -20,7 +20,7 @@ interface Props { appLink?: AppLink; } -export const SectionContainer = ({ title, appLink, children, hasError }: Props) => { +export function SectionContainer({ title, appLink, children, hasError }: Props) { const { core } = usePluginContext(); return ( ); -}; +} diff --git a/x-pack/plugins/observability/public/components/app/section/logs/index.tsx b/x-pack/plugins/observability/public/components/app/section/logs/index.tsx index 9b232ea33cbfb..aa1dc1640125e 100644 --- a/x-pack/plugins/observability/public/components/app/section/logs/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/logs/index.tsx @@ -45,7 +45,7 @@ function getColorPerItem(series?: LogsFetchDataResponse['series']) { return colorsPerItem; } -export const LogsSection = ({ absoluteTime, relativeTime, bucketSize }: Props) => { +export function LogsSection({ absoluteTime, relativeTime, bucketSize }: Props) { const history = useHistory(); const { start, end } = absoluteTime; @@ -57,7 +57,7 @@ export const LogsSection = ({ absoluteTime, relativeTime, bucketSize }: Props) = bucketSize, }); } - }, [start, end, bucketSize]); + }, [start, end, bucketSize, relativeTime]); const min = moment.utc(absoluteTime.start).valueOf(); const max = moment.utc(absoluteTime.end).valueOf(); @@ -160,4 +160,4 @@ export const LogsSection = ({ absoluteTime, relativeTime, bucketSize }: Props) = ); -}; +} diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx b/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx index 9e5fdadaf4e5f..8bce8205902fa 100644 --- a/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx @@ -46,7 +46,7 @@ const StyledProgress = styled.div<{ color?: string }>` } `; -export const MetricsSection = ({ absoluteTime, relativeTime, bucketSize }: Props) => { +export function MetricsSection({ absoluteTime, relativeTime, bucketSize }: Props) { const theme = useContext(ThemeContext); const { start, end } = absoluteTime; @@ -58,7 +58,7 @@ export const MetricsSection = ({ absoluteTime, relativeTime, bucketSize }: Props bucketSize, }); } - }, [start, end, bucketSize]); + }, [start, end, bucketSize, relativeTime]); const isLoading = status === FETCH_STATUS.LOADING; @@ -162,9 +162,9 @@ export const MetricsSection = ({ absoluteTime, relativeTime, bucketSize }: Props ); -}; +} -const AreaChart = ({ +function AreaChart({ serie, isLoading, color, @@ -172,7 +172,7 @@ const AreaChart = ({ serie?: Series; isLoading: boolean; color: string; -}) => { +}) { const chartTheme = useChartTheme(); return ( @@ -191,4 +191,4 @@ const AreaChart = ({ )} ); -}; +} diff --git a/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx b/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx index 73a566460a593..cfb06af3224c7 100644 --- a/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx @@ -35,7 +35,7 @@ interface Props { bucketSize?: string; } -export const UptimeSection = ({ absoluteTime, relativeTime, bucketSize }: Props) => { +export function UptimeSection({ absoluteTime, relativeTime, bucketSize }: Props) { const theme = useContext(ThemeContext); const history = useHistory(); @@ -48,7 +48,7 @@ export const UptimeSection = ({ absoluteTime, relativeTime, bucketSize }: Props) bucketSize, }); } - }, [start, end, bucketSize]); + }, [start, end, bucketSize, relativeTime]); const min = moment.utc(absoluteTime.start).valueOf(); const max = moment.utc(absoluteTime.end).valueOf(); @@ -138,9 +138,9 @@ export const UptimeSection = ({ absoluteTime, relativeTime, bucketSize }: Props) ); -}; +} -const UptimeBarSeries = ({ +function UptimeBarSeries({ id, label, series, @@ -152,7 +152,7 @@ const UptimeBarSeries = ({ series?: Series; color: string; ticktFormatter: TickFormatter; -}) => { +}) { if (!series) { return null; } @@ -188,4 +188,4 @@ const UptimeBarSeries = ({ /> ); -}; +} diff --git a/x-pack/plugins/observability/public/components/app/styled_stat/index.tsx b/x-pack/plugins/observability/public/components/app/styled_stat/index.tsx index fe38df6484c29..a58a0c8309723 100644 --- a/x-pack/plugins/observability/public/components/app/styled_stat/index.tsx +++ b/x-pack/plugins/observability/public/components/app/styled_stat/index.tsx @@ -21,7 +21,7 @@ interface Props extends Partial { const EMPTY_VALUE = '--'; -export const StyledStat = (props: Props) => { +export function StyledStat(props: Props) { const { description = EMPTY_VALUE, title = EMPTY_VALUE, ...rest } = props; return ; -}; +} diff --git a/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx b/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx index ea79f4d08d701..55746ff6576a9 100644 --- a/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx @@ -14,37 +14,45 @@ import { EuiPopoverProps, } from '@elastic/eui'; -import React, { HTMLAttributes } from 'react'; +import React, { HTMLAttributes, ReactNode } from 'react'; import { EuiListGroupItemProps } from '@elastic/eui/src/components/list_group/list_group_item'; import styled from 'styled-components'; type Props = EuiPopoverProps & HTMLAttributes; -export const SectionTitle: React.FC<{}> = (props) => ( - <> - -
{props.children}
-
- - -); - -export const SectionSubtitle: React.FC<{}> = (props) => ( - <> - - {props.children} - - - -); - -export const SectionLinks: React.FC<{}> = (props) => ( - - {props.children} - -); - -export const SectionSpacer: React.FC<{}> = () => ; +export function SectionTitle({ children }: { children?: ReactNode }) { + return ( + <> + +
{children}
+
+ + + ); +} + +export function SectionSubtitle({ children }: { children?: ReactNode }) { + return ( + <> + + {children} + + + + ); +} + +export function SectionLinks({ children }: { children?: ReactNode }) { + return ( + + {children} + + ); +} + +export function SectionSpacer() { + return ; +} export const Section = styled.div` margin-bottom: 24px; @@ -54,10 +62,14 @@ export const Section = styled.div` `; export type SectionLinkProps = EuiListGroupItemProps; -export const SectionLink: React.FC = (props) => ( - -); +export function SectionLink(props: SectionLinkProps) { + return ; +} -export const ActionMenuDivider: React.FC<{}> = (props) => ; +export function ActionMenuDivider() { + return ; +} -export const ActionMenu: React.FC = (props) => ; +export function ActionMenu(props: Props) { + return ; +} diff --git a/x-pack/plugins/observability/public/components/shared/data_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/data_picker/index.tsx index cc77c1ed72b4a..1c4f465a1d301 100644 --- a/x-pack/plugins/observability/public/components/shared/data_picker/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/data_picker/index.tsx @@ -31,7 +31,7 @@ interface Props { refreshInterval: number; } -export const DatePicker = ({ rangeFrom, rangeTo, refreshPaused, refreshInterval }: Props) => { +export function DatePicker({ rangeFrom, rangeTo, refreshPaused, refreshInterval }: Props) { const location = useLocation(); const history = useHistory(); @@ -86,4 +86,4 @@ export const DatePicker = ({ rangeFrom, rangeTo, refreshPaused, refreshInterval onRefresh={onTimeChange} /> ); -}; +} diff --git a/x-pack/plugins/observability/public/pages/home/index.tsx b/x-pack/plugins/observability/public/pages/home/index.tsx index 59513fc047f17..349533346f2ad 100644 --- a/x-pack/plugins/observability/public/pages/home/index.tsx +++ b/x-pack/plugins/observability/public/pages/home/index.tsx @@ -8,7 +8,7 @@ import { useHistory } from 'react-router-dom'; import { fetchHasData } from '../../data_handler'; import { useFetcher } from '../../hooks/use_fetcher'; -export const HomePage = () => { +export function HomePage() { const history = useHistory(); const { data = {} } = useFetcher(() => fetchHasData(), []); @@ -23,4 +23,4 @@ export const HomePage = () => { } return <>; -}; +} diff --git a/x-pack/plugins/observability/public/pages/landing/index.tsx b/x-pack/plugins/observability/public/pages/landing/index.tsx index 81485953f8713..4d8bd4bf2c789 100644 --- a/x-pack/plugins/observability/public/pages/landing/index.tsx +++ b/x-pack/plugins/observability/public/pages/landing/index.tsx @@ -27,7 +27,7 @@ const EuiCardWithoutPadding = styled(EuiCard)` padding: 0; `; -export const LandingPage = () => { +export function LandingPage() { const { core } = usePluginContext(); const theme = useContext(ThemeContext); @@ -124,4 +124,4 @@ export const LandingPage = () => { ); -}; +} diff --git a/x-pack/plugins/observability/public/pages/overview/index.tsx b/x-pack/plugins/observability/public/pages/overview/index.tsx index 088fab032d930..32bdb00577bd2 100644 --- a/x-pack/plugins/observability/public/pages/overview/index.tsx +++ b/x-pack/plugins/observability/public/pages/overview/index.tsx @@ -38,14 +38,14 @@ function calculatetBucketSize({ start, end }: { start?: number; end?: number }) } } -export const OverviewPage = ({ routeParams }: Props) => { +export function OverviewPage({ routeParams }: Props) { const { core } = usePluginContext(); const { data: alerts = [], status: alertStatus } = useFetcher(() => { return getObservabilityAlerts({ core }); - }, []); + }, [core]); - const { data: newsFeed } = useFetcher(() => getNewsFeed({ core }), []); + const { data: newsFeed } = useFetcher(() => getNewsFeed({ core }), [core]); const theme = useContext(ThemeContext); const timePickerTime = useKibanaUISettings(UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS); @@ -206,4 +206,4 @@ export const OverviewPage = ({ routeParams }: Props) => { ); -}; +} diff --git a/x-pack/plugins/observability/public/pages/overview/loading_observability.tsx b/x-pack/plugins/observability/public/pages/overview/loading_observability.tsx index 90e3104443e6b..0f4fa9b864744 100644 --- a/x-pack/plugins/observability/public/pages/overview/loading_observability.tsx +++ b/x-pack/plugins/observability/public/pages/overview/loading_observability.tsx @@ -20,7 +20,7 @@ const CentralizedFlexGroup = styled(EuiFlexGroup)` min-height: calc(100vh - ${(props) => props.theme.eui.euiHeaderChildSize}); `; -export const LoadingObservability = () => { +export function LoadingObservability() { const theme = useContext(ThemeContext); return ( @@ -50,4 +50,4 @@ export const LoadingObservability = () => { ); -}; +} diff --git a/x-pack/plugins/observability/public/typings/eui_styled_components.tsx b/x-pack/plugins/observability/public/typings/eui_styled_components.tsx index aab16f9d79c4b..9e547b58bc736 100644 --- a/x-pack/plugins/observability/public/typings/eui_styled_components.tsx +++ b/x-pack/plugins/observability/public/typings/eui_styled_components.tsx @@ -16,23 +16,25 @@ export interface EuiTheme { darkMode: boolean; } -const EuiThemeProvider = < +function EuiThemeProvider< OuterTheme extends styledComponents.DefaultTheme = styledComponents.DefaultTheme >({ darkMode = false, ...otherProps }: Omit, 'theme'> & { darkMode?: boolean; -}) => ( - ({ - ...outerTheme, - eui: darkMode ? euiDarkVars : euiLightVars, - darkMode, - })} - /> -); +}) { + return ( + ({ + ...outerTheme, + eui: darkMode ? euiDarkVars : euiLightVars, + darkMode, + })} + /> + ); +} const { default: euiStyled, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.tsx index e898a362c7771..71734affd42ce 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.tsx @@ -35,7 +35,7 @@ const MyEuiSuperSelect = styled(EuiSuperSelect)` `; interface AddItemProps { field: FieldHook; - dataTestSubj: string; + dataTestSubj: string; // eslint-disable-line react/no-unused-prop-types idAria: string; isDisabled: boolean; } diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx index fd75c229d479d..49fe3438664c6 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx @@ -22,6 +22,7 @@ interface TagsFilterPopoverProps { selectedTags: string[]; tags: string[]; onSelectedTagsChanged: Dispatch>; + // eslint-disable-next-line react/no-unused-prop-types isLoading: boolean; // TO DO reimplement? } diff --git a/yarn.lock b/yarn.lock index 1bb8fab0372ae..fd6019750dda5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7557,6 +7557,15 @@ array-includes@^3.0.3: define-properties "^1.1.2" es-abstract "^1.7.0" +array-includes@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348" + integrity sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0" + is-string "^1.0.5" + array-initial@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/array-initial/-/array-initial-1.1.0.tgz#2fa74b26739371c3947bd7a7adc73be334b3d795" @@ -7643,6 +7652,15 @@ array.prototype.flatmap@^1.2.1: es-abstract "^1.10.0" function-bind "^1.1.1" +array.prototype.flatmap@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.3.tgz#1c13f84a178566042dd63de4414440db9222e443" + integrity sha512-OOEk+lkePcg+ODXIpvuU9PAryCikCJyo7GlDG1upleEpQRx6mzL9puEBkozQ5iAx20KV0l3DbyQwqciJtqe5Pg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + function-bind "^1.1.1" + arraybuffer.slice@~0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675" @@ -13369,39 +13387,39 @@ es-abstract@^1.10.0, es-abstract@^1.11.0, es-abstract@^1.13.0, es-abstract@^1.14 string.prototype.trimleft "^2.1.1" string.prototype.trimright "^2.1.1" -es-abstract@^1.15.0, es-abstract@^1.17.0-next.1: - version "1.17.4" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.4.tgz#e3aedf19706b20e7c2594c35fc0d57605a79e184" - integrity sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ== +es-abstract@^1.17.0, es-abstract@^1.17.4, es-abstract@^1.17.5: + version "1.17.6" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" + integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw== dependencies: es-to-primitive "^1.2.1" function-bind "^1.1.1" has "^1.0.3" has-symbols "^1.0.1" - is-callable "^1.1.5" - is-regex "^1.0.5" + is-callable "^1.2.0" + is-regex "^1.1.0" object-inspect "^1.7.0" object-keys "^1.1.1" object.assign "^4.1.0" - string.prototype.trimleft "^2.1.1" - string.prototype.trimright "^2.1.1" + string.prototype.trimend "^1.0.1" + string.prototype.trimstart "^1.0.1" -es-abstract@^1.17.4, es-abstract@^1.17.5: - version "1.17.6" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" - integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw== +es-abstract@^1.17.0-next.1: + version "1.17.4" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.4.tgz#e3aedf19706b20e7c2594c35fc0d57605a79e184" + integrity sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ== dependencies: es-to-primitive "^1.2.1" function-bind "^1.1.1" has "^1.0.3" has-symbols "^1.0.1" - is-callable "^1.2.0" - is-regex "^1.1.0" + is-callable "^1.1.5" + is-regex "^1.0.5" object-inspect "^1.7.0" object-keys "^1.1.1" object.assign "^4.1.0" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" + string.prototype.trimleft "^2.1.1" + string.prototype.trimright "^2.1.1" es-get-iterator@^1.1.0: version "1.1.0" @@ -13713,11 +13731,6 @@ eslint-plugin-es@^3.0.0: eslint-utils "^2.0.0" regexpp "^3.0.0" -eslint-plugin-eslint-plugin@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-eslint-plugin/-/eslint-plugin-eslint-plugin-2.1.0.tgz#a7a00f15a886957d855feacaafee264f039e62d5" - integrity sha512-kT3A/ZJftt28gbl/Cv04qezb/NQ1dwYIbi8lyf806XMxkus7DvOVCLIfTXMrorp322Pnoez7+zabXH29tADIDg== - eslint-plugin-import@^2.19.1: version "2.19.1" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.19.1.tgz#5654e10b7839d064dd0d46cd1b88ec2133a11448" @@ -13804,21 +13817,22 @@ eslint-plugin-react-perf@^3.2.3: resolved "https://registry.yarnpkg.com/eslint-plugin-react-perf/-/eslint-plugin-react-perf-3.2.3.tgz#e28d42d3a1f7ec3c8976a94735d8e17e7d652a45" integrity sha512-bMiPt7uywwS1Ly25n752NE3Ei0XBZ3igplTkZ8GPJKyZVVUd3cHgzILGeQW2HIeAkzQ9zwk9HM6EcYDipdFk3Q== -eslint-plugin-react@^7.17.0: - version "7.17.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.17.0.tgz#a31b3e134b76046abe3cd278e7482bd35a1d12d7" - integrity sha512-ODB7yg6lxhBVMeiH1c7E95FLD4E/TwmFjltiU+ethv7KPdCwgiFuOZg9zNRHyufStTDLl/dEFqI2Q1VPmCd78A== +eslint-plugin-react@^7.20.3: + version "7.20.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.20.3.tgz#0590525e7eb83890ce71f73c2cf836284ad8c2f1" + integrity sha512-txbo090buDeyV0ugF3YMWrzLIUqpYTsWSDZV9xLSmExE1P/Kmgg9++PD931r+KEWS66O1c9R4srLVVHmeHpoAg== dependencies: - array-includes "^3.0.3" + array-includes "^3.1.1" + array.prototype.flatmap "^1.2.3" doctrine "^2.1.0" - eslint-plugin-eslint-plugin "^2.1.0" has "^1.0.3" - jsx-ast-utils "^2.2.3" - object.entries "^1.1.0" - object.fromentries "^2.0.1" - object.values "^1.1.0" + jsx-ast-utils "^2.4.1" + object.entries "^1.1.2" + object.fromentries "^2.0.2" + object.values "^1.1.1" prop-types "^15.7.2" - resolve "^1.13.1" + resolve "^1.17.0" + string.prototype.matchall "^4.0.2" eslint-rule-composer@^0.3.0: version "0.3.0" @@ -18059,6 +18073,15 @@ internal-ip@^4.3.0: default-gateway "^4.2.0" ipaddr.js "^1.9.0" +internal-slot@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.2.tgz#9c2e9fb3cd8e5e4256c6f45fe310067fcfa378a3" + integrity sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g== + dependencies: + es-abstract "^1.17.0-next.1" + has "^1.0.3" + side-channel "^1.0.2" + interpret@1.2.0, interpret@^1.0.0, interpret@^1.1.0, interpret@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" @@ -20100,12 +20123,12 @@ jsx-ast-utils@^2.2.1: array-includes "^3.0.3" object.assign "^4.1.0" -jsx-ast-utils@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz#8a9364e402448a3ce7f14d357738310d9248054f" - integrity sha512-EdIHFMm+1BPynpKOpdPqiOsvnIrInRGJD7bzPZdPkjitQEqpdpUuFpq4T0npZFKTiB3RhWFdGN+oqOJIdhDhQA== +jsx-ast-utils@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz#1114a4c1209481db06c690c2b4f488cc665f657e" + integrity sha512-z1xSldJ6imESSzOjd3NNkieVJKRlKYSOtMG8SFyCj2FIrvSaSuli/WjpBkEzCBoR9bYYYFgqJw61Xhu7Lcgk+w== dependencies: - array-includes "^3.0.3" + array-includes "^3.1.1" object.assign "^4.1.0" jsx-to-string@^1.4.0: @@ -23352,6 +23375,15 @@ object.entries@^1.0.4, object.entries@^1.1.0, object.entries@^1.1.1: function-bind "^1.1.1" has "^1.0.3" +object.entries@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.2.tgz#bc73f00acb6b6bb16c203434b10f9a7e797d3add" + integrity sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + has "^1.0.3" + object.fromentries@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-1.0.0.tgz#e90ec27445ec6e37f48be9af9077d9aa8bef0d40" @@ -23362,16 +23394,6 @@ object.fromentries@^1.0.0: function-bind "^1.1.1" has "^1.0.1" -object.fromentries@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.1.tgz#050f077855c7af8ae6649f45c80b16ee2d31e704" - integrity sha512-PUQv8Hbg3j2QX0IQYv3iAGCbGcu4yY4KQ92/dhA4sFSixBmSmp13UpDLs6jGK8rBtbmhNNIK99LD2k293jpiGA== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.15.0" - function-bind "^1.1.1" - has "^1.0.3" - object.fromentries@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.2.tgz#4a09c9b9bb3843dd0f89acdb517a794d4f355ac9" @@ -27505,7 +27527,7 @@ resolve@1.8.1: dependencies: path-parse "^1.0.5" -resolve@^1.1.10, resolve@^1.1.5, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.0, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.7.1, resolve@^1.8.1: +resolve@^1.1.10, resolve@^1.1.5, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.0, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.17.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.7.1, resolve@^1.8.1: version "1.17.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== @@ -29396,6 +29418,18 @@ string.prototype.matchall@^3.0.0: has-symbols "^1.0.0" regexp.prototype.flags "^1.2.0" +string.prototype.matchall@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.2.tgz#48bb510326fb9fdeb6a33ceaa81a6ea04ef7648e" + integrity sha512-N/jp6O5fMf9os0JU3E72Qhf590RSRZU/ungsL/qJUYVTNv7hTG0P/dbPjxINVN9jpscu3nzYwKESU3P3RY5tOg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0" + has-symbols "^1.0.1" + internal-slot "^1.0.2" + regexp.prototype.flags "^1.3.0" + side-channel "^1.0.2" + string.prototype.padend@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz#f3aaef7c1719f170c5eab1c32bf780d96e21f2f0" From 76150a4026c161fc4a264e83724c576917f2fb5f Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Mon, 27 Jul 2020 08:20:27 -0500 Subject: [PATCH 33/96] Observability i18n fixes (#72984) * Format `xpack.apm.percentOfParent` correctly so the transactions page in APM doesn't crash. In English it reads like, "(X% of transaction)". I'm not sure what the intention of the changed translation is, but I've changed it to be the equivalent of "(X% transaction)". A correction to the Japanese form here would be appreciated, but this fixes the crash and gets the message across. * Format `xpack.infra.logs.customizeLogs.textSizeRadioGroup` correctly. This was not causing the whole logs page to crash, but was causing an error in the JS console. --- x-pack/plugins/translations/translations/ja-JP.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index cf789d1e7c450..8baebbb4939be 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4257,7 +4257,7 @@ "xpack.apm.metrics.transactionChart.transactionDurationLabel": "トランザクション時間", "xpack.apm.metrics.transactionChart.transactionsPerMinuteLabel": "1 分あたりのトランザクション数", "xpack.apm.notAvailableLabel": "N/A", - "xpack.apm.percentOfParent": "({parentType, select, transaction { 件中 {value} 件のトランザクション} トレース {trace} })", + "xpack.apm.percentOfParent": "({value} {parentType, select, transaction {トランザクション} trace {トレース} })", "xpack.apm.propertiesTable.agentFeature.noDataAvailableLabel": "利用可能なデータがありません", "xpack.apm.propertiesTable.agentFeature.noResultFound": "\"{value}\"に対する結果が見つかりませんでした。", "xpack.apm.propertiesTable.tabs.exceptionStacktraceLabel": "例外のスタックトレース", @@ -7430,7 +7430,7 @@ "xpack.infra.logs.customizeLogs.customizeButtonLabel": "カスタマイズ", "xpack.infra.logs.customizeLogs.lineWrappingFormRowLabel": "改行", "xpack.infra.logs.customizeLogs.textSizeFormRowLabel": "テキストサイズ", - "xpack.infra.logs.customizeLogs.textSizeRadioGroup": "{textScale, select, small {小さい} 中くらい {Medium} 大きい {Large} その他の {{textScale}} }", + "xpack.infra.logs.customizeLogs.textSizeRadioGroup": "{textScale, select, small {小さい} medium {中くらい} large {大きい} other {{textScale}}}", "xpack.infra.logs.customizeLogs.wrapLongLinesSwitchLabel": "長い行を改行", "xpack.infra.logs.emptyView.checkForNewDataButtonLabel": "新規データを確認", "xpack.infra.logs.emptyView.noLogMessageDescription": "フィルターを調整してみてください。", From bc65c5e160031e8e93e77e2bab72574ae7fefe9b Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 27 Jul 2020 16:40:02 +0300 Subject: [PATCH 34/96] [Security Solution][Cases] Create useAllCasesModal hook (#72602) --- .../cases/components/all_cases/index.test.tsx | 148 +++++++++++++++--- .../cases/components/all_cases/index.tsx | 93 +++++++---- .../components/all_cases_modal/index.tsx | 1 + .../all_cases_modal.test.tsx | 74 +++++++++ .../use_all_cases_modal/all_cases_modal.tsx | 47 ++++++ .../use_all_cases_modal/index.test.tsx | 143 +++++++++++++++++ .../components/use_all_cases_modal/index.tsx | 85 ++++++++++ .../use_all_cases_modal/translations.ts | 10 ++ .../common/lib/kibana/__mocks__/index.ts | 1 + .../components/graph_overlay/index.tsx | 48 ++---- .../components/timeline/properties/index.tsx | 46 +----- 11 files changed, 567 insertions(+), 129 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.test.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/translations.ts diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx index 23cabd6778cc0..f5ed151ebac3c 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx @@ -14,6 +14,8 @@ import { TestProviders } from '../../../common/mock'; import { useGetCasesMockState } from '../../containers/mock'; import * as i18n from './translations'; +import { useKibana } from '../../../common/lib/kibana'; +import { createUseKibanaMock } from '../../../common/mock/kibana_react'; import { getEmptyTagValue } from '../../../common/components/empty_value'; import { useDeleteCases } from '../../containers/use_delete_cases'; import { useGetCases } from '../../containers/use_get_cases'; @@ -26,6 +28,7 @@ jest.mock('../../containers/use_delete_cases'); jest.mock('../../containers/use_get_cases'); jest.mock('../../containers/use_get_cases_status'); +const useKibanaMock = useKibana as jest.Mock; const useDeleteCasesMock = useDeleteCases as jest.Mock; const useGetCasesMock = useGetCases as jest.Mock; const useGetCasesStatusMock = useGetCasesStatus as jest.Mock; @@ -33,6 +36,8 @@ const useUpdateCasesMock = useUpdateCases as jest.Mock; jest.mock('../../../common/components/link_to'); +jest.mock('../../../common/lib/kibana'); + describe('AllCases', () => { const dispatchResetIsDeleted = jest.fn(); const dispatchResetIsUpdated = jest.fn(); @@ -45,6 +50,7 @@ describe('AllCases', () => { const setSelectedCases = jest.fn(); const updateBulkStatus = jest.fn(); const fetchCasesStatus = jest.fn(); + const onRowClick = jest.fn(); const emptyTag = getEmptyTagValue().props.children; const defaultGetCases = { @@ -77,6 +83,9 @@ describe('AllCases', () => { dispatchResetIsUpdated, updateBulkStatus, }; + + let navigateToApp: jest.Mock; + /* eslint-disable no-console */ // Silence until enzyme fixed to use ReactTestUtils.act() const originalError = console.error; @@ -89,10 +98,20 @@ describe('AllCases', () => { /* eslint-enable no-console */ beforeEach(() => { jest.resetAllMocks(); - useUpdateCasesMock.mockImplementation(() => defaultUpdateCases); - useGetCasesMock.mockImplementation(() => defaultGetCases); - useDeleteCasesMock.mockImplementation(() => defaultDeleteCases); - useGetCasesStatusMock.mockImplementation(() => defaultCasesStatus); + navigateToApp = jest.fn(); + const kibanaMock = createUseKibanaMock()(); + useKibanaMock.mockReturnValue({ + ...kibanaMock, + services: { + application: { + navigateToApp, + }, + }, + }); + useUpdateCasesMock.mockReturnValue(defaultUpdateCases); + useGetCasesMock.mockReturnValue(defaultGetCases); + useDeleteCasesMock.mockReturnValue(defaultDeleteCases); + useGetCasesStatusMock.mockReturnValue(defaultCasesStatus); moment.tz.setDefault('UTC'); }); it('should render AllCases', () => { @@ -125,7 +144,7 @@ describe('AllCases', () => { ); }); it('should render empty fields', () => { - useGetCasesMock.mockImplementation(() => ({ + useGetCasesMock.mockReturnValue({ ...defaultGetCases, data: { ...defaultGetCases.data, @@ -141,7 +160,7 @@ describe('AllCases', () => { }, ], }, - })); + }); const wrapper = mount( @@ -202,10 +221,10 @@ describe('AllCases', () => { }); }); it('opens case when row action icon clicked', () => { - useGetCasesMock.mockImplementation(() => ({ + useGetCasesMock.mockReturnValue({ ...defaultGetCases, filterOptions: { ...defaultGetCases.filterOptions, status: 'closed' }, - })); + }); const wrapper = mount( @@ -223,10 +242,11 @@ describe('AllCases', () => { }); }); it('Bulk delete', () => { - useGetCasesMock.mockImplementation(() => ({ + useGetCasesMock.mockReturnValue({ ...defaultGetCases, selectedCases: useGetCasesMockState.data.cases, - })); + }); + useDeleteCasesMock .mockReturnValueOnce({ ...defaultDeleteCases, @@ -257,10 +277,10 @@ describe('AllCases', () => { ); }); it('Bulk close status update', () => { - useGetCasesMock.mockImplementation(() => ({ + useGetCasesMock.mockReturnValue({ ...defaultGetCases, selectedCases: useGetCasesMockState.data.cases, - })); + }); const wrapper = mount( @@ -272,14 +292,14 @@ describe('AllCases', () => { expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, 'closed'); }); it('Bulk open status update', () => { - useGetCasesMock.mockImplementation(() => ({ + useGetCasesMock.mockReturnValue({ ...defaultGetCases, selectedCases: useGetCasesMockState.data.cases, filterOptions: { ...defaultGetCases.filterOptions, status: 'closed', }, - })); + }); const wrapper = mount( @@ -291,10 +311,10 @@ describe('AllCases', () => { expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, 'open'); }); it('isDeleted is true, refetch', () => { - useDeleteCasesMock.mockImplementation(() => ({ + useDeleteCasesMock.mockReturnValue({ ...defaultDeleteCases, isDeleted: true, - })); + }); mount( @@ -306,10 +326,10 @@ describe('AllCases', () => { expect(dispatchResetIsDeleted).toBeCalled(); }); it('isUpdated is true, refetch', () => { - useUpdateCasesMock.mockImplementation(() => ({ + useUpdateCasesMock.mockReturnValue({ ...defaultUpdateCases, isUpdated: true, - })); + }); mount( @@ -320,4 +340,96 @@ describe('AllCases', () => { expect(fetchCasesStatus).toBeCalled(); expect(dispatchResetIsUpdated).toBeCalled(); }); + + it('should not render header when modal=true', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="all-cases-header"]').exists()).toBe(false); + }); + + it('should not render table utility bar when modal=true', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="case-table-utility-bar-actions"]').exists()).toBe(false); + }); + + it('case table should not be selectable when modal=true', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="cases-table"]').first().prop('isSelectable')).toBe(false); + }); + + it('should call onRowClick with no cases and modal=true', () => { + useGetCasesMock.mockReturnValue({ + ...defaultGetCases, + data: { + ...defaultGetCases.data, + total: 0, + cases: [], + }, + }); + + const wrapper = mount( + + + + ); + + wrapper.find('[data-test-subj="cases-table-add-case"]').first().simulate('click'); + expect(onRowClick).toHaveBeenCalled(); + }); + + it('should call navigateToApp with no cases and modal=false', () => { + useGetCasesMock.mockReturnValue({ + ...defaultGetCases, + data: { + ...defaultGetCases.data, + total: 0, + cases: [], + }, + }); + + const wrapper = mount( + + + + ); + + wrapper.find('[data-test-subj="cases-table-add-case"]').first().simulate('click'); + expect(navigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: '/create' }); + }); + + it('should call onRowClick when clicking a case with modal=true', () => { + const wrapper = mount( + + + + ); + + wrapper.find('[data-test-subj="cases-table-row-1"]').first().simulate('click'); + expect(onRowClick).toHaveBeenCalledWith('1'); + }); + + it('should NOT call onRowClick when clicking a case with modal=true', () => { + const wrapper = mount( + + + + ); + + wrapper.find('[data-test-subj="cases-table-row-1"]').first().simulate('click'); + expect(onRowClick).not.toHaveBeenCalled(); + }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx index f46dd9e858c7f..42a87de2aa07b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -/* eslint-disable react-hooks/exhaustive-deps */ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { EuiBasicTable, @@ -16,7 +15,7 @@ import { EuiTableSortingType, } from '@elastic/eui'; import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types'; -import { isEmpty } from 'lodash/fp'; +import { isEmpty, memoize } from 'lodash/fp'; import styled, { css } from 'styled-components'; import * as i18n from './translations'; @@ -137,7 +136,7 @@ export const AllCases = React.memo( (refetchFilter: () => void) => { filterRefetch.current = refetchFilter; }, - [filterRefetch.current] + [filterRefetch] ); const refreshCases = useCallback( (dataRefresh = true) => { @@ -149,7 +148,7 @@ export const AllCases = React.memo( filterRefetch.current(); } }, - [filterOptions, queryParams, filterRefetch.current] + [filterRefetch, refetchCases, setSelectedCases, fetchCasesStatus] ); useEffect(() => { @@ -161,7 +160,7 @@ export const AllCases = React.memo( refreshCases(); dispatchResetIsUpdated(); } - }, [isDeleted, isUpdated]); + }, [isDeleted, isUpdated, refreshCases, dispatchResetIsDeleted, dispatchResetIsUpdated]); const confirmDeleteModal = useMemo( () => ( ( )} /> ), - [deleteBulk, deleteThisCase, isDisplayConfirmDeleteModal] + [ + deleteBulk, + deleteThisCase, + isDisplayConfirmDeleteModal, + handleToggleModal, + handleOnDeleteConfirm, + ] ); - const toggleDeleteModal = useCallback((deleteCase: Case) => { - handleToggleModal(); - setDeleteThisCase(deleteCase); - }, []); + const toggleDeleteModal = useCallback( + (deleteCase: Case) => { + handleToggleModal(); + setDeleteThisCase(deleteCase); + }, + [handleToggleModal] + ); const toggleBulkDeleteModal = useCallback( (caseIds: string[]) => { @@ -195,14 +203,14 @@ export const AllCases = React.memo( const convertToDeleteCases: DeleteCase[] = caseIds.map((id) => ({ id })); setDeleteBulk(convertToDeleteCases); }, - [selectedCases] + [selectedCases, setDeleteBulk, handleToggleModal] ); const handleUpdateCaseStatus = useCallback( (status: string) => { updateBulkStatus(selectedCases, status); }, - [selectedCases] + [selectedCases, updateBulkStatus] ); const selectedCaseIds = useMemo( @@ -223,7 +231,7 @@ export const AllCases = React.memo( })} /> ), - [selectedCaseIds, filterOptions.status, toggleBulkDeleteModal] + [selectedCaseIds, filterOptions.status, toggleBulkDeleteModal, handleUpdateCaseStatus] ); const handleDispatchUpdate = useCallback( (args: Omit) => { @@ -278,7 +286,7 @@ export const AllCases = React.memo( setQueryParams(newQueryParams); refreshCases(false); }, - [queryParams] + [queryParams, refreshCases, setQueryParams] ); const onFilterChangedCallback = useCallback( @@ -291,7 +299,7 @@ export const AllCases = React.memo( setFilters(newFilterOptions); refreshCases(false); }, - [filterOptions, queryParams] + [refreshCases, setQueryParams, setFilters] ); const memoizedGetCasesColumns = useMemo( @@ -311,9 +319,10 @@ export const AllCases = React.memo( const sorting: EuiTableSortingType = { sort: { field: queryParams.sortField, direction: queryParams.sortOrder }, }; + const euiBasicTableSelectionProps = useMemo>( () => ({ onSelectionChange: setSelectedCases }), - [selectedCases] + [setSelectedCases] ); const isCasesLoading = useMemo( () => loading.indexOf('cases') > -1 || loading.indexOf('caseUpdate') > -1, @@ -322,6 +331,35 @@ export const AllCases = React.memo( const isDataEmpty = useMemo(() => data.total === 0, [data]); const TableWrap = useMemo(() => (isModal ? 'span' : Panel), [isModal]); + + const onTableRowClick = useMemo( + () => + memoize<(id: string) => () => void>((id) => () => { + if (onRowClick) { + onRowClick(id); + } + }), + [onRowClick] + ); + + const tableRowProps = useCallback( + (item) => { + const rowProps = { + 'data-test-subj': `cases-table-row-${item.id}`, + }; + + if (isModal) { + return { + ...rowProps, + onClick: onTableRowClick(item.id), + }; + } + + return rowProps; + }, + [isModal, onTableRowClick] + ); + return ( <> {!isEmpty(actionsErrors) && ( @@ -329,7 +367,13 @@ export const AllCases = React.memo( )} {!isModal && ( - + ( {!isModal && ( - + {i18n.SHOWING_SELECTED_CASES(selectedCases.length)} @@ -441,6 +485,7 @@ export const AllCases = React.memo( onClick={goToCreateCase} href={formatUrl(getCreateCaseUrl())} iconType="plusInCircle" + data-test-subj="cases-table-add-case" > {i18n.ADD_NEW_CASE} @@ -449,17 +494,7 @@ export const AllCases = React.memo( } onChange={tableOnChangeCallback} pagination={memoizedPagination} - rowProps={(item) => - isModal - ? { - onClick: () => { - if (onRowClick != null) { - onRowClick(item.id); - } - }, - } - : {} - } + rowProps={tableRowProps} selection={userCanCrud && !isModal ? euiBasicTableSelectionProps : undefined} sorting={sorting} /> diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.tsx index d8f2e5293ee1b..efbe3e667c27b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.tsx @@ -12,6 +12,7 @@ import { EuiModalHeaderTitle, EuiOverlayMask, } from '@elastic/eui'; + import { useGetUserSavedObjectPermissions } from '../../../common/lib/kibana'; import { AllCases } from '../all_cases'; import * as i18n from './translations'; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.test.tsx b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.test.tsx new file mode 100644 index 0000000000000..6039fec2464cc --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.test.tsx @@ -0,0 +1,74 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; +import '../../../common/mock/match_media'; +import { AllCasesModal } from './all_cases_modal'; +import { TestProviders } from '../../../common/mock'; + +jest.mock('../all_cases', () => { + const AllCases = () => { + return <>; + }; + return { AllCases }; +}); + +jest.mock('../../../common/lib/kibana', () => { + const originalModule = jest.requireActual('../../../common/lib/kibana'); + return { + ...originalModule, + useGetUserSavedObjectPermissions: jest.fn(), + }; +}); + +const onCloseCaseModal = jest.fn(); +const onRowClick = jest.fn(); +const defaultProps = { + onCloseCaseModal, + onRowClick, +}; + +describe('AllCasesModal', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('renders', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj='all-cases-modal']`).exists()).toBeTruthy(); + }); + + it('Closing modal calls onCloseCaseModal', () => { + const wrapper = mount( + + + + ); + + wrapper.find('.euiModal__closeIcon').first().simulate('click'); + expect(onCloseCaseModal).toBeCalled(); + }); + + it('pass the correct props to AllCases component', () => { + const wrapper = mount( + + + + ); + + const props = wrapper.find('AllCases').props(); + expect(props).toEqual({ + userCanCrud: false, + onRowClick, + isModal: true, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx new file mode 100644 index 0000000000000..7a12f9211e969 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx @@ -0,0 +1,47 @@ +/* + * 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, { memo } from 'react'; +import { + EuiModal, + EuiModalBody, + EuiModalHeader, + EuiModalHeaderTitle, + EuiOverlayMask, +} from '@elastic/eui'; + +import { useGetUserSavedObjectPermissions } from '../../../common/lib/kibana'; +import { AllCases } from '../all_cases'; +import * as i18n from './translations'; + +export interface AllCasesModalProps { + onCloseCaseModal: () => void; + onRowClick: (id?: string) => void; +} + +const AllCasesModalComponent: React.FC = ({ + onCloseCaseModal, + onRowClick, +}: AllCasesModalProps) => { + const userPermissions = useGetUserSavedObjectPermissions(); + const userCanCrud = userPermissions?.crud ?? false; + return ( + + + + {i18n.SELECT_CASE_TITLE} + + + + + + + ); +}; + +export const AllCasesModal = memo(AllCasesModalComponent); + +AllCasesModal.displayName = 'AllCasesModal'; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx new file mode 100644 index 0000000000000..b5bf68cbf6dc8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable react/display-name */ + +import React from 'react'; +import { renderHook, act } from '@testing-library/react-hooks'; + +import { useKibana } from '../../../common/lib/kibana'; +import '../../../common/mock/match_media'; +import { TimelineId } from '../../../../common/types/timeline'; +import { useAllCasesModal, UseAllCasesModalProps, UseAllCasesModalReturnedValues } from '.'; +import { TestProviders } from '../../../common/mock'; +import { createUseKibanaMock } from '../../../common/mock/kibana_react'; + +jest.mock('../../../common/lib/kibana'); + +const useKibanaMock = useKibana as jest.Mock; + +describe('useAllCasesModal', () => { + const navigateToApp = jest.fn(() => Promise.resolve()); + + beforeEach(() => { + jest.clearAllMocks(); + const kibanaMock = createUseKibanaMock()(); + useKibanaMock.mockImplementation(() => ({ + ...kibanaMock, + services: { + application: { + navigateToApp, + }, + }, + })); + }); + + it('init', async () => { + const { result } = renderHook( + () => useAllCasesModal({ timelineId: TimelineId.test }), + { + wrapper: ({ children }) => {children}, + } + ); + + expect(result.current.showModal).toBe(false); + }); + + it('opens the modal', async () => { + const { result } = renderHook( + () => useAllCasesModal({ timelineId: TimelineId.test }), + { + wrapper: ({ children }) => {children}, + } + ); + + act(() => { + result.current.onOpenModal(); + }); + + expect(result.current.showModal).toBe(true); + }); + + it('closes the modal', async () => { + const { result } = renderHook( + () => useAllCasesModal({ timelineId: TimelineId.test }), + { + wrapper: ({ children }) => {children}, + } + ); + + act(() => { + result.current.onOpenModal(); + result.current.onCloseModal(); + }); + + expect(result.current.showModal).toBe(false); + }); + + it('returns a memoized value', async () => { + const { result, rerender } = renderHook( + () => useAllCasesModal({ timelineId: TimelineId.test }), + { + wrapper: ({ children }) => {children}, + } + ); + + const result1 = result.current; + act(() => rerender()); + const result2 = result.current; + + expect(result1).toBe(result2); + }); + + it('closes the modal when clicking a row', async () => { + const { result } = renderHook( + () => useAllCasesModal({ timelineId: TimelineId.test }), + { + wrapper: ({ children }) => {children}, + } + ); + + act(() => { + result.current.onOpenModal(); + result.current.onRowClick(); + }); + + expect(result.current.showModal).toBe(false); + }); + + it('navigates to the correct path without id', async () => { + const { result } = renderHook( + () => useAllCasesModal({ timelineId: TimelineId.test }), + { + wrapper: ({ children }) => {children}, + } + ); + + act(() => { + result.current.onOpenModal(); + result.current.onRowClick(); + }); + + expect(navigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: '/create' }); + }); + + it('navigates to the correct path with id', async () => { + const { result } = renderHook( + () => useAllCasesModal({ timelineId: TimelineId.test }), + { + wrapper: ({ children }) => {children}, + } + ); + + act(() => { + result.current.onOpenModal(); + result.current.onRowClick('case-id'); + }); + + expect(navigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: '/case-id' }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.tsx b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.tsx new file mode 100644 index 0000000000000..f7fc7963b3844 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useCallback, useMemo } from 'react'; + +import { useDispatch, useSelector } from 'react-redux'; +import { APP_ID } from '../../../../common/constants'; +import { SecurityPageName } from '../../../app/types'; +import { useKibana } from '../../../common/lib/kibana'; +import { getCaseDetailsUrl, getCreateCaseUrl } from '../../../common/components/link_to'; +import { State } from '../../../common/store'; +import { setInsertTimeline } from '../../../timelines/store/timeline/actions'; +import { timelineSelectors } from '../../../timelines/store/timeline'; + +import { AllCasesModal } from './all_cases_modal'; + +export interface UseAllCasesModalProps { + timelineId: string; +} + +export interface UseAllCasesModalReturnedValues { + Modal: React.FC; + showModal: boolean; + onCloseModal: () => void; + onOpenModal: () => void; + onRowClick: (id?: string) => void; +} + +export const useAllCasesModal = ({ + timelineId, +}: UseAllCasesModalProps): UseAllCasesModalReturnedValues => { + const dispatch = useDispatch(); + const { navigateToApp } = useKibana().services.application; + const timeline = useSelector((state: State) => + timelineSelectors.selectTimeline(state, timelineId) + ); + + const [showModal, setShowModal] = useState(false); + const onCloseModal = useCallback(() => setShowModal(false), []); + const onOpenModal = useCallback(() => setShowModal(true), []); + + const onRowClick = useCallback( + async (id?: string) => { + onCloseModal(); + + await navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { + path: id != null ? getCaseDetailsUrl({ id }) : getCreateCaseUrl(), + }); + + dispatch( + setInsertTimeline({ + graphEventId: timeline.graphEventId ?? '', + timelineId, + timelineSavedObjectId: timeline.savedObjectId ?? '', + timelineTitle: timeline.title, + }) + ); + }, + // dispatch causes unnecessary rerenders + // eslint-disable-next-line react-hooks/exhaustive-deps + [timeline, navigateToApp, onCloseModal, timelineId] + ); + + const Modal: React.FC = useCallback( + () => + showModal ? : null, + [onCloseModal, onRowClick, showModal] + ); + + const state = useMemo( + () => ({ + Modal, + showModal, + onCloseModal, + onOpenModal, + onRowClick, + }), + [showModal, onCloseModal, onOpenModal, onRowClick, Modal] + ); + + return state; +}; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/translations.ts b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/translations.ts new file mode 100644 index 0000000000000..e0f84d8541424 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/translations.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 { i18n } from '@kbn/i18n'; +export const SELECT_CASE_TITLE = i18n.translate('xpack.securitySolution.case.caseModal.title', { + defaultMessage: 'Select case to attach timeline', +}); diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts index 6ada887ece175..2c52acd3ec747 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts @@ -24,3 +24,4 @@ export const useToasts = jest.fn(() => notificationServiceMock.createStartContra export const useCurrentUser = jest.fn(); export const withKibana = jest.fn(createWithKibanaMock()); export const KibanaContextProvider = jest.fn(createKibanaContextProviderMock()); +export const useGetUserSavedObjectPermissions = jest.fn(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx index 54b30aca44a1f..97d1d11395c7f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx @@ -13,18 +13,14 @@ import { EuiToolTip, } from '@elastic/eui'; import { noop } from 'lodash/fp'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { connect, ConnectedProps, useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; -import { SecurityPageName } from '../../../app/types'; import { FULL_SCREEN } from '../timeline/body/column_headers/translations'; -import { AllCasesModal } from '../../../cases/components/all_cases_modal'; import { EXIT_FULL_SCREEN } from '../../../common/components/exit_full_screen/translations'; -import { APP_ID, FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants'; +import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants'; import { useFullScreen } from '../../../common/containers/use_full_screen'; -import { getCaseDetailsUrl, getCreateCaseUrl } from '../../../common/components/link_to'; -import { useKibana } from '../../../common/lib/kibana'; import { State } from '../../../common/store'; import { TimelineId, TimelineType } from '../../../../common/types/timeline'; import { timelineSelectors } from '../../store/timeline'; @@ -32,12 +28,9 @@ import { timelineDefaults } from '../../store/timeline/defaults'; import { TimelineModel } from '../../store/timeline/model'; import { isFullScreen } from '../timeline/body/column_headers'; import { NewCase, ExistingCase } from '../timeline/properties/helpers'; -import { UNTITLED_TIMELINE } from '../timeline/properties/translations'; -import { - setInsertTimeline, - updateTimelineGraphEventId, -} from '../../../timelines/store/timeline/actions'; +import { updateTimelineGraphEventId } from '../../../timelines/store/timeline/actions'; import { Resolver } from '../../../resolver/view'; +import { useAllCasesModal } from '../../../cases/components/use_all_cases_modal'; import * as i18n from './translations'; @@ -112,35 +105,16 @@ const GraphOverlayComponent = ({ timelineType, }: OwnProps & PropsFromRedux) => { const dispatch = useDispatch(); - const { navigateToApp } = useKibana().services.application; const onCloseOverlay = useCallback(() => { dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: '' })); }, [dispatch, timelineId]); - const [showCaseModal, setShowCaseModal] = useState(false); - const onOpenCaseModal = useCallback(() => setShowCaseModal(true), []); - const onCloseCaseModal = useCallback(() => setShowCaseModal(false), [setShowCaseModal]); + const currentTimeline = useSelector((state: State) => timelineSelectors.selectTimeline(state, timelineId) ); - const onRowClick = useCallback( - (id?: string) => { - onCloseCaseModal(); - - navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { - path: id != null ? getCaseDetailsUrl({ id }) : getCreateCaseUrl(), - }).then(() => { - dispatch( - setInsertTimeline({ - graphEventId, - timelineId, - timelineSavedObjectId: currentTimeline.savedObjectId, - timelineTitle: title.length > 0 ? title : UNTITLED_TIMELINE, - }) - ); - }); - }, - [currentTimeline, dispatch, graphEventId, navigateToApp, onCloseCaseModal, timelineId, title] - ); + + const { Modal: AllCasesModal, onOpenModal: onOpenCaseModal } = useAllCasesModal({ timelineId }); + const { timelineFullScreen, setTimelineFullScreen, @@ -210,11 +184,7 @@ const GraphOverlayComponent = ({ databaseDocumentID={graphEventId} resolverComponentInstanceID={currentTimeline.id} /> - + ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx index 96a773507a30a..9eea95a0a9b1a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx @@ -6,7 +6,6 @@ import React, { useState, useCallback, useMemo } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; import { TimelineStatusLiteral, TimelineTypeLiteral } from '../../../../../common/types/timeline'; import { useThrottledResizeObserver } from '../../../../common/components/utils'; import { Note } from '../../../../common/lib/note'; @@ -17,15 +16,7 @@ import { AssociateNote, UpdateNote } from '../../notes/helpers'; import { TimelineProperties } from './styles'; import { PropertiesRight } from './properties_right'; import { PropertiesLeft } from './properties_left'; -import { AllCasesModal } from '../../../../cases/components/all_cases_modal'; -import { SecurityPageName } from '../../../../app/types'; -import * as i18n from './translations'; -import { State } from '../../../../common/store'; -import { timelineSelectors } from '../../../store/timeline'; -import { setInsertTimeline } from '../../../store/timeline/actions'; -import { useKibana } from '../../../../common/lib/kibana'; -import { APP_ID } from '../../../../../common/constants'; -import { getCaseDetailsUrl, getCreateCaseUrl } from '../../../../common/components/link_to'; +import { useAllCasesModal } from '../../../../cases/components/use_all_cases_modal'; type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void; type UpdateTitle = ({ id, title }: { id: string; title: string }) => void; @@ -86,12 +77,10 @@ export const Properties = React.memo( updateTitle, usersViewing, }) => { - const { navigateToApp } = useKibana().services.application; const { ref, width = 0 } = useThrottledResizeObserver(300); const [showActions, setShowActions] = useState(false); const [showNotes, setShowNotes] = useState(false); const [showTimelineModal, setShowTimelineModal] = useState(false); - const dispatch = useDispatch(); const onButtonClick = useCallback(() => setShowActions(!showActions), [showActions]); const onToggleShowNotes = useCallback(() => setShowNotes(!showNotes), [showNotes]); @@ -103,32 +92,7 @@ export const Properties = React.memo( setShowTimelineModal(true); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const [showCaseModal, setShowCaseModal] = useState(false); - const onCloseCaseModal = useCallback(() => setShowCaseModal(false), []); - const onOpenCaseModal = useCallback(() => setShowCaseModal(true), []); - const currentTimeline = useSelector((state: State) => - timelineSelectors.selectTimeline(state, timelineId) - ); - - const onRowClick = useCallback( - (id?: string) => { - onCloseCaseModal(); - - navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { - path: id != null ? getCaseDetailsUrl({ id }) : getCreateCaseUrl(), - }).then(() => - dispatch( - setInsertTimeline({ - graphEventId, - timelineId, - timelineSavedObjectId: currentTimeline.savedObjectId, - timelineTitle: title.length > 0 ? title : i18n.UNTITLED_TIMELINE, - }) - ) - ); - }, - [currentTimeline, dispatch, graphEventId, navigateToApp, onCloseCaseModal, timelineId, title] - ); + const { Modal: AllCasesModal, onOpenModal: onOpenCaseModal } = useAllCasesModal({ timelineId }); const datePickerWidth = useMemo( () => @@ -195,11 +159,7 @@ export const Properties = React.memo( updateNote={updateNote} usersViewing={usersViewing} /> - + ); } From 2a77307af18ebce2422da9e9b2c91a0abdeb4ff3 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Mon, 27 Jul 2020 09:22:37 -0500 Subject: [PATCH 35/96] [APM] Use core.chrome to set window title (#73232) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I noticed there's a `core.chrome.docTitle.change` method. It can take a string or array of strings and provides its own separator character if given an array. Replace our setting of `window.document.title` directly in the APM and Observability plugins with using the chrome method. This changes the title to be, for example, "トランザクション - opbeans-node - サービス - APM - Elastic" instead of "トランザクション | opbeans-node | サービス | APM | Elastic", using " - " as a separator instead of " | ". --- .../app/Main/UpdateBreadcrumbs.test.tsx | 55 +++++++++++-------- .../components/app/Main/UpdateBreadcrumbs.tsx | 9 ++- .../public/application/application.test.tsx | 29 ++++++++++ .../public/application/index.tsx | 7 +-- 4 files changed, 66 insertions(+), 34 deletions(-) create mode 100644 x-pack/plugins/observability/public/application/application.test.tsx diff --git a/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.test.tsx b/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.test.tsx index 6aec6e9bf181a..2c19356a7fd52 100644 --- a/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.test.tsx @@ -16,6 +16,7 @@ import { } from '../../../context/ApmPluginContext/MockApmPluginContext'; const setBreadcrumbs = jest.fn(); +const changeTitle = jest.fn(); function mountBreadcrumb(route: string, params = '') { mount( @@ -27,6 +28,7 @@ function mountBreadcrumb(route: string, params = '') { ...mockApmPluginContextValue.core, chrome: { ...mockApmPluginContextValue.core.chrome, + docTitle: { change: changeTitle }, setBreadcrumbs, }, }, @@ -42,23 +44,14 @@ function mountBreadcrumb(route: string, params = '') { } describe('UpdateBreadcrumbs', () => { - let realDoc: Document; - beforeEach(() => { - realDoc = window.document; - (window.document as any) = { - title: 'Kibana', - }; setBreadcrumbs.mockReset(); + changeTitle.mockReset(); }); - afterEach(() => { - (window.document as any) = realDoc; - }); - - it('Homepage', () => { + it('Changes the homepage title', () => { mountBreadcrumb('/'); - expect(window.document.title).toMatchInlineSnapshot(`"APM"`); + expect(changeTitle).toHaveBeenCalledWith(['APM']); }); it('/services/:serviceName/errors/:groupId', () => { @@ -90,9 +83,13 @@ describe('UpdateBreadcrumbs', () => { }, { text: 'myGroupId', href: undefined }, ]); - expect(window.document.title).toMatchInlineSnapshot( - `"myGroupId | Errors | opbeans-node | Services | APM"` - ); + expect(changeTitle).toHaveBeenCalledWith([ + 'myGroupId', + 'Errors', + 'opbeans-node', + 'Services', + 'APM', + ]); }); it('/services/:serviceName/errors', () => { @@ -104,9 +101,12 @@ describe('UpdateBreadcrumbs', () => { { text: 'opbeans-node', href: '#/services/opbeans-node?kuery=myKuery' }, { text: 'Errors', href: undefined }, ]); - expect(window.document.title).toMatchInlineSnapshot( - `"Errors | opbeans-node | Services | APM"` - ); + expect(changeTitle).toHaveBeenCalledWith([ + 'Errors', + 'opbeans-node', + 'Services', + 'APM', + ]); }); it('/services/:serviceName/transactions', () => { @@ -118,9 +118,12 @@ describe('UpdateBreadcrumbs', () => { { text: 'opbeans-node', href: '#/services/opbeans-node?kuery=myKuery' }, { text: 'Transactions', href: undefined }, ]); - expect(window.document.title).toMatchInlineSnapshot( - `"Transactions | opbeans-node | Services | APM"` - ); + expect(changeTitle).toHaveBeenCalledWith([ + 'Transactions', + 'opbeans-node', + 'Services', + 'APM', + ]); }); it('/services/:serviceName/transactions/view?transactionName=my-transaction-name', () => { @@ -139,8 +142,12 @@ describe('UpdateBreadcrumbs', () => { }, { text: 'my-transaction-name', href: undefined }, ]); - expect(window.document.title).toMatchInlineSnapshot( - `"my-transaction-name | Transactions | opbeans-node | Services | APM"` - ); + expect(changeTitle).toHaveBeenCalledWith([ + 'my-transaction-name', + 'Transactions', + 'opbeans-node', + 'Services', + 'APM', + ]); }); }); diff --git a/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx b/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx index 7a27eae6e89f7..e7657c63f41bb 100644 --- a/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx @@ -22,10 +22,7 @@ interface Props { } function getTitleFromBreadCrumbs(breadcrumbs: Breadcrumb[]) { - return breadcrumbs - .map(({ value }) => value) - .reverse() - .join(' | '); + return breadcrumbs.map(({ value }) => value).reverse(); } class UpdateBreadcrumbsComponent extends React.Component { @@ -43,7 +40,9 @@ class UpdateBreadcrumbsComponent extends React.Component { } ); - document.title = getTitleFromBreadCrumbs(this.props.breadcrumbs); + this.props.core.chrome.docTitle.change( + getTitleFromBreadCrumbs(this.props.breadcrumbs) + ); this.props.core.chrome.setBreadcrumbs(breadcrumbs); } diff --git a/x-pack/plugins/observability/public/application/application.test.tsx b/x-pack/plugins/observability/public/application/application.test.tsx new file mode 100644 index 0000000000000..db7fca140be89 --- /dev/null +++ b/x-pack/plugins/observability/public/application/application.test.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 from 'react'; +import { renderApp } from './'; +import { Observable } from 'rxjs'; +import { CoreStart, AppMountParameters } from 'src/core/public'; + +describe('renderApp', () => { + it('renders', () => { + const core = ({ + application: { currentAppId$: new Observable(), navigateToUrl: () => {} }, + chrome: { docTitle: { change: () => {} }, setBreadcrumbs: () => {} }, + i18n: { Context: ({ children }: { children: React.ReactNode }) => children }, + uiSettings: { get: () => false }, + } as unknown) as CoreStart; + const params = ({ + element: window.document.createElement('div'), + } as unknown) as AppMountParameters; + + expect(() => { + const unmount = renderApp(core, params); + unmount(); + }).not.toThrowError(); + }); +}); diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index b0134ed8b746b..4c0147dc3cd51 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -23,10 +23,7 @@ const observabilityLabelBreadcrumb = { }; function getTitleFromBreadCrumbs(breadcrumbs: Breadcrumbs) { - return breadcrumbs - .map(({ text }) => text) - .reverse() - .join(' | '); + return breadcrumbs.map(({ text }) => text).reverse(); } function App() { @@ -42,7 +39,7 @@ function App() { const breadcrumb = [observabilityLabelBreadcrumb, ...route.breadcrumb]; useEffect(() => { core.chrome.setBreadcrumbs(breadcrumb); - document.title = getTitleFromBreadCrumbs(breadcrumb); + core.chrome.docTitle.change(getTitleFromBreadCrumbs(breadcrumb)); }, [core, breadcrumb]); const { query, path: pathParams } = useRouteParams(route.params); From aa45ac89b07be9ccaffdc05afb890de277ead4a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Mon, 27 Jul 2020 16:36:25 +0200 Subject: [PATCH 36/96] [Logs UI] Return empty result sets instead of 500 or 404 for analysis results (#72824) This changes the analysis results routes to return empty result sets with HTTP status code 200 instead of and inconsistent status codes 500 or 404. --- .../infra/server/lib/log_analysis/common.ts | 13 +--- .../infra/server/lib/log_analysis/errors.ts | 7 -- .../log_entry_categories_analysis.ts | 65 ++++++++----------- .../log_analysis/log_entry_rate_analysis.ts | 22 ++----- .../queries/log_entry_data_sets.ts | 2 +- .../log_analysis/queries/log_entry_rate.ts | 2 +- .../queries/top_log_entry_categories.ts | 2 +- .../results/log_entry_anomalies_datasets.ts | 9 +-- .../results/log_entry_categories.ts | 9 +-- .../results/log_entry_category_datasets.ts | 9 +-- .../results/log_entry_category_examples.ts | 9 +-- .../results/log_entry_examples.ts | 6 +- .../log_analysis/results/log_entry_rate.ts | 6 +- 13 files changed, 44 insertions(+), 117 deletions(-) diff --git a/x-pack/plugins/infra/server/lib/log_analysis/common.ts b/x-pack/plugins/infra/server/lib/log_analysis/common.ts index 218281d875a46..4d2be94c7cd62 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/common.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/common.ts @@ -14,7 +14,6 @@ import { logEntryDatasetsResponseRT, } from './queries/log_entry_data_sets'; import { decodeOrThrow } from '../../../common/runtime_types'; -import { NoLogAnalysisResultsIndexError } from './errors'; import { startTracingSpan, TracingSpan } from '../../../common/performance_tracing'; export async function fetchMlJob(mlAnomalyDetectors: MlAnomalyDetectors, jobId: string) { @@ -67,16 +66,8 @@ export async function getLogEntryDatasets( ) ); - if (logEntryDatasetsResponse._shards.total === 0) { - throw new NoLogAnalysisResultsIndexError( - `Failed to find ml indices for jobs: ${jobIds.join(', ')}.` - ); - } - - const { - after_key: afterKey, - buckets: latestBatchBuckets, - } = logEntryDatasetsResponse.aggregations.dataset_buckets; + const { after_key: afterKey, buckets: latestBatchBuckets = [] } = + logEntryDatasetsResponse.aggregations?.dataset_buckets ?? {}; logEntryDatasetBuckets = [...logEntryDatasetBuckets, ...latestBatchBuckets]; afterLatestBatchKey = afterKey; diff --git a/x-pack/plugins/infra/server/lib/log_analysis/errors.ts b/x-pack/plugins/infra/server/lib/log_analysis/errors.ts index 09fee8844fbc5..a6d0db25084e8 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/errors.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/errors.ts @@ -6,13 +6,6 @@ /* eslint-disable max-classes-per-file */ -export class NoLogAnalysisResultsIndexError extends Error { - constructor(message?: string) { - super(message); - Object.setPrototypeOf(this, new.target.prototype); - } -} - export class NoLogAnalysisMlJobError extends Error { constructor(message?: string) { super(message); diff --git a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts index a455a03d936a5..ff9e3c7d2167c 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts @@ -15,11 +15,7 @@ import { import { startTracingSpan } from '../../../common/performance_tracing'; import { decodeOrThrow } from '../../../common/runtime_types'; import type { MlAnomalyDetectors, MlSystem } from '../../types'; -import { - InsufficientLogAnalysisMlJobConfigurationError, - NoLogAnalysisResultsIndexError, - UnknownCategoryError, -} from './errors'; +import { InsufficientLogAnalysisMlJobConfigurationError, UnknownCategoryError } from './errors'; import { createLogEntryCategoriesQuery, logEntryCategoriesResponseRT, @@ -235,38 +231,33 @@ async function fetchTopLogEntryCategories( const esSearchSpan = finalizeEsSearchSpan(); - if (topLogEntryCategoriesResponse._shards.total === 0) { - throw new NoLogAnalysisResultsIndexError( - `Failed to find ml result index for job ${logEntryCategoriesCountJobId}.` - ); - } - - const topLogEntryCategories = topLogEntryCategoriesResponse.aggregations.terms_category_id.buckets.map( - (topCategoryBucket) => { - const maximumAnomalyScoresByDataset = topCategoryBucket.filter_record.terms_dataset.buckets.reduce< - Record - >( - (accumulatedMaximumAnomalyScores, datasetFromRecord) => ({ - ...accumulatedMaximumAnomalyScores, - [datasetFromRecord.key]: datasetFromRecord.maximum_record_score.value ?? 0, - }), - {} - ); - - return { - categoryId: parseCategoryId(topCategoryBucket.key), - logEntryCount: topCategoryBucket.filter_model_plot.sum_actual.value ?? 0, - datasets: topCategoryBucket.filter_model_plot.terms_dataset.buckets - .map((datasetBucket) => ({ - name: datasetBucket.key, - maximumAnomalyScore: maximumAnomalyScoresByDataset[datasetBucket.key] ?? 0, - })) - .sort(compareDatasetsByMaximumAnomalyScore) - .reverse(), - maximumAnomalyScore: topCategoryBucket.filter_record.maximum_record_score.value ?? 0, - }; - } - ); + const topLogEntryCategories = + topLogEntryCategoriesResponse.aggregations?.terms_category_id.buckets.map( + (topCategoryBucket) => { + const maximumAnomalyScoresByDataset = topCategoryBucket.filter_record.terms_dataset.buckets.reduce< + Record + >( + (accumulatedMaximumAnomalyScores, datasetFromRecord) => ({ + ...accumulatedMaximumAnomalyScores, + [datasetFromRecord.key]: datasetFromRecord.maximum_record_score.value ?? 0, + }), + {} + ); + + return { + categoryId: parseCategoryId(topCategoryBucket.key), + logEntryCount: topCategoryBucket.filter_model_plot.sum_actual.value ?? 0, + datasets: topCategoryBucket.filter_model_plot.terms_dataset.buckets + .map((datasetBucket) => ({ + name: datasetBucket.key, + maximumAnomalyScore: maximumAnomalyScoresByDataset[datasetBucket.key] ?? 0, + })) + .sort(compareDatasetsByMaximumAnomalyScore) + .reverse(), + maximumAnomalyScore: topCategoryBucket.filter_record.maximum_record_score.value ?? 0, + }; + } + ) ?? []; return { topLogEntryCategories, diff --git a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_rate_analysis.ts b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_rate_analysis.ts index 7bfc85ba78a0e..ce3acd0dba8cf 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_rate_analysis.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_rate_analysis.ts @@ -4,10 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pipe } from 'fp-ts/lib/pipeable'; -import { map, fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; -import { throwErrors, createPlainError } from '../../../common/runtime_types'; +import { decodeOrThrow } from '../../../common/runtime_types'; import { logRateModelPlotResponseRT, createLogEntryRateQuery, @@ -15,7 +12,6 @@ import { CompositeTimestampPartitionKey, } from './queries'; import { getJobId } from '../../../common/log_analysis'; -import { NoLogAnalysisResultsIndexError } from './errors'; import type { MlSystem } from '../../types'; const COMPOSITE_AGGREGATION_BATCH_SIZE = 1000; @@ -50,22 +46,14 @@ export async function getLogEntryRateBuckets( ) ); - if (mlModelPlotResponse._shards.total === 0) { - throw new NoLogAnalysisResultsIndexError( - `Failed to query ml result index for job ${logRateJobId}.` - ); - } - - const { after_key: afterKey, buckets: latestBatchBuckets } = pipe( - logRateModelPlotResponseRT.decode(mlModelPlotResponse), - map((response) => response.aggregations.timestamp_partition_buckets), - fold(throwErrors(createPlainError), identity) - ); + const { after_key: afterKey, buckets: latestBatchBuckets = [] } = + decodeOrThrow(logRateModelPlotResponseRT)(mlModelPlotResponse).aggregations + ?.timestamp_partition_buckets ?? {}; mlModelPlotBuckets = [...mlModelPlotBuckets, ...latestBatchBuckets]; afterLatestBatchKey = afterKey; - if (latestBatchBuckets.length < COMPOSITE_AGGREGATION_BATCH_SIZE) { + if (afterKey == null || latestBatchBuckets.length < COMPOSITE_AGGREGATION_BATCH_SIZE) { break; } } diff --git a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_data_sets.ts b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_data_sets.ts index 7627ccd8c4996..53971a91d86b1 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_data_sets.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_data_sets.ts @@ -67,7 +67,7 @@ export type LogEntryDatasetBucket = rt.TypeOf; export const logEntryDatasetsResponseRT = rt.intersection([ commonSearchSuccessResponseFieldsRT, - rt.type({ + rt.partial({ aggregations: rt.type({ dataset_buckets: rt.intersection([ rt.type({ diff --git a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_rate.ts b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_rate.ts index 52edcf09cdfc2..e82dd8ef4443c 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_rate.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_rate.ts @@ -162,7 +162,7 @@ export const logRateModelPlotBucketRT = rt.type({ export type LogRateModelPlotBucket = rt.TypeOf; -export const logRateModelPlotResponseRT = rt.type({ +export const logRateModelPlotResponseRT = rt.partial({ aggregations: rt.type({ timestamp_partition_buckets: rt.intersection([ rt.type({ diff --git a/x-pack/plugins/infra/server/lib/log_analysis/queries/top_log_entry_categories.ts b/x-pack/plugins/infra/server/lib/log_analysis/queries/top_log_entry_categories.ts index 355dde9ec7c4a..5d3d9bc8b4036 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/queries/top_log_entry_categories.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/queries/top_log_entry_categories.ts @@ -159,7 +159,7 @@ export type LogEntryCategoryBucket = rt.TypeOf; export const topLogEntryCategoriesResponseRT = rt.intersection([ commonSearchSuccessResponseFieldsRT, - rt.type({ + rt.partial({ aggregations: rt.type({ terms_category_id: rt.type({ buckets: rt.array(logEntryCategoryBucketRT), diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_anomalies_datasets.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_anomalies_datasets.ts index d3d0862eee9aa..f1f1a1681a901 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_anomalies_datasets.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_anomalies_datasets.ts @@ -12,10 +12,7 @@ import { } from '../../../../common/http_api/log_analysis'; import { createValidationFunction } from '../../../../common/runtime_types'; import type { InfraBackendLibs } from '../../../lib/infra_types'; -import { - getLogEntryAnomaliesDatasets, - NoLogAnalysisResultsIndexError, -} from '../../../lib/log_analysis'; +import { getLogEntryAnomaliesDatasets } from '../../../lib/log_analysis'; import { assertHasInfraMlPlugins } from '../../../utils/request_context'; export const initGetLogEntryAnomaliesDatasetsRoute = ({ framework }: InfraBackendLibs) => { @@ -58,10 +55,6 @@ export const initGetLogEntryAnomaliesDatasetsRoute = ({ framework }: InfraBacken throw error; } - if (error instanceof NoLogAnalysisResultsIndexError) { - return response.notFound({ body: { message: error.message } }); - } - return response.customError({ statusCode: error.statusCode ?? 500, body: { diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_categories.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_categories.ts index f9f31f28dffeb..f57132ef1b505 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_categories.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_categories.ts @@ -12,10 +12,7 @@ import { } from '../../../../common/http_api/log_analysis'; import { createValidationFunction } from '../../../../common/runtime_types'; import type { InfraBackendLibs } from '../../../lib/infra_types'; -import { - getTopLogEntryCategories, - NoLogAnalysisResultsIndexError, -} from '../../../lib/log_analysis'; +import { getTopLogEntryCategories } from '../../../lib/log_analysis'; import { assertHasInfraMlPlugins } from '../../../utils/request_context'; export const initGetLogEntryCategoriesRoute = ({ framework }: InfraBackendLibs) => { @@ -69,10 +66,6 @@ export const initGetLogEntryCategoriesRoute = ({ framework }: InfraBackendLibs) throw error; } - if (error instanceof NoLogAnalysisResultsIndexError) { - return response.notFound({ body: { message: error.message } }); - } - return response.customError({ statusCode: error.statusCode ?? 500, body: { diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_datasets.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_datasets.ts index 69b1e942464fd..b99ff920f81e4 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_datasets.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_datasets.ts @@ -12,10 +12,7 @@ import { } from '../../../../common/http_api/log_analysis'; import { createValidationFunction } from '../../../../common/runtime_types'; import type { InfraBackendLibs } from '../../../lib/infra_types'; -import { - getLogEntryCategoryDatasets, - NoLogAnalysisResultsIndexError, -} from '../../../lib/log_analysis'; +import { getLogEntryCategoryDatasets } from '../../../lib/log_analysis'; import { assertHasInfraMlPlugins } from '../../../utils/request_context'; export const initGetLogEntryCategoryDatasetsRoute = ({ framework }: InfraBackendLibs) => { @@ -58,10 +55,6 @@ export const initGetLogEntryCategoryDatasetsRoute = ({ framework }: InfraBackend throw error; } - if (error instanceof NoLogAnalysisResultsIndexError) { - return response.notFound({ body: { message: error.message } }); - } - return response.customError({ statusCode: error.statusCode ?? 500, body: { diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_examples.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_examples.ts index 8baeaac3d1699..11098ebe5c65b 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_examples.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_examples.ts @@ -12,10 +12,7 @@ import { } from '../../../../common/http_api/log_analysis'; import { createValidationFunction } from '../../../../common/runtime_types'; import type { InfraBackendLibs } from '../../../lib/infra_types'; -import { - getLogEntryCategoryExamples, - NoLogAnalysisResultsIndexError, -} from '../../../lib/log_analysis'; +import { getLogEntryCategoryExamples } from '../../../lib/log_analysis'; import { assertHasInfraMlPlugins } from '../../../utils/request_context'; export const initGetLogEntryCategoryExamplesRoute = ({ framework, sources }: InfraBackendLibs) => { @@ -68,10 +65,6 @@ export const initGetLogEntryCategoryExamplesRoute = ({ framework, sources }: Inf throw error; } - if (error instanceof NoLogAnalysisResultsIndexError) { - return response.notFound({ body: { message: error.message } }); - } - return response.customError({ statusCode: error.statusCode ?? 500, body: { diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_examples.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_examples.ts index be4caee769506..7838a64a6045e 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_examples.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_examples.ts @@ -7,7 +7,7 @@ import Boom from 'boom'; import { createValidationFunction } from '../../../../common/runtime_types'; import { InfraBackendLibs } from '../../../lib/infra_types'; -import { NoLogAnalysisResultsIndexError, getLogEntryExamples } from '../../../lib/log_analysis'; +import { getLogEntryExamples } from '../../../lib/log_analysis'; import { assertHasInfraMlPlugins } from '../../../utils/request_context'; import { getLogEntryExamplesRequestPayloadRT, @@ -68,10 +68,6 @@ export const initGetLogEntryExamplesRoute = ({ framework, sources }: InfraBacken throw error; } - if (error instanceof NoLogAnalysisResultsIndexError) { - return response.notFound({ body: { message: error.message } }); - } - return response.customError({ statusCode: error.statusCode ?? 500, body: { diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts index 3b05f6ed23aae..cd23c0193e291 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts @@ -13,7 +13,7 @@ import { GetLogEntryRateSuccessResponsePayload, } from '../../../../common/http_api/log_analysis'; import { createValidationFunction } from '../../../../common/runtime_types'; -import { NoLogAnalysisResultsIndexError, getLogEntryRateBuckets } from '../../../lib/log_analysis'; +import { getLogEntryRateBuckets } from '../../../lib/log_analysis'; import { assertHasInfraMlPlugins } from '../../../utils/request_context'; export const initGetLogEntryRateRoute = ({ framework }: InfraBackendLibs) => { @@ -56,10 +56,6 @@ export const initGetLogEntryRateRoute = ({ framework }: InfraBackendLibs) => { throw error; } - if (error instanceof NoLogAnalysisResultsIndexError) { - return response.notFound({ body: { message: error.message } }); - } - return response.customError({ statusCode: error.statusCode ?? 500, body: { From 02e3fca77258b166b20dbbf2dc280e146b59ec27 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Mon, 27 Jul 2020 16:01:03 +0100 Subject: [PATCH 37/96] fix icon type (#73254) --- .../components/timeline/header/index.test.tsx | 95 +++++++++++++++++++ .../components/timeline/header/index.tsx | 2 +- 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx index 58c213dc884ea..e0043f3b232da 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx @@ -85,5 +85,100 @@ describe('Header', () => { expect(wrapper.find('[data-test-subj="timelineCallOutUnauthorized"]').exists()).toEqual(true); }); + + test('it renders the unauthorized call out with correct icon', () => { + const testProps = { + ...props, + filterManager: new FilterManager(mockUiSettingsForFilterManager), + showCallOutUnauthorizedMsg: true, + }; + + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="timelineCallOutUnauthorized"]').first().prop('iconType') + ).toEqual('alert'); + }); + + test('it renders the unauthorized call out with correct message', () => { + const testProps = { + ...props, + filterManager: new FilterManager(mockUiSettingsForFilterManager), + showCallOutUnauthorizedMsg: true, + }; + + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="timelineCallOutUnauthorized"]').first().prop('title') + ).toEqual( + 'You can use Timeline to investigate events, but you do not have the required permissions to save timelines for future use. If you need to save timelines, contact your Kibana administrator.' + ); + }); + + test('it renders the immutable timeline call out providers', () => { + const testProps = { + ...props, + filterManager: new FilterManager(mockUiSettingsForFilterManager), + showCallOutUnauthorizedMsg: false, + status: TimelineStatus.immutable, + }; + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="timelineImmutableCallOut"]').exists()).toEqual(true); + }); + + test('it renders the immutable timeline call out with correct icon', () => { + const testProps = { + ...props, + filterManager: new FilterManager(mockUiSettingsForFilterManager), + showCallOutUnauthorizedMsg: false, + status: TimelineStatus.immutable, + }; + + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="timelineImmutableCallOut"]').first().prop('iconType') + ).toEqual('alert'); + }); + + test('it renders the immutable timeline call out with correct message', () => { + const testProps = { + ...props, + filterManager: new FilterManager(mockUiSettingsForFilterManager), + showCallOutUnauthorizedMsg: false, + status: TimelineStatus.immutable, + }; + + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="timelineImmutableCallOut"]').first().prop('title') + ).toEqual( + 'This timeline is immutable, therefore not allowed to save it within the security application, though you may continue to use the timeline to search and filter security events' + ); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx index aa3ce88acc200..75bfb52f2756b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx @@ -75,7 +75,7 @@ const TimelineHeaderComponent: React.FC = ({ data-test-subj="timelineImmutableCallOut" title={i18n.CALL_OUT_IMMUTIABLE} color="primary" - iconType="info" + iconType="alert" size="s" /> )} From 9aa5e1772da99fb90f248e9991591fda27160edc Mon Sep 17 00:00:00 2001 From: igoristic Date: Mon, 27 Jul 2020 11:10:25 -0400 Subject: [PATCH 38/96] [Monitoring] "Internal Monitoring" deprecation warning (#72020) * Internal Monitoring deprecation * Fixed type * Added if cloud logic * Fixed i18n test * Addressed code review feedback * Fixed types * Changed query * Added delay to fix a test * Fixed tests * Fixed text Co-authored-by: Elastic Machine --- .../public/lib/internal_monitoring_toasts.tsx | 123 ++++++++++++++++++ .../monitoring/public/services/clusters.js | 34 ++++- .../check/internal_monitoring.ts | 85 ++++++++++++ .../api/v1/elasticsearch_settings/index.js | 1 + .../monitoring/server/routes/api/v1/ui.js | 1 + .../functional/apps/monitoring/time_filter.js | 7 + 6 files changed, 245 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/monitoring/public/lib/internal_monitoring_toasts.tsx create mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts diff --git a/x-pack/plugins/monitoring/public/lib/internal_monitoring_toasts.tsx b/x-pack/plugins/monitoring/public/lib/internal_monitoring_toasts.tsx new file mode 100644 index 0000000000000..b6ecb631d005a --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/internal_monitoring_toasts.tsx @@ -0,0 +1,123 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSpacer, EuiLink } from '@elastic/eui'; +import { Legacy } from '../legacy_shims'; +import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; +import { isInSetupMode, toggleSetupMode } from './setup_mode'; + +export interface MonitoringIndicesTypes { + legacyIndices: number; + metricbeatIndices: number; +} + +const enterSetupModeLabel = () => + i18n.translate('xpack.monitoring.internalMonitoringToast.enterSetupMode', { + defaultMessage: 'Enter setup mode', + }); + +const learnMoreLabel = () => + i18n.translate('xpack.monitoring.internalMonitoringToast.learnMoreAction', { + defaultMessage: 'Learn more', + }); + +const showIfLegacyOnlyIndices = () => { + const { ELASTIC_WEBSITE_URL } = Legacy.shims.docLinks; + const toast = Legacy.shims.toastNotifications.addWarning({ + title: toMountPoint( + + ), + text: toMountPoint( +
+

+ {i18n.translate('xpack.monitoring.internalMonitoringToast.description', { + defaultMessage: `It appears you are using "Legacy Collection" for Stack Monitoring. + This method of monitoring will no longer be supported in the next major release (8.0.0). + Please follow the steps in setup mode to start monitoring with Metricbeat.`, + })} +

+ { + Legacy.shims.toastNotifications.remove(toast); + toggleSetupMode(true); + }} + > + {enterSetupModeLabel()} + + + + + {learnMoreLabel()} + +
+ ), + }); +}; + +const showIfLegacyAndMetricbeatIndices = () => { + const { ELASTIC_WEBSITE_URL } = Legacy.shims.docLinks; + const toast = Legacy.shims.toastNotifications.addWarning({ + title: toMountPoint( + + ), + text: toMountPoint( +
+

+ {i18n.translate('xpack.monitoring.internalAndMetricbeatMonitoringToast.description', { + defaultMessage: `It appears you are using both Metricbeat and "Legacy Collection" for Stack Monitoring. + In 8.0.0, you must use Metricbeat to collect monitoring data. + Please follow the steps in setup mode to migrate the rest of the monitoring to Metricbeat.`, + })} +

+ { + Legacy.shims.toastNotifications.remove(toast); + toggleSetupMode(true); + }} + > + {enterSetupModeLabel()} + + + + + {learnMoreLabel()} + +
+ ), + }); +}; + +export const showInternalMonitoringToast = ({ + legacyIndices, + metricbeatIndices, +}: MonitoringIndicesTypes) => { + if (isInSetupMode()) { + return; + } + + if (legacyIndices && !metricbeatIndices) { + showIfLegacyOnlyIndices(); + } else if (legacyIndices && metricbeatIndices) { + showIfLegacyAndMetricbeatIndices(); + } +}; diff --git a/x-pack/plugins/monitoring/public/services/clusters.js b/x-pack/plugins/monitoring/public/services/clusters.js index 5173984dbe868..7f772ac1e1bcd 100644 --- a/x-pack/plugins/monitoring/public/services/clusters.js +++ b/x-pack/plugins/monitoring/public/services/clusters.js @@ -7,6 +7,7 @@ import { ajaxErrorHandlersProvider } from '../lib/ajax_error_handler'; import { Legacy } from '../legacy_shims'; import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../common/constants'; +import { showInternalMonitoringToast } from '../lib/internal_monitoring_toasts'; import { showSecurityToast } from '../alerts/lib/security_toasts'; function formatClusters(clusters) { @@ -21,6 +22,7 @@ function formatCluster(cluster) { } let once = false; +let inTransit = false; export function monitoringClustersProvider($injector) { return (clusterUuid, ccs, codePaths) => { @@ -63,19 +65,39 @@ export function monitoringClustersProvider($injector) { }); } - if (!once) { + function ensureMetricbeatEnabled() { + if (Legacy.shims.isCloud) { + return Promise.resolve(); + } + + return $http + .get('../api/monitoring/v1/elasticsearch_settings/check/internal_monitoring') + .then(({ data }) => { + showInternalMonitoringToast({ + legacyIndices: data.legacy_indices, + metricbeatIndices: data.mb_indices, + }); + }) + .catch((err) => { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); + } + + if (!once && !inTransit) { + inTransit = true; return getClusters().then((clusters) => { if (clusters.length) { - return ensureAlertsEnabled() - .then(({ data }) => { + Promise.all([ensureAlertsEnabled(), ensureMetricbeatEnabled()]) + .then(([{ data }]) => { showSecurityToast(data); once = true; - return clusters; }) .catch(() => { // Intentionally swallow the error as this will retry the next page load - return clusters; - }); + }) + .finally(() => (inTransit = false)); } return clusters; }); diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts new file mode 100644 index 0000000000000..4473d824c9e30 --- /dev/null +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RequestHandlerContext } from 'kibana/server'; +// @ts-ignore +import { getIndexPatterns } from '../../../../../lib/cluster/get_index_patterns'; +// @ts-ignore +import { handleError } from '../../../../../lib/errors'; +import { RouteDependencies } from '../../../../../types'; + +const queryBody = { + size: 0, + aggs: { + types: { + terms: { + field: '_index', + size: 10, + }, + }, + }, +}; + +const checkLatestMonitoringIsLegacy = async (context: RequestHandlerContext, index: string) => { + const { client: esClient } = context.core.elasticsearch.legacy; + const result = await esClient.callAsCurrentUser('search', { + index, + body: queryBody, + }); + + const { aggregations } = result; + const counts = { + legacyIndicesCount: 0, + mbIndicesCount: 0, + }; + + if (!aggregations) { + return counts; + } + + const { + types: { buckets }, + } = aggregations; + counts.mbIndicesCount = buckets.filter(({ key }: { key: string }) => key.includes('-mb-')).length; + + counts.legacyIndicesCount = buckets.length - counts.mbIndicesCount; + return counts; +}; + +export function internalMonitoringCheckRoute(server: unknown, npRoute: RouteDependencies) { + npRoute.router.get( + { + path: '/api/monitoring/v1/elasticsearch_settings/check/internal_monitoring', + validate: false, + }, + async (context, _request, response) => { + try { + const typeCount = { + legacy_indices: 0, + mb_indices: 0, + }; + + const { esIndexPattern, kbnIndexPattern, lsIndexPattern } = getIndexPatterns(server); + const indexCounts = await Promise.all([ + checkLatestMonitoringIsLegacy(context, esIndexPattern), + checkLatestMonitoringIsLegacy(context, kbnIndexPattern), + checkLatestMonitoringIsLegacy(context, lsIndexPattern), + ]); + + indexCounts.forEach((counts) => { + typeCount.legacy_indices += counts.legacyIndicesCount; + typeCount.mb_indices += counts.mbIndicesCount; + }); + + return response.ok({ + body: typeCount, + }); + } catch (err) { + throw handleError(err); + } + } + ); +} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/index.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/index.js index d7ef71efc0b51..906057d221868 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/index.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/index.js @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +export { internalMonitoringCheckRoute } from './check/internal_monitoring'; export { clusterSettingsCheckRoute } from './check/cluster'; export { nodesSettingsCheckRoute } from './check/nodes'; export { setCollectionEnabledRoute } from './set/collection_enabled'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/ui.js b/x-pack/plugins/monitoring/server/routes/api/v1/ui.js index de0213ec84689..e8daf52582437 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/ui.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/ui.js @@ -20,6 +20,7 @@ export { ccrShardRoute, } from './elasticsearch'; export { + internalMonitoringCheckRoute, clusterSettingsCheckRoute, nodesSettingsCheckRoute, setCollectionEnabledRoute, diff --git a/x-pack/test/functional/apps/monitoring/time_filter.js b/x-pack/test/functional/apps/monitoring/time_filter.js index d7ffdb4a7900d..11557d995218e 100644 --- a/x-pack/test/functional/apps/monitoring/time_filter.js +++ b/x-pack/test/functional/apps/monitoring/time_filter.js @@ -7,6 +7,8 @@ import expect from '@kbn/expect'; import { getLifecycleMethods } from './_get_lifecycle_methods'; +const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['header', 'timePicker']); const testSubjects = getService('testSubjects'); @@ -35,6 +37,11 @@ export default function ({ getService, getPageObjects }) { }); it('should send another request when changing the time picker', async () => { + /** + * TODO: The value should either be removed or lowered after: + * https://github.com/elastic/kibana/issues/72997 is resolved + */ + await delay(3000); await PageObjects.timePicker.setAbsoluteRange( 'Aug 15, 2016 @ 21:00:00.000', 'Aug 16, 2016 @ 00:00:00.000' From 6d4bb9dc0d5bf8bbcdff17f73b4c77b0f1ccea35 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Mon, 27 Jul 2020 11:13:26 -0400 Subject: [PATCH 39/96] [SECURITY_SOLUTION][ENDPOINT] Handle Host list/details policy links to non-existing policies (#73208) * Make API call to check policies and save it to store * change policy list and details to not show policy as a link if it does not exist --- .../pages/endpoint_hosts/store/action.ts | 9 +- .../pages/endpoint_hosts/store/index.test.ts | 1 + .../pages/endpoint_hosts/store/middleware.ts | 115 +++++++++++++++++- .../pages/endpoint_hosts/store/reducer.ts | 9 ++ .../pages/endpoint_hosts/store/selectors.ts | 8 ++ .../management/pages/endpoint_hosts/types.ts | 2 + .../view/components/host_policy_link.tsx | 53 ++++++++ .../view/details/host_details.tsx | 29 +---- .../pages/endpoint_hosts/view/index.tsx | 18 +-- .../store/policy_list/services/ingest.ts | 15 +++ 10 files changed, 224 insertions(+), 35 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/host_policy_link.tsx 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 4c01b3644cf63..621fab2e4ee11 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 @@ -12,6 +12,7 @@ import { import { ServerApiError } from '../../../../common/types'; import { GetPolicyListResponse } from '../../policy/types'; import { GetPackagesResponse } from '../../../../../../ingest_manager/common'; +import { HostState } from '../types'; interface ServerReturnedHostList { type: 'serverReturnedHostList'; @@ -75,6 +76,11 @@ interface ServerReturnedEndpointPackageInfo { payload: GetPackagesResponse['response'][0]; } +interface ServerReturnedHostNonExistingPolicies { + type: 'serverReturnedHostNonExistingPolicies'; + payload: HostState['nonExistingPolicies']; +} + export type HostAction = | ServerReturnedHostList | ServerFailedToReturnHostList @@ -87,4 +93,5 @@ export type HostAction = | UserSelectedEndpointPolicy | ServerCancelledHostListLoading | ServerCancelledPolicyItemsLoading - | ServerReturnedEndpointPackageInfo; + | ServerReturnedEndpointPackageInfo + | ServerReturnedHostNonExistingPolicies; 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 f2c205661b32c..b6e18506b6111 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 @@ -50,6 +50,7 @@ describe('HostList store concerns', () => { selectedPolicyId: undefined, policyItemsLoading: false, endpointPackageInfo: undefined, + nonExistingPolicies: {}, }); }); 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 12fa3dc47beac..edeca5659ee38 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 @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostResultList } from '../../../../../common/endpoint/types'; +import { HttpSetup } from 'kibana/public'; +import { HostInfo, HostResultList } from '../../../../../common/endpoint/types'; import { GetPolicyListResponse } from '../../policy/types'; import { ImmutableMiddlewareFactory } from '../../../../common/store'; import { @@ -13,12 +14,15 @@ import { uiQueryParams, listData, endpointPackageInfo, + nonExistingPolicies, } from './selectors'; import { HostState } from '../types'; import { sendGetEndpointSpecificPackageConfigs, sendGetEndpointSecurityPackage, + sendGetAgentConfigList, } from '../../policy/store/policy_list/services/ingest'; +import { AGENT_CONFIG_SAVED_OBJECT_TYPE } from '../../../../../../ingest_manager/common'; export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (coreStart) => { return ({ getState, dispatch }) => (next) => async (action) => { @@ -58,6 +62,23 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor type: 'serverReturnedHostList', payload: hostResponse, }); + + getNonExistingPoliciesForHostsList( + coreStart.http, + hostResponse.hosts, + nonExistingPolicies(state) + ) + .then((missingPolicies) => { + if (missingPolicies !== undefined) { + dispatch({ + type: 'serverReturnedHostNonExistingPolicies', + payload: missingPolicies, + }); + } + }) + // Ignore Errors, since this should not hinder the user's ability to use the UI + // eslint-disable-next-line no-console + .catch((error) => console.error(error)); } catch (error) { dispatch({ type: 'serverFailedToReturnHostList', @@ -117,6 +138,23 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor type: 'serverReturnedHostList', payload: response, }); + + getNonExistingPoliciesForHostsList( + coreStart.http, + response.hosts, + nonExistingPolicies(state) + ) + .then((missingPolicies) => { + if (missingPolicies !== undefined) { + dispatch({ + type: 'serverReturnedHostNonExistingPolicies', + payload: missingPolicies, + }); + } + }) + // Ignore Errors, since this should not hinder the user's ability to use the UI + // eslint-disable-next-line no-console + .catch((error) => console.error(error)); } catch (error) { dispatch({ type: 'serverFailedToReturnHostList', @@ -133,11 +171,25 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor // call the host details api const { selected_host: selectedHost } = uiQueryParams(state); try { - const response = await coreStart.http.get(`/api/endpoint/metadata/${selectedHost}`); + const response = await coreStart.http.get( + `/api/endpoint/metadata/${selectedHost}` + ); dispatch({ type: 'serverReturnedHostDetails', payload: response, }); + getNonExistingPoliciesForHostsList(coreStart.http, [response], nonExistingPolicies(state)) + .then((missingPolicies) => { + if (missingPolicies !== undefined) { + dispatch({ + type: 'serverReturnedHostNonExistingPolicies', + payload: missingPolicies, + }); + } + }) + // Ignore Errors, since this should not hinder the user's ability to use the UI + // eslint-disable-next-line no-console + .catch((error) => console.error(error)); } catch (error) { dispatch({ type: 'serverFailedToReturnHostDetails', @@ -163,3 +215,62 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor } }; }; + +const getNonExistingPoliciesForHostsList = async ( + http: HttpSetup, + hosts: HostResultList['hosts'], + currentNonExistingPolicies: HostState['nonExistingPolicies'] +): Promise => { + if (hosts.length === 0) { + return; + } + + // Create an array of unique policy IDs that are not yet known to be non-existing. + const policyIdsToCheck = Array.from( + new Set( + hosts + .filter((host) => !currentNonExistingPolicies[host.metadata.Endpoint.policy.applied.id]) + .map((host) => host.metadata.Endpoint.policy.applied.id) + ) + ); + + if (policyIdsToCheck.length === 0) { + return; + } + + // We use the Agent Config API here, instead of the Package Config, because we can't use + // filter by ID of the Saved Object. Agent Config, however, keeps a reference (array) of + // Package Ids that it uses, thus if a reference exists there, then the package config (policy) + // exists. + const policiesFound = ( + await sendGetAgentConfigList(http, { + query: { + kuery: `${AGENT_CONFIG_SAVED_OBJECT_TYPE}.package_configs: (${policyIdsToCheck.join( + ' or ' + )})`, + }, + }) + ).items.reduce((list, agentConfig) => { + (agentConfig.package_configs as string[]).forEach((packageConfig) => { + list[packageConfig as string] = true; + }); + return list; + }, {}); + + const nonExisting = policyIdsToCheck.reduce( + (list, policyId) => { + if (policiesFound[policyId]) { + return list; + } + list[policyId] = true; + return list; + }, + {} + ); + + if (Object.keys(nonExisting).length === 0) { + return; + } + + return nonExisting; +}; 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 993267cf1a704..7f68baa4b85bd 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 @@ -28,6 +28,7 @@ export const initialHostListState: Immutable = { selectedPolicyId: undefined, policyItemsLoading: false, endpointPackageInfo: undefined, + nonExistingPolicies: {}, }; /* eslint-disable-next-line complexity */ @@ -57,6 +58,14 @@ export const hostListReducer: ImmutableReducer = ( error: action.payload, loading: false, }; + } else if (action.type === 'serverReturnedHostNonExistingPolicies') { + return { + ...state, + nonExistingPolicies: { + ...state.nonExistingPolicies, + ...action.payload, + }, + }; } else if (action.type === 'serverReturnedHostDetails') { return { ...state, 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 4f47eaf565d8c..6e0823a920413 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 @@ -195,3 +195,11 @@ export const policyResponseStatus: (state: Immutable) => string = cre return (policyResponse && policyResponse?.Endpoint?.policy?.applied?.status) || ''; } ); + +/** + * returns the list of known non-existing polices that may have been in the Host API response. + * @param state + */ +export const nonExistingPolicies: ( + state: Immutable +) => Immutable = (state) => state.nonExistingPolicies; 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 a5f37a0b49e8f..582a59cfd7605 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 @@ -50,6 +50,8 @@ export interface HostState { 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; } /** diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/host_policy_link.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/host_policy_link.tsx new file mode 100644 index 0000000000000..ec4d7e87b721d --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/host_policy_link.tsx @@ -0,0 +1,53 @@ +/* + * 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, { memo, useMemo } from 'react'; +import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui'; +import { useHostSelector } from '../hooks'; +import { nonExistingPolicies } from '../../store/selectors'; +import { getPolicyDetailPath } from '../../../../common/routing'; +import { useFormatUrl } from '../../../../../common/components/link_to'; +import { SecurityPageName } from '../../../../../../common/constants'; +import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; + +/** + * A policy link (to details) that first checks to see if the policy id exists against + * the `nonExistingPolicies` value in the store. If it does not exist, then regular + * text is returned. + */ +export const HostPolicyLink = memo< + Omit & { + policyId: string; + } +>(({ policyId, children, onClick, ...otherProps }) => { + const missingPolicies = useHostSelector(nonExistingPolicies); + const { formatUrl } = useFormatUrl(SecurityPageName.administration); + const { toRoutePath, toRouteUrl } = useMemo(() => { + const toPath = getPolicyDetailPath(policyId); + return { + toRoutePath: toPath, + toRouteUrl: formatUrl(toPath), + }; + }, [formatUrl, policyId]); + const clickHandler = useNavigateByRouterEventHandler(toRoutePath, onClick); + + if (missingPolicies[policyId]) { + return ( + + {children} + + ); + } + + return ( + // eslint-disable-next-line @elastic/eui/href-or-on-click + + {children} + + ); +}); + +HostPolicyLink.displayName = 'HostPolicyLink'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx index 62efa621e6e3b..cea66acbef8ca 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx @@ -26,10 +26,11 @@ import { POLICY_STATUS_TO_HEALTH_COLOR } from '../host_constants'; import { FormattedDateAndTime } from '../../../../../common/components/endpoint/formatted_date_time'; import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; import { LinkToApp } from '../../../../../common/components/endpoint/link_to_app'; -import { getHostDetailsPath, getPolicyDetailPath } from '../../../../common/routing'; +import { getHostDetailsPath } from '../../../../common/routing'; import { SecurityPageName } from '../../../../../app/types'; import { useFormatUrl } from '../../../../../common/components/link_to'; import { AgentDetailsReassignConfigAction } from '../../../../../../../ingest_manager/public'; +import { HostPolicyLink } from '../components/host_policy_link'; const HostIds = styled(EuiListGroupItem)` margin-top: 0; @@ -116,15 +117,6 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseRoutePath); - const [policyDetailsRoutePath, policyDetailsRouteUrl] = useMemo(() => { - return [ - getPolicyDetailPath(details.Endpoint.policy.applied.id), - formatUrl(getPolicyDetailPath(details.Endpoint.policy.applied.id)), - ]; - }, [details.Endpoint.policy.applied.id, formatUrl]); - - const policyDetailsClickHandler = useNavigateByRouterEventHandler(policyDetailsRoutePath); - const detailsResultsPolicy = useMemo(() => { return [ { @@ -133,14 +125,12 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { }), description: ( <> - {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} - {details.Endpoint.policy.applied.name} - + ), }, @@ -171,14 +161,7 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { ), }, ]; - }, [ - details, - policyResponseUri, - policyStatus, - policyStatusClickHandler, - policyDetailsRouteUrl, - policyDetailsClickHandler, - ]); + }, [details, policyResponseUri, policyStatus, policyStatusClickHandler]); const detailsResultsLower = useMemo(() => { return [ { 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 c5ed71cba46d9..e38ef1bd5fe86 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 @@ -46,9 +46,10 @@ import { AgentConfigDetailsDeployAgentAction, } from '../../../../../../ingest_manager/public'; import { SecurityPageName } from '../../../../app/types'; -import { getHostListPath, getHostDetailsPath, getPolicyDetailPath } from '../../../common/routing'; +import { getHostListPath, getHostDetailsPath } from '../../../common/routing'; import { useFormatUrl } from '../../../../common/components/link_to'; import { HostAction } from '../store/action'; +import { HostPolicyLink } from './components/host_policy_link'; const HostListNavLink = memo<{ name: string; @@ -241,15 +242,14 @@ export const HostList = () => { truncateText: true, // eslint-disable-next-line react/display-name render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied']) => { - const toRoutePath = getPolicyDetailPath(policy.id); - const toRouteUrl = formatUrl(toRoutePath); return ( - + + {policy.name} + ); }, }, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts index 48b6bedf50fd8..c6e6146f4d5e4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts @@ -12,12 +12,15 @@ import { DeletePackageConfigsRequest, PACKAGE_CONFIG_SAVED_OBJECT_TYPE, GetPackagesResponse, + GetAgentConfigsRequest, + GetAgentConfigsResponse, } from '../../../../../../../../ingest_manager/common'; import { GetPolicyListResponse, GetPolicyResponse, UpdatePolicyResponse } from '../../../types'; import { NewPolicyData } from '../../../../../../../common/endpoint/types'; const INGEST_API_ROOT = `/api/ingest_manager`; export const INGEST_API_PACKAGE_CONFIGS = `${INGEST_API_ROOT}/package_configs`; +const INGEST_API_AGENT_CONFIGS = `${INGEST_API_ROOT}/agent_configs`; const INGEST_API_FLEET = `${INGEST_API_ROOT}/fleet`; const INGEST_API_FLEET_AGENT_STATUS = `${INGEST_API_FLEET}/agent-status`; export const INGEST_API_EPM_PACKAGES = `${INGEST_API_ROOT}/epm/packages`; @@ -75,6 +78,18 @@ export const sendDeletePackageConfig = ( }); }; +/** + * Retrieve a list of Agent Configurations + * @param http + * @param options + */ +export const sendGetAgentConfigList = ( + http: HttpStart, + options: HttpFetchOptions & GetAgentConfigsRequest +) => { + return http.get(INGEST_API_AGENT_CONFIGS, options); +}; + /** * Updates a package config * From b15a0a97f742a4b1c2cbd424c3f842860b4331c0 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 27 Jul 2020 10:35:18 -0500 Subject: [PATCH 40/96] [Security Solution][Detections] Adds ip_range and text types to value list upload form (#73109) * Adds two more types to the value lists form * Adds `ip_range` and `text` types * Replaces radio group with select * Add custom command for attaching a file to an input This will be used to excercise value list uploads. * Add some missing test subjects for our value lists modal * Add cypress test for value lists modal This exercises the happy path: opening the modal, uploading a list, and asserting that it subsequently appears in the table. Co-authored-by: Elastic Machine --- .../cypress/fixtures/value_list.txt | 6 +++ .../cypress/integration/value_lists.spec.ts | 43 ++++++++++++++++ .../cypress/screens/lists.ts | 11 ++++ .../cypress/support/commands.js | 19 +++++++ .../cypress/support/index.d.ts | 1 + .../security_solution/cypress/tasks/lists.ts | 36 +++++++++++++ .../value_lists_management_modal/form.tsx | 50 +++++++++---------- .../value_lists_management_modal/table.tsx | 1 + .../translations.ts | 14 ++++++ .../pages/detection_engine/rules/index.tsx | 1 + 10 files changed, 156 insertions(+), 26 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/fixtures/value_list.txt create mode 100644 x-pack/plugins/security_solution/cypress/integration/value_lists.spec.ts create mode 100644 x-pack/plugins/security_solution/cypress/screens/lists.ts create mode 100644 x-pack/plugins/security_solution/cypress/tasks/lists.ts diff --git a/x-pack/plugins/security_solution/cypress/fixtures/value_list.txt b/x-pack/plugins/security_solution/cypress/fixtures/value_list.txt new file mode 100644 index 0000000000000..2b40f036c62d2 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/fixtures/value_list.txt @@ -0,0 +1,6 @@ +these +are +keywords +for +a +list diff --git a/x-pack/plugins/security_solution/cypress/integration/value_lists.spec.ts b/x-pack/plugins/security_solution/cypress/integration/value_lists.spec.ts new file mode 100644 index 0000000000000..2804a8ac2ea8c --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/value_lists.spec.ts @@ -0,0 +1,43 @@ +/* + * 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 { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; +import { DETECTIONS_URL } from '../urls/navigation'; +import { + waitForAlertsPanelToBeLoaded, + waitForAlertsIndexToBeCreated, + goToManageAlertsDetectionRules, +} from '../tasks/alerts'; +import { + waitForListsIndexToBeCreated, + waitForValueListsModalToBeLoaded, + openValueListsModal, + selectValueListsFile, + uploadValueList, +} from '../tasks/lists'; +import { VALUE_LISTS_TABLE, VALUE_LISTS_ROW } from '../screens/lists'; + +describe('value lists', () => { + describe('management modal', () => { + it('creates a keyword list from an uploaded file', () => { + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); + waitForAlertsPanelToBeLoaded(); + waitForAlertsIndexToBeCreated(); + waitForListsIndexToBeCreated(); + goToManageAlertsDetectionRules(); + waitForValueListsModalToBeLoaded(); + openValueListsModal(); + selectValueListsFile(); + uploadValueList(); + + cy.get(VALUE_LISTS_TABLE) + .find(VALUE_LISTS_ROW) + .should(($row) => { + expect($row.text()).to.contain('value_list.txt'); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/screens/lists.ts b/x-pack/plugins/security_solution/cypress/screens/lists.ts new file mode 100644 index 0000000000000..35205a27e5a3c --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/screens/lists.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. + */ + +export const VALUE_LISTS_MODAL_ACTIVATOR = '[data-test-subj="open-value-lists-modal-button"]'; +export const VALUE_LISTS_TABLE = '[data-test-subj="value-lists-table"]'; +export const VALUE_LISTS_ROW = '.euiTableRow'; +export const VALUE_LIST_FILE_PICKER = '[data-test-subj="value-list-file-picker"]'; +export const VALUE_LIST_FILE_UPLOAD_BUTTON = '[data-test-subj="value-lists-form-import-action"]'; diff --git a/x-pack/plugins/security_solution/cypress/support/commands.js b/x-pack/plugins/security_solution/cypress/support/commands.js index 8b75f068a53da..789759643e319 100644 --- a/x-pack/plugins/security_solution/cypress/support/commands.js +++ b/x-pack/plugins/security_solution/cypress/support/commands.js @@ -39,3 +39,22 @@ Cypress.Commands.add('stubSecurityApi', function (dataFileName) { cy.fixture(dataFileName).as(`${dataFileName}JSON`); cy.route('POST', 'api/solutions/security/graphql', `@${dataFileName}JSON`); }); + +Cypress.Commands.add( + 'attachFile', + { + prevSubject: 'element', + }, + (input, fileName, fileType = 'text/plain') => { + cy.fixture(fileName) + .then((content) => Cypress.Blob.base64StringToBlob(content, fileType)) + .then((blob) => { + const testFile = new File([blob], fileName, { type: fileType }); + const dataTransfer = new DataTransfer(); + + dataTransfer.items.add(testFile); + input[0].files = dataTransfer.files; + return input; + }); + } +); diff --git a/x-pack/plugins/security_solution/cypress/support/index.d.ts b/x-pack/plugins/security_solution/cypress/support/index.d.ts index 12c11ffd27750..906e526e2c4a0 100644 --- a/x-pack/plugins/security_solution/cypress/support/index.d.ts +++ b/x-pack/plugins/security_solution/cypress/support/index.d.ts @@ -7,5 +7,6 @@ declare namespace Cypress { interface Chainable { stubSecurityApi(dataFileName: string): Chainable; + attachFile(fileName: string, fileType?: string): Chainable; } } diff --git a/x-pack/plugins/security_solution/cypress/tasks/lists.ts b/x-pack/plugins/security_solution/cypress/tasks/lists.ts new file mode 100644 index 0000000000000..638c69c087adf --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/tasks/lists.ts @@ -0,0 +1,36 @@ +/* + * 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 { + VALUE_LISTS_MODAL_ACTIVATOR, + VALUE_LIST_FILE_PICKER, + VALUE_LIST_FILE_UPLOAD_BUTTON, +} from '../screens/lists'; + +export const waitForListsIndexToBeCreated = () => { + cy.request({ url: '/api/lists/index', retryOnStatusCodeFailure: true }).then((response) => { + if (response.status !== 200) { + cy.wait(7500); + } + }); +}; + +export const waitForValueListsModalToBeLoaded = () => { + cy.get(VALUE_LISTS_MODAL_ACTIVATOR).should('exist'); + cy.get(VALUE_LISTS_MODAL_ACTIVATOR).should('not.be.disabled'); +}; + +export const openValueListsModal = () => { + cy.get(VALUE_LISTS_MODAL_ACTIVATOR).click(); +}; + +export const selectValueListsFile = () => { + cy.get(VALUE_LIST_FILE_PICKER).attachFile('value_list.txt').trigger('change', { force: true }); +}; + +export const uploadValueList = () => { + cy.get(VALUE_LIST_FILE_UPLOAD_BUTTON).click(); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx index aab665289e80d..c35cc612129d5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useState, ReactNode, useEffect, useRef } from 'react'; -import styled from 'styled-components'; +import React, { useCallback, useState, useEffect, useRef } from 'react'; import { EuiButton, EuiButtonEmpty, @@ -14,34 +13,30 @@ import { EuiFilePicker, EuiFlexGroup, EuiFlexItem, - EuiRadioGroup, + EuiSelect, + EuiSelectOption, } from '@elastic/eui'; import { useImportList, ListSchema, Type } from '../../../shared_imports'; import * as i18n from './translations'; import { useKibana } from '../../../common/lib/kibana'; -const InlineRadioGroup = styled(EuiRadioGroup)` - display: flex; - - .euiRadioGroup__item + .euiRadioGroup__item { - margin: 0 0 0 12px; - } -`; - -interface ListTypeOptions { - id: Type; - label: ReactNode; -} - -const options: ListTypeOptions[] = [ +const options: EuiSelectOption[] = [ { - id: 'keyword', - label: i18n.KEYWORDS_RADIO, + value: 'keyword', + text: i18n.KEYWORDS_RADIO, }, { - id: 'ip', - label: i18n.IP_RADIO, + value: 'ip', + text: i18n.IP_RADIO, + }, + { + value: 'ip_range', + text: i18n.IP_RANGE_RADIO, + }, + { + value: 'text', + text: i18n.TEXT_RADIO, }, ]; @@ -63,8 +58,10 @@ export const ValueListsFormComponent: React.FC = ({ onError const fileIsValid = !file || validFileTypes.some((fileType) => file.type === fileType); - // EuiRadioGroup's onChange only infers 'string' from our options - const handleRadioChange = useCallback((t: string) => setType(t as Type), [setType]); + const handleRadioChange = useCallback( + (event: React.ChangeEvent) => setType(event.target.value as Type), + [setType] + ); const handleFileChange = useCallback((files: FileList | null) => { setFile(files?.item(0) ?? null); @@ -133,6 +130,7 @@ export const ValueListsFormComponent: React.FC = ({ onError > = ({ onError - - + {importState.loading && ( diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table.tsx index 850716ce54e26..a2e3b73a0abf0 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table.tsx @@ -35,6 +35,7 @@ export const ValueListsTableComponent: React.FC = ({

{i18n.TABLE_TITLE}

{ )} Date: Mon, 27 Jul 2020 18:12:32 +0200 Subject: [PATCH 41/96] [code coverage] add iframe embedded and enterprise search tests (#73267) --- x-pack/scripts/functional_tests.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index ee8af9e040401..c568b92e85515 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -10,6 +10,8 @@ const alwaysImportedTests = [ require.resolve('../test/functional_with_es_ssl/config.ts'), require.resolve('../test/functional/config_security_basic.ts'), require.resolve('../test/functional/config_security_trial.ts'), + require.resolve('../test/functional_embedded/config.ts'), + require.resolve('../test/functional_enterprise_search/without_host_configured.config.ts'), ]; const onlyNotInCoverageTests = [ require.resolve('../test/api_integration/config_security_basic.ts'), @@ -51,9 +53,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/licensing_plugin/config.legacy.ts'), require.resolve('../test/endpoint_api_integration_no_ingest/config.ts'), require.resolve('../test/reporting_api_integration/config.js'), - require.resolve('../test/functional_embedded/config.ts'), require.resolve('../test/ingest_manager_api_integration/config.ts'), - require.resolve('../test/functional_enterprise_search/without_host_configured.config.ts'), ]; require('@kbn/plugin-helpers').babelRegister(); From d9a646113cbd3c520df571b5eaa91364626f6272 Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Mon, 27 Jul 2020 17:36:05 +0100 Subject: [PATCH 42/96] [ML] Fixes raw data drilldowns for Apache, Nginx, Auditbeat modules (#73280) --- .../modules/apache_ecs/ml/low_request_rate_ecs.json | 4 ++-- .../apache_ecs/ml/source_ip_request_rate_ecs.json | 10 ++++------ .../apache_ecs/ml/source_ip_url_count_ecs.json | 10 ++++------ .../modules/apache_ecs/ml/status_code_rate_ecs.json | 11 ++++------- .../modules/apache_ecs/ml/visitor_rate_ecs.json | 4 ++-- .../ml/docker_high_count_process_events_ecs.json | 7 ++----- .../ml/docker_rare_process_activity_ecs.json | 7 ++----- .../ml/hosts_high_count_process_events_ecs.json | 7 ++----- .../ml/hosts_rare_process_activity_ecs.json | 7 ++----- .../modules/nginx_ecs/ml/low_request_rate_ecs.json | 4 ++-- .../nginx_ecs/ml/source_ip_request_rate_ecs.json | 10 ++++------ .../modules/nginx_ecs/ml/source_ip_url_count_ecs.json | 10 ++++------ .../modules/nginx_ecs/ml/status_code_rate_ecs.json | 11 ++++------- .../modules/nginx_ecs/ml/visitor_rate_ecs.json | 4 ++-- 14 files changed, 40 insertions(+), 66 deletions(-) diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/low_request_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/low_request_rate_ecs.json index d6d3879e8300f..5950d088d49e2 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/low_request_rate_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/low_request_rate_ecs.json @@ -1,7 +1,7 @@ { "groups": ["apache"], "description": "HTTP Access Logs: Detect low request rates (ECS)", - "analysis_config" : { + "analysis_config": { "bucket_span": "15m", "summary_count_field_name": "doc_count", "detectors": [ @@ -27,7 +27,7 @@ "custom_urls": [ { "url_name": "Raw data", - "url_value": "discover#/ml_http_access_filebeat_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(columns:!(_source),filters:!((\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.dataset,negate:!f,params:(query:\u0027apache.access\u0027),type:phrase,value:\u0027apache.access\u0027),query:(match:(event.dataset:(query:\u0027apache.access\u0027,type:phrase))))),index:\u0027INDEX_PATTERN_ID\u0027,interval:auto,query:(language:kuery,query:\u0027\u0027),sort:!(\u0027@timestamp\u0027,desc))" + "url_value": "discover#/?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(columns:!(_source),filters:!((\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.dataset,negate:!f,params:(query:\u0027apache.access\u0027),type:phrase,value:\u0027apache.access\u0027),query:(match:(event.dataset:(query:\u0027apache.access\u0027,type:phrase))))),index:\u0027INDEX_PATTERN_ID\u0027,interval:auto,query:(language:kuery,query:\u0027\u0027),sort:!(\u0027@timestamp\u0027,desc))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_request_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_request_rate_ecs.json index 876b89b03952f..f888e4d44c844 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_request_rate_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_request_rate_ecs.json @@ -1,18 +1,16 @@ { "groups": ["apache"], "description": "HTTP Access Logs: Detect unusual source IPs - high request rates (ECS)", - "analysis_config" : { + "analysis_config": { "bucket_span": "1h", "detectors": [ - { + { "detector_description": "Apache access source IP high count", "function": "high_count", "over_field_name": "source.address" } ], - "influencers": [ - "source.address" - ] + "influencers": ["source.address"] }, "data_description": { "time_field": "@timestamp", @@ -27,7 +25,7 @@ }, { "url_name": "Raw data", - "url_value": "discover#/ml_http_access_filebeat_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(columns:!(_source),filters:!((\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.dataset,negate:!f,params:(query:\u0027apache.access\u0027),type:phrase,value:\u0027apache.access\u0027),query:(match:(event.dataset:(query:\u0027apache.access\u0027,type:phrase)))),(\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:source.address,negate:!f,params:(query:\u0027$source.address$\u0027),type:phrase,value:\u0027$source.address$\u0027),query:(match:(source.address:(query:\u0027$source.address$\u0027,type:phrase))))),index:\u0027INDEX_PATTERN_ID\u0027,interval:auto,query:(language:kuery,query:\u0027\u0027),sort:!(\u0027@timestamp\u0027,desc))" + "url_value": "discover#/?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(columns:!(_source),filters:!((\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.dataset,negate:!f,params:(query:\u0027apache.access\u0027),type:phrase,value:\u0027apache.access\u0027),query:(match:(event.dataset:(query:\u0027apache.access\u0027,type:phrase)))),(\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:source.address,negate:!f,params:(query:\u0027$source.address$\u0027),type:phrase,value:\u0027$source.address$\u0027),query:(match:(source.address:(query:\u0027$source.address$\u0027,type:phrase))))),index:\u0027INDEX_PATTERN_ID\u0027,interval:auto,query:(language:kuery,query:\u0027\u0027),sort:!(\u0027@timestamp\u0027,desc))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_url_count_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_url_count_ecs.json index 810c61073ecc6..e4886b531ba42 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_url_count_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_url_count_ecs.json @@ -1,19 +1,17 @@ { "groups": ["apache"], "description": "HTTP Access Logs: Detect unusual source IPs - high distinct count of URLs (ECS)", - "analysis_config" : { + "analysis_config": { "bucket_span": "1h", "detectors": [ - { + { "detector_description": "Apache access source IP high dc URL", "function": "high_distinct_count", "field_name": "url.original", "over_field_name": "source.address" } ], - "influencers": [ - "source.address" - ] + "influencers": ["source.address"] }, "data_description": { "time_field": "@timestamp", @@ -28,7 +26,7 @@ }, { "url_name": "Raw data", - "url_value": "discover#/ml_http_access_filebeat_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(columns:!(_source),filters:!((\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.dataset,negate:!f,params:(query:\u0027apache.access\u0027),type:phrase,value:\u0027apache.access\u0027),query:(match:(event.dataset:(query:\u0027apache.access\u0027,type:phrase)))),(\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:source.address,negate:!f,params:(query:\u0027$source.address$\u0027),type:phrase,value:\u0027$source.address$\u0027),query:(match:(source.address:(query:\u0027$source.address$\u0027,type:phrase))))),index:\u0027INDEX_PATTERN_ID\u0027,interval:auto,query:(language:kuery,query:\u0027\u0027),sort:!(\u0027@timestamp\u0027,desc))" + "url_value": "discover#/?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(columns:!(_source),filters:!((\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.dataset,negate:!f,params:(query:\u0027apache.access\u0027),type:phrase,value:\u0027apache.access\u0027),query:(match:(event.dataset:(query:\u0027apache.access\u0027,type:phrase)))),(\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:source.address,negate:!f,params:(query:\u0027$source.address$\u0027),type:phrase,value:\u0027$source.address$\u0027),query:(match:(source.address:(query:\u0027$source.address$\u0027,type:phrase))))),index:\u0027INDEX_PATTERN_ID\u0027,interval:auto,query:(language:kuery,query:\u0027\u0027),sort:!(\u0027@timestamp\u0027,desc))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/status_code_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/status_code_rate_ecs.json index a9341e43723a6..ac5bd5e478c16 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/status_code_rate_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/status_code_rate_ecs.json @@ -1,19 +1,16 @@ { "groups": ["apache"], "description": "HTTP Access Logs: Detect unusual status code rates (ECS)", - "analysis_config" : { + "analysis_config": { "bucket_span": "15m", "detectors": [ - { + { "detector_description": "Apache access status code rate", "function": "count", "partition_field_name": "http.response.status_code" } ], - "influencers": [ - "http.response.status_code", - "source.address" - ] + "influencers": ["http.response.status_code", "source.address"] }, "analysis_limits": { "model_memory_limit": "100mb" @@ -34,7 +31,7 @@ }, { "url_name": "Raw data", - "url_value": "discover#/ml_http_access_filebeat_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(columns:!(_source),filters:!((\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.dataset,negate:!f,params:(query:\u0027apache.access\u0027),type:phrase,value:\u0027apache.access\u0027),query:(match:(event.dataset:(query:\u0027apache.access\u0027,type:phrase)))),(\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:http.response.status_code,negate:!f,params:(query:\u0027$http.response.status_code$\u0027),type:phrase,value:\u0027$http.response.status_code$\u0027),query:(match:(http.response.status_code:(query:\u0027$http.response.status_code$\u0027,type:phrase))))),index:\u0027INDEX_PATTERN_ID\u0027,interval:auto,query:(language:kuery,query:\u0027\u0027),sort:!(\u0027@timestamp\u0027,desc))" + "url_value": "discover#/?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(columns:!(_source),filters:!((\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.dataset,negate:!f,params:(query:\u0027apache.access\u0027),type:phrase,value:\u0027apache.access\u0027),query:(match:(event.dataset:(query:\u0027apache.access\u0027,type:phrase)))),(\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:http.response.status_code,negate:!f,params:(query:\u0027$http.response.status_code$\u0027),type:phrase,value:\u0027$http.response.status_code$\u0027),query:(match:(http.response.status_code:(query:\u0027$http.response.status_code$\u0027,type:phrase))))),index:\u0027INDEX_PATTERN_ID\u0027,interval:auto,query:(language:kuery,query:\u0027\u0027),sort:!(\u0027@timestamp\u0027,desc))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/visitor_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/visitor_rate_ecs.json index 5bc641315bc3f..f513e53a964f3 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/visitor_rate_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/visitor_rate_ecs.json @@ -1,7 +1,7 @@ { "groups": ["apache"], "description": "HTTP Access Logs: Detect unusual visitor rates (ECS)", - "analysis_config" : { + "analysis_config": { "bucket_span": "15m", "summary_count_field_name": "dc_source_address", "detectors": [ @@ -27,7 +27,7 @@ "custom_urls": [ { "url_name": "Raw data", - "url_value": "discover#/ml_http_access_filebeat_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(columns:!(_source),filters:!((\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.dataset,negate:!f,params:(query:\u0027apache.access\u0027),type:phrase,value:\u0027apache.access\u0027),query:(match:(event.dataset:(query:\u0027apache.access\u0027,type:phrase))))),index:\u0027INDEX_PATTERN_ID\u0027,interval:auto,query:(language:kuery,query:\u0027\u0027),sort:!(\u0027@timestamp\u0027,desc))" + "url_value": "discover#/?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(columns:!(_source),filters:!((\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.dataset,negate:!f,params:(query:\u0027apache.access\u0027),type:phrase,value:\u0027apache.access\u0027),query:(match:(event.dataset:(query:\u0027apache.access\u0027,type:phrase))))),index:\u0027INDEX_PATTERN_ID\u0027,interval:auto,query:(language:kuery,query:\u0027\u0027),sort:!(\u0027@timestamp\u0027,desc))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_high_count_process_events_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_high_count_process_events_ecs.json index 27949c76b3e13..046736b6f5559 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_high_count_process_events_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_high_count_process_events_ecs.json @@ -11,10 +11,7 @@ "partition_field_name": "container.name" } ], - "influencers": [ - "container.name", - "process.executable" - ] + "influencers": ["container.name", "process.executable"] }, "analysis_limits": { "model_memory_limit": "256mb", @@ -35,7 +32,7 @@ { "url_name": "Raw data", "time_range": "1h", - "url_value": "discover#/ml_auditbeat_docker_process_events_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(index:\u0027INDEX_PATTERN_ID\u0027,query:(language:kuery,query:\u0027container.name:\u0022$container.name$\u0022\u0027))" + "url_value": "discover#/?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(index:\u0027INDEX_PATTERN_ID\u0027,query:(language:kuery,query:\u0027container.name:\u0022$container.name$\u0022\u0027))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_rare_process_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_rare_process_activity_ecs.json index 899518f30f7a3..ab405d47484d9 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_rare_process_activity_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_rare_process_activity_ecs.json @@ -12,10 +12,7 @@ "partition_field_name": "container.name" } ], - "influencers": [ - "container.name", - "process.executable" - ] + "influencers": ["container.name", "process.executable"] }, "analysis_limits": { "model_memory_limit": "256mb" @@ -35,7 +32,7 @@ { "url_name": "Raw data", "time_range": "1h", - "url_value": "discover#/ml_auditbeat_docker_process_events_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(index:\u0027INDEX_PATTERN_ID\u0027,query:(language:kuery,query:\u0027container.name:\u0022$container.name$\u0022 AND process.executable:\u0022$process.executable$\u0022\u0027))" + "url_value": "discover#/?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(index:\u0027INDEX_PATTERN_ID\u0027,query:(language:kuery,query:\u0027container.name:\u0022$container.name$\u0022 AND process.executable:\u0022$process.executable$\u0022\u0027))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_high_count_process_events_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_high_count_process_events_ecs.json index 1664e19096ee3..192842309dd92 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_high_count_process_events_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_high_count_process_events_ecs.json @@ -11,10 +11,7 @@ "partition_field_name": "host.name" } ], - "influencers": [ - "host.name", - "process.executable" - ] + "influencers": ["host.name", "process.executable"] }, "analysis_limits": { "model_memory_limit": "256mb" @@ -34,7 +31,7 @@ { "url_name": "Raw data", "time_range": "1h", - "url_value": "discover#/ml_auditbeat_hosts_process_events_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(index:\u0027INDEX_PATTERN_ID\u0027,query:(language:kuery,query:\u0027host.name:\u0022$host.name$\u0022\u0027))" + "url_value": "discover#/?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(index:\u0027INDEX_PATTERN_ID\u0027,query:(language:kuery,query:\u0027host.name:\u0022$host.name$\u0022\u0027))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_rare_process_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_rare_process_activity_ecs.json index d83f36db5a491..9448537b387c2 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_rare_process_activity_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_rare_process_activity_ecs.json @@ -12,10 +12,7 @@ "partition_field_name": "host.name" } ], - "influencers": [ - "host.name", - "process.executable" - ] + "influencers": ["host.name", "process.executable"] }, "analysis_limits": { "model_memory_limit": "256mb" @@ -35,7 +32,7 @@ { "url_name": "Raw data", "time_range": "1h", - "url_value": "discover#/ml_auditbeat_hosts_process_events_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(index:\u0027INDEX_PATTERN_ID\u0027,query:(language:kuery,query:\u0027host.name:\u0022$host.name$\u0022 AND process.executable:\u0022$process.executable$\u0022\u0027))" + "url_value": "discover#/?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(index:\u0027INDEX_PATTERN_ID\u0027,query:(language:kuery,query:\u0027host.name:\u0022$host.name$\u0022 AND process.executable:\u0022$process.executable$\u0022\u0027))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/low_request_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/low_request_rate_ecs.json index 54c2f540e334f..3dfe04766a9e9 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/low_request_rate_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/low_request_rate_ecs.json @@ -1,7 +1,7 @@ { "groups": ["nginx"], "description": "HTTP Access Logs: Detect low request rates (ECS)", - "analysis_config" : { + "analysis_config": { "bucket_span": "15m", "summary_count_field_name": "doc_count", "detectors": [ @@ -27,7 +27,7 @@ "custom_urls": [ { "url_name": "Raw data", - "url_value": "discover#/ml_http_access_filebeat_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(columns:!(_source),filters:!((\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.dataset,negate:!f,params:(query:\u0027nginx.access\u0027),type:phrase,value:\u0027nginx.access\u0027),query:(match:(event.dataset:(query:\u0027nginx.access\u0027,type:phrase))))),index:\u0027INDEX_PATTERN_ID\u0027,interval:auto,query:(language:kuery,query:\u0027\u0027),sort:!(\u0027@timestamp\u0027,desc))" + "url_value": "discover#/?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(columns:!(_source),filters:!((\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.dataset,negate:!f,params:(query:\u0027nginx.access\u0027),type:phrase,value:\u0027nginx.access\u0027),query:(match:(event.dataset:(query:\u0027nginx.access\u0027,type:phrase))))),index:\u0027INDEX_PATTERN_ID\u0027,interval:auto,query:(language:kuery,query:\u0027\u0027),sort:!(\u0027@timestamp\u0027,desc))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_request_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_request_rate_ecs.json index 6fc7ce7e0699d..209b4e66dbac4 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_request_rate_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_request_rate_ecs.json @@ -1,18 +1,16 @@ { "groups": ["nginx"], "description": "HTTP Access Logs: Detect unusual source IPs - high request rates (ECS)", - "analysis_config" : { + "analysis_config": { "bucket_span": "1h", "detectors": [ - { + { "detector_description": "Nginx access source IP high count", "function": "high_count", "over_field_name": "source.address" } ], - "influencers": [ - "source.address" - ] + "influencers": ["source.address"] }, "data_description": { "time_field": "@timestamp", @@ -27,7 +25,7 @@ }, { "url_name": "Raw data", - "url_value": "discover#/ml_http_access_filebeat_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(columns:!(_source),filters:!((\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.dataset,negate:!f,params:(query:\u0027nginx.access\u0027),type:phrase,value:\u0027nginx.access\u0027),query:(match:(event.dataset:(query:\u0027nginx.access\u0027,type:phrase)))),(\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:source.address,negate:!f,params:(query:\u0027$source.address$\u0027),type:phrase,value:\u0027$source.address$\u0027),query:(match:(source.address:(query:\u0027$source.address$\u0027,type:phrase))))),index:\u0027INDEX_PATTERN_ID\u0027,interval:auto,query:(language:kuery,query:\u0027\u0027),sort:!(\u0027@timestamp\u0027,desc))" + "url_value": "discover#/?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(columns:!(_source),filters:!((\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.dataset,negate:!f,params:(query:\u0027nginx.access\u0027),type:phrase,value:\u0027nginx.access\u0027),query:(match:(event.dataset:(query:\u0027nginx.access\u0027,type:phrase)))),(\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:source.address,negate:!f,params:(query:\u0027$source.address$\u0027),type:phrase,value:\u0027$source.address$\u0027),query:(match:(source.address:(query:\u0027$source.address$\u0027,type:phrase))))),index:\u0027INDEX_PATTERN_ID\u0027,interval:auto,query:(language:kuery,query:\u0027\u0027),sort:!(\u0027@timestamp\u0027,desc))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_url_count_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_url_count_ecs.json index 1c3f9f96a36b4..dea65ef701cb1 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_url_count_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_url_count_ecs.json @@ -1,19 +1,17 @@ { "groups": ["nginx"], "description": "HTTP Access Logs: Detect unusual source IPs - high distinct count of URLs (ECS)", - "analysis_config" : { + "analysis_config": { "bucket_span": "1h", "detectors": [ - { + { "detector_description": "Nginx access source IP high dc URL", "function": "high_distinct_count", "field_name": "url.original", "over_field_name": "source.address" } ], - "influencers": [ - "source.address" - ] + "influencers": ["source.address"] }, "data_description": { "time_field": "@timestamp", @@ -28,7 +26,7 @@ }, { "url_name": "Raw data", - "url_value": "discover#/ml_http_access_filebeat_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(columns:!(_source),filters:!((\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.dataset,negate:!f,params:(query:\u0027nginx.access\u0027),type:phrase,value:\u0027nginx.access\u0027),query:(match:(event.dataset:(query:\u0027nginx.access\u0027,type:phrase)))),(\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:source.address,negate:!f,params:(query:\u0027$source.address$\u0027),type:phrase,value:\u0027$source.address$\u0027),query:(match:(source.address:(query:\u0027$source.address$\u0027,type:phrase))))),index:\u0027INDEX_PATTERN_ID\u0027,interval:auto,query:(language:kuery,query:\u0027\u0027),sort:!(\u0027@timestamp\u0027,desc))" + "url_value": "discover#/?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(columns:!(_source),filters:!((\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.dataset,negate:!f,params:(query:\u0027nginx.access\u0027),type:phrase,value:\u0027nginx.access\u0027),query:(match:(event.dataset:(query:\u0027nginx.access\u0027,type:phrase)))),(\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:source.address,negate:!f,params:(query:\u0027$source.address$\u0027),type:phrase,value:\u0027$source.address$\u0027),query:(match:(source.address:(query:\u0027$source.address$\u0027,type:phrase))))),index:\u0027INDEX_PATTERN_ID\u0027,interval:auto,query:(language:kuery,query:\u0027\u0027),sort:!(\u0027@timestamp\u0027,desc))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/status_code_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/status_code_rate_ecs.json index df917ed43c5fa..2475b33aa24f2 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/status_code_rate_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/status_code_rate_ecs.json @@ -1,19 +1,16 @@ { "groups": ["nginx"], "description": "HTTP Access Logs: Detect unusual status code rates (ECS)", - "analysis_config" : { + "analysis_config": { "bucket_span": "15m", "detectors": [ - { + { "detector_description": "Nginx access status code rate", "function": "count", "partition_field_name": "http.response.status_code" } ], - "influencers": [ - "http.response.status_code", - "source.address" - ] + "influencers": ["http.response.status_code", "source.address"] }, "analysis_limits": { "model_memory_limit": "100mb" @@ -34,7 +31,7 @@ }, { "url_name": "Raw data", - "url_value": "discover#/ml_http_access_filebeat_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(columns:!(_source),filters:!((\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.dataset,negate:!f,params:(query:\u0027nginx.access\u0027),type:phrase,value:\u0027nginx.access\u0027),query:(match:(event.dataset:(query:\u0027nginx.access\u0027,type:phrase)))),(\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:http.response.status_code,negate:!f,params:(query:\u0027$http.response.status_code$\u0027),type:phrase,value:\u0027$http.response.status_code$\u0027),query:(match:(http.response.status_code:(query:\u0027$http.response.status_code$\u0027,type:phrase))))),index:\u0027INDEX_PATTERN_ID\u0027,interval:auto,query:(language:kuery,query:\u0027\u0027),sort:!(\u0027@timestamp\u0027,desc))" + "url_value": "discover#/?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(columns:!(_source),filters:!((\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.dataset,negate:!f,params:(query:\u0027nginx.access\u0027),type:phrase,value:\u0027nginx.access\u0027),query:(match:(event.dataset:(query:\u0027nginx.access\u0027,type:phrase)))),(\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:http.response.status_code,negate:!f,params:(query:\u0027$http.response.status_code$\u0027),type:phrase,value:\u0027$http.response.status_code$\u0027),query:(match:(http.response.status_code:(query:\u0027$http.response.status_code$\u0027,type:phrase))))),index:\u0027INDEX_PATTERN_ID\u0027,interval:auto,query:(language:kuery,query:\u0027\u0027),sort:!(\u0027@timestamp\u0027,desc))" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/visitor_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/visitor_rate_ecs.json index 5ff35a7e2aed7..3182ac3fd3a79 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/visitor_rate_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/visitor_rate_ecs.json @@ -1,7 +1,7 @@ { "groups": ["nginx"], "description": "HTTP Access Logs: Detect unusual visitor rates (ECS)", - "analysis_config" : { + "analysis_config": { "bucket_span": "15m", "summary_count_field_name": "dc_source_address", "detectors": [ @@ -27,7 +27,7 @@ "custom_urls": [ { "url_name": "Raw data", - "url_value": "discover#/ml_http_access_filebeat_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(columns:!(_source),filters:!((\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.dataset,negate:!f,params:(query:\u0027nginx.access\u0027),type:phrase,value:\u0027nginx.access\u0027),query:(match:(event.dataset:(query:\u0027nginx.access\u0027,type:phrase))))),index:\u0027INDEX_PATTERN_ID\u0027,interval:auto,query:(language:kuery,query:\u0027\u0027),sort:!(\u0027@timestamp\u0027,desc))" + "url_value": "discover#/?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(columns:!(_source),filters:!((\u0027$state\u0027:(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.dataset,negate:!f,params:(query:\u0027nginx.access\u0027),type:phrase,value:\u0027nginx.access\u0027),query:(match:(event.dataset:(query:\u0027nginx.access\u0027,type:phrase))))),index:\u0027INDEX_PATTERN_ID\u0027,interval:auto,query:(language:kuery,query:\u0027\u0027),sort:!(\u0027@timestamp\u0027,desc))" } ] } From 46dcc0bd4929b406ec01012a4465f1f3d892ea67 Mon Sep 17 00:00:00 2001 From: Katrin Freihofner Date: Mon, 27 Jul 2020 19:10:03 +0200 Subject: [PATCH 43/96] Adds styling changes to uptime overview and details page (#71840) Co-authored-by: Elastic Machine --- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../ping_histogram.test.tsx.snap | 9 ++- .../common/charts/duration_chart.tsx | 7 +-- .../common/charts/ping_histogram.tsx | 9 +-- .../ml_integerations.test.tsx.snap | 6 +- .../__snapshots__/ml_manage_job.test.tsx.snap | 6 +- .../components/monitor/ml/manage_ml_job.tsx | 10 ++-- .../components/monitor/monitor_charts.tsx | 2 +- .../monitor_duration/monitor_duration.tsx | 9 +-- .../__snapshots__/ping_list.test.tsx.snap | 2 +- .../monitor/ping_list/ping_list.tsx | 2 +- .../monitor_status.bar.test.tsx.snap | 10 ++-- .../status_by_location.test.tsx.snap | 58 ++++++++----------- .../status_bar/status_by_location.tsx | 10 ++-- .../__snapshots__/monitor_list.test.tsx.snap | 47 +++++---------- .../monitor_list/monitor_list_header.tsx | 38 ++++-------- 17 files changed, 90 insertions(+), 137 deletions(-) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8baebbb4939be..cf79f463b35cb 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -16283,7 +16283,6 @@ "xpack.uptime.ml.enableAnomalyDetectionPanel.manageMLJobDescription.noteText": "注:ジョブが結果の計算を開始するまでに少し時間がかかる場合があります。", "xpack.uptime.ml.enableAnomalyDetectionPanel.startTrial": "無料の 14 日トライアルを開始", "xpack.uptime.ml.enableAnomalyDetectionPanel.startTrialDesc": "期間異常検知機能を利用するには、Elastic Platinum ライセンスが必要です。", - "xpack.uptime.monitorCharts.durationChart.bottomAxis.title": "タイムスタンプ", "xpack.uptime.monitorCharts.durationChart.leftAxis.title": "期間ms", "xpack.uptime.monitorCharts.monitorDuration.titleLabel": "監視期間", "xpack.uptime.monitorCharts.monitorDuration.titleLabelWithAnomaly": "監視期間 (異常: {noOfAnomalies})", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5b81804faf715..b45fe1baa9e9a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -16290,7 +16290,6 @@ "xpack.uptime.ml.enableAnomalyDetectionPanel.manageMLJobDescription.noteText": "注意:可能要过几分钟后,作业才会开始计算结果。", "xpack.uptime.ml.enableAnomalyDetectionPanel.startTrial": "开始为期 14 天的免费试用", "xpack.uptime.ml.enableAnomalyDetectionPanel.startTrialDesc": "要访问持续时间异常检测,必须订阅 Elastic 白金级许可证。", - "xpack.uptime.monitorCharts.durationChart.bottomAxis.title": "鏃堕棿鎴", "xpack.uptime.monitorCharts.durationChart.leftAxis.title": "持续时间 (ms)", "xpack.uptime.monitorCharts.monitorDuration.titleLabel": "监测持续时间(毫秒)", "xpack.uptime.monitorCharts.monitorDuration.titleLabelWithAnomaly": "监测持续时间(异常:{noOfAnomalies})", diff --git a/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/ping_histogram.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/ping_histogram.test.tsx.snap index fe20071ced4cb..7fdb2e4ede75b 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/ping_histogram.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/ping_histogram.test.tsx.snap @@ -2,11 +2,14 @@ exports[`PingHistogram component renders the component without errors 1`] = ` Array [ -

Pings over time -

, + , +
,
getTickFormat(d)} title={i18n.translate('xpack.uptime.monitorCharts.durationChart.leftAxis.title', { - defaultMessage: 'Duration ms', + defaultMessage: 'Duration in ms', })} /> diff --git a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx index d5f3b1b164ad9..39b8a38f60982 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx @@ -13,7 +13,7 @@ import { timeFormatter, BrushEndListener, } from '@elastic/charts'; -import { EuiTitle } from '@elastic/eui'; +import { EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useContext } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -173,14 +173,15 @@ export const PingHistogramComponent: React.FC = ({ return ( <> - -

+ +

-

+

+ {content} ); diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap index 24c4e818a0592..15f5c03512bf1 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap @@ -8,18 +8,18 @@ exports[`ML Integrations renders without errors 1`] = ` class="euiPopover__anchor" >
,
-

- Down in 2 Locations -

-
+ Down in 2 locations + `; exports[`StatusByLocation component renders properly against props 1`] = ` - +

-
+ `; exports[`StatusByLocation component renders when down in some locations 1`] = ` -
-

- Down in 1/2 Locations -

-
+ Down in 1/2 locations + `; exports[`StatusByLocation component renders when only one location and it is down 1`] = ` -
-

- Down in 1 Location -

-
+ Down in 1 location + `; exports[`StatusByLocation component renders when only one location and it is up 1`] = ` -
-

- Up in 1 Location -

-
+ Up in 1 location + `; exports[`StatusByLocation component renders when up in all locations 1`] = ` -
-

- Up in 2 Locations -

-
+ Up in 2 locations + `; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_by_location.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_by_location.tsx index 461ffc10124fd..fb2a55bb4059b 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_by_location.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_by_location.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { EuiText } from '@elastic/eui'; +import { EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { MonitorLocation } from '../../../../../common/runtime_types'; @@ -43,7 +43,7 @@ export const StatusByLocations = ({ locations }: StatusByLocationsProps) => { } return ( - +

{locations.length <= 1 ? ( { status, loc: statusMessage, }} - defaultMessage="{status} in {loc} Location" + defaultMessage="{status} in {loc} location" /> ) : ( { status, loc: statusMessage, }} - defaultMessage="{status} in {loc} Locations" + defaultMessage="{status} in {loc} locations" /> )}

-
+ ); }; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap index b6ce1eceb62a7..42ac821c10c7a 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap @@ -813,7 +813,13 @@ exports[`MonitorList component renders the monitor list 1`] = ` } .c1 { - margin-left: auto; + position: absolute; + right: 16px; + top: 16px; +} + +.c0 { + position: relative; } .c4 { @@ -828,26 +834,6 @@ exports[`MonitorList component renders the monitor list 1`] = ` } } -@media only screen and (max-width:768px) { - .c0.c0 > :first-child { - -webkit-flex-basis: 40% !important; - -ms-flex-preferred-size: 40% !important; - flex-basis: 40% !important; - } - - .c0.c0 > :nth-child(2) { - -webkit-order: 3; - -ms-flex-order: 3; - order: 3; - } - - .c0.c0 > :nth-child(3) { - -webkit-flex-basis: 60% !important; - -ms-flex-preferred-size: 60% !important; - flex-basis: 60% !important; - } -} -
@@ -936,20 +922,13 @@ exports[`MonitorList component renders the monitor list 1`] = `
-
+ Certificates status +